diff --git a/finlib/src/ffi/py/mod.rs b/finlib/src/ffi/py/mod.rs index 20e1ebf..8f7a9c1 100644 --- a/finlib/src/ffi/py/mod.rs +++ b/finlib/src/ffi/py/mod.rs @@ -1 +1,2 @@ -pub mod portfolio; \ No newline at end of file +pub mod portfolio; +pub mod options; \ No newline at end of file diff --git a/finlib/src/ffi/py/options.rs b/finlib/src/ffi/py/options.rs new file mode 100644 index 0000000..aab186b --- /dev/null +++ b/finlib/src/ffi/py/options.rs @@ -0,0 +1,21 @@ +use pyo3::prelude::*; +use crate::options::blackscholes::{OptionVariables, OptionGreeks}; +use crate::risk::portfolio::{Portfolio, PortfolioAsset}; + +#[pymethods] +impl OptionVariables { + #[new] + pub fn init(underlying_price: f64, + strike_price: f64, + volatility: f64, + risk_free_interest_rate: f64, + dividend: f64, + time_to_expiration: f64) -> Self { + OptionVariables::from(underlying_price, + strike_price, + volatility, + risk_free_interest_rate, + dividend, + time_to_expiration) + } +} \ No newline at end of file diff --git a/finlib/src/options/blackscholes/OptionSurface.rs b/finlib/src/options/blackscholes/OptionSurface.rs new file mode 100644 index 0000000..bcb5c3e --- /dev/null +++ b/finlib/src/options/blackscholes/OptionSurface.rs @@ -0,0 +1,174 @@ +use core::ops::Range; +use std::sync::{Arc, Mutex}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use crate::options::blackscholes::OptionVariables; + +pub struct OptionSurface { + underlying_price: Range<isize>, + underlying_price_bounds: (f64, f64), + strike_price: Range<isize>, + strike_price_bounds: (f64, f64), + volatility: Range<isize>, + volatility_bounds: (f64, f64), + risk_free_interest_rate: Range<isize>, + risk_free_interest_rate_bounds: (f64, f64), + dividend: Range<isize>, + dividend_bounds: (f64, f64), + time_to_expiration: Range<isize>, + time_to_expiration_bounds: (f64, f64) +} + +impl OptionSurface { + pub fn from(underlying_price: Range<isize>, + underlying_price_bounds: (f64, f64), + strike_price: Range<isize>, + strike_price_bounds: (f64, f64), + volatility: Range<isize>, + volatility_bounds: (f64, f64), + risk_free_interest_rate: Range<isize>, + risk_free_interest_rate_bounds: (f64, f64), + dividend: Range<isize>, + dividend_bounds: (f64, f64), + time_to_expiration: Range<isize>, + time_to_expiration_bounds: (f64, f64)) -> Self { + Self { + underlying_price, + underlying_price_bounds, + strike_price, + strike_price_bounds, + volatility, + volatility_bounds, + risk_free_interest_rate, + risk_free_interest_rate_bounds, + dividend, + dividend_bounds, + time_to_expiration, + time_to_expiration_bounds, + } + } + + pub fn walk(self) -> Vec<OptionVariables> { + + // #[cfg(feature = "parallel")] + // { + // let vec: Arc<Mutex<Vec<OptionVariables>>> = Arc::new(Mutex::new(vec![])); + // self.underlying_price + // .into_par_iter() + // .for_each(|p| { + // self.strike_price + // .clone() + // .into_par_iter() + // .for_each(|s| { + // self.volatility + // .clone() + // .into_par_iter() + // .for_each(|v| { + // self.risk_free_interest_rate + // .clone() + // .into_par_iter() + // .for_each(|i| { + // self.dividend + // .clone() + // .into_par_iter() + // .for_each(|d| { + // self.time_to_expiration + // .clone() + // .into_par_iter() + // .for_each(|t| { + // let mut m = vec.clone(); + // let mut guard = m.lock().unwrap(); + // guard.push(OptionVariables::from( + // self.underlying_price_bounds.0 + (self.underlying_price_bounds.1 - self.underlying_price_bounds.0) * p as f64, + // self.strike_price_bounds.0 + (self.strike_price_bounds.1 - self.strike_price_bounds.0) * s as f64, + // self.volatility_bounds.0 + (self.volatility_bounds.1 - self.volatility_bounds.0) * v as f64, + // self.risk_free_interest_rate_bounds.0 + (self.risk_free_interest_rate_bounds.1 - self.risk_free_interest_rate_bounds.0) * i as f64, + // self.dividend_bounds.0 + (self.dividend_bounds.1 - self.dividend_bounds.0) * d as f64, + // self.time_to_expiration_bounds.0 + (self.time_to_expiration_bounds.1 - self.time_to_expiration_bounds.0) * t as f64 + // )); + // }) + // }) + // }) + // }) + // }) + // }); + // + // Arc::try_unwrap(vec).unwrap().into_inner().unwrap() + // } + // #[cfg(not(feature = "parallel"))] + { + let mut vec: Vec<OptionVariables> = Vec::with_capacity( + self.underlying_price.len() + * self.strike_price.len() + * self.volatility.len() + * self.risk_free_interest_rate.len() + * self.dividend.len() + * self.time_to_expiration.len() + ); + for p in self.underlying_price { + for s in self.strike_price.clone() { + for v in self.volatility.clone() { + for i in self.risk_free_interest_rate.clone() { + for d in self.dividend.clone() { + for t in self.time_to_expiration.clone() { + let v = OptionVariables::from( + self.underlying_price_bounds.0 + (self.underlying_price_bounds.1 - self.underlying_price_bounds.0) * p as f64, + self.strike_price_bounds.0 + (self.strike_price_bounds.1 - self.strike_price_bounds.0) * s as f64, + self.volatility_bounds.0 + (self.volatility_bounds.1 - self.volatility_bounds.0) * v as f64, + self.risk_free_interest_rate_bounds.0 + (self.risk_free_interest_rate_bounds.1 - self.risk_free_interest_rate_bounds.0) * i as f64, + self.dividend_bounds.0 + (self.dividend_bounds.1 - self.dividend_bounds.0) * d as f64, + self.time_to_expiration_bounds.0 + (self.time_to_expiration_bounds.1 - self.time_to_expiration_bounds.0) * t as f64 + ); + vec.push(v); + } + } + } + } + } + } + + vec + } + } +} + +#[cfg(test)] +mod tests { + use crate::options::blackscholes::{CallOption, Option, PutOption}; + use super::*; + + #[test] + fn walk_test() { + let w = OptionSurface::from( + (0 .. 50), + (100., 200.), + (0 .. 50), + (100., 200.), + (0 .. 5), + (0.25, 0.50), + (0 .. 10), + (0.05, 0.08), + (0 .. 1), + (0.01, 0.02), + (0 .. 10), + (30./365.25, 30./365.25), + ); + + let a = w.walk(); + + let options = a + .par_iter() + .map(|v| { + let mut call = v.call(); + let mut put = v.put(); + + call.calc_greeks(); + put.calc_greeks(); + + (call, put) + }) + .collect::<Vec<(CallOption, PutOption)>>(); + + let a1 = a.first(); + } +} \ No newline at end of file diff --git a/finlib/src/options/blackscholes/mod.rs b/finlib/src/options/blackscholes/mod.rs index a45eba5..7235352 100644 --- a/finlib/src/options/blackscholes/mod.rs +++ b/finlib/src/options/blackscholes/mod.rs @@ -1,5 +1,14 @@ -use statrs::distribution::{ContinuousCDF, Normal}; +mod OptionSurface; +use statrs::distribution::{Continuous, ContinuousCDF, Normal}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; +#[cfg(feature = "py")] +use pyo3::prelude::*; + +#[cfg_attr(feature = "wasm", wasm_bindgen)] +#[cfg_attr(feature = "py", pyclass)] +#[cfg_attr(feature = "ffi", repr(C))] #[derive(Debug, Copy, Clone, Default, PartialEq, PartialOrd)] pub struct OptionVariables { underlying_price: f64, @@ -8,7 +17,8 @@ pub struct OptionVariables { risk_free_interest_rate: f64, dividend: f64, time_to_expiration: f64, - d1: std::option::Option<f64> + d1: std::option::Option<f64>, + d2: std::option::Option<f64> } impl OptionVariables { @@ -26,7 +36,8 @@ impl OptionVariables { risk_free_interest_rate, dividend, time_to_expiration, - d1: None + d1: None, + d2: None } } @@ -34,6 +45,7 @@ impl OptionVariables { let n = Normal::new(0., 1.0).unwrap(); let (d1, d2) = self.d1_d2(); self.d1 = Some(d1); + self.d2 = Some(d2); let first = self.underlying_price * (-self.dividend * self.time_to_expiration).exp() * n.cdf(d1); @@ -47,6 +59,7 @@ impl OptionVariables { let n = Normal::new(0., 1.0).unwrap(); let (d1, d2) = self.d1_d2(); self.d1 = Some(d1); + self.d2 = Some(d2); let first = self.strike_price * (-self.risk_free_interest_rate * self.time_to_expiration).exp() * n.cdf(-d2); @@ -72,7 +85,7 @@ impl OptionVariables { (first + second) / denominator } - pub fn d2(&self, d1: f64, ) -> f64 { + pub fn d2(&self, d1: f64) -> f64 { d1 - (self.volatility * f64::sqrt(self.time_to_expiration)) } } @@ -83,17 +96,23 @@ pub trait Option { fn vega(&self) -> f64; fn theta(&self) -> f64; fn rho(&self) -> f64; + fn calc_greeks(&mut self); + fn has_greeks(&self) -> bool; } +// #[cfg_attr(feature = "wasm", wasm_bindgen)] +#[cfg_attr(feature = "py", pyclass)] +#[cfg_attr(feature = "ffi", repr(C))] #[derive(Debug, Copy, Clone, Default, PartialEq, PartialOrd)] pub struct CallOption { pub price: f64, - pub variables: OptionVariables + pub variables: OptionVariables, + pub greeks: std::option::Option<OptionGreeks> } impl CallOption { pub fn from(price: f64, variables: OptionVariables) -> Self { - Self { price, variables } + Self { price, variables, greeks: None } } } @@ -113,23 +132,50 @@ impl Option for CallOption { } fn theta(&self) -> f64 { - todo!() + let n = Normal::new(0., 1.0).unwrap(); + let first = theta_first(&self.variables, &n); + + let second = self.variables.risk_free_interest_rate + * self.variables.strike_price + * (-self.variables.risk_free_interest_rate * self.variables.time_to_expiration).exp() + * n.cdf(self.variables.d2.unwrap()); + + let third = self.variables.dividend + * self.variables.underlying_price + * (-self.variables.dividend * self.variables.time_to_expiration).exp() + * n.cdf(self.variables.d1.unwrap()); + + first - second + third } fn rho(&self) -> f64 { - todo!() + let n = Normal::new(0., 1.0).unwrap(); + + self.variables.strike_price * self.variables.time_to_expiration * (-self.variables.risk_free_interest_rate * self.variables.time_to_expiration).exp() * n.cdf(self.variables.d2.unwrap()) + } + + fn calc_greeks(&mut self) { + self.greeks = Some(OptionGreeks::from(self)); + } + + fn has_greeks(&self) -> bool { + self.greeks.is_some() } } +// #[cfg_attr(feature = "wasm", wasm_bindgen)] +#[cfg_attr(feature = "py", pyclass)] +#[cfg_attr(feature = "ffi", repr(C))] #[derive(Debug, Copy, Clone, Default, PartialEq, PartialOrd)] pub struct PutOption { pub price: f64, - pub variables: OptionVariables + pub variables: OptionVariables, + pub greeks: std::option::Option<OptionGreeks> } impl PutOption { pub fn from(price: f64, variables: OptionVariables) -> Self { - Self { price, variables } + Self { price, variables, greeks: None } } } @@ -149,12 +195,42 @@ impl Option for PutOption { } fn theta(&self) -> f64 { - todo!() + let n = Normal::new(0., 1.0).unwrap(); + let first = theta_first(&self.variables, &n); + + let second = self.variables.risk_free_interest_rate + * self.variables.strike_price + * (-self.variables.risk_free_interest_rate * self.variables.time_to_expiration).exp() + * n.cdf(-self.variables.d2.unwrap()); + + let third = self.variables.dividend + * self.variables.underlying_price + * (-self.variables.dividend * self.variables.time_to_expiration).exp() + * n.cdf(-self.variables.d1.unwrap()); + + first + second - third } fn rho(&self) -> f64 { - todo!() + let n = Normal::new(0., 1.0).unwrap(); + + - self.variables.strike_price * self.variables.time_to_expiration * (-self.variables.risk_free_interest_rate * self.variables.time_to_expiration).exp() * n.cdf(-self.variables.d2.unwrap()) } + + fn calc_greeks(&mut self) { + self.greeks = Some(OptionGreeks::from(self)); + } + + fn has_greeks(&self) -> bool { + self.greeks.is_some() + } +} + +fn theta_first(v: &OptionVariables, n: &Normal) -> f64 { + let numerator = v.underlying_price * v.volatility * (-v.dividend * v.time_to_expiration).exp(); + let denominator = 2. * f64::sqrt(v.time_to_expiration); + + -(numerator / denominator) * n.pdf(v.d1.unwrap()) } pub fn gamma(v: &OptionVariables) -> f64 { @@ -163,7 +239,7 @@ pub fn gamma(v: &OptionVariables) -> f64 { let numerator = (-v.dividend * v.time_to_expiration).exp(); let denominator = v.underlying_price * v.volatility * f64::sqrt(v.time_to_expiration); - (numerator / denominator) * n.cdf(v.d1.unwrap()) + (numerator / denominator) * n.pdf(v.d1.unwrap()) } pub fn vega(v: &OptionVariables) -> f64 { @@ -171,13 +247,39 @@ pub fn vega(v: &OptionVariables) -> f64 { let numerator = (-v.dividend * v.time_to_expiration).exp(); - v.underlying_price * numerator * f64::sqrt(v.time_to_expiration) * n.cdf(v.d1.unwrap()) / 100. + v.underlying_price * numerator * f64::sqrt(v.time_to_expiration) * n.pdf(v.d1.unwrap()) +} + +#[cfg_attr(feature = "wasm", wasm_bindgen)] +#[cfg_attr(feature = "py", pyclass)] +#[cfg_attr(feature = "ffi", repr(C))] +#[derive(Debug, Copy, Clone, Default, PartialEq, PartialOrd)] +pub struct OptionGreeks { + delta: f64, + gamma: f64, + vega: f64, + theta: f64, + rho: f64 +} + +impl OptionGreeks { + pub fn from(option: &impl Option) -> Self { + Self { + delta: option.delta(), + gamma: option.gamma(), + vega: option.vega(), + theta: option.theta(), + rho: option.rho() + } + } } #[cfg(test)] mod tests { use super::*; + // https://goodcalculators.com/black-scholes-calculator/ + fn get_example_option() -> OptionVariables { OptionVariables::from(100., 100., 0.25, 0.05, 0.01, 30./365.25) } @@ -250,4 +352,21 @@ mod tests { let diff = (rho - -4.060).abs(); assert!(diff < 0.01); } + + #[test] + fn call_theta_test() { + let v = get_example_option(); + + let diff = (v.call().theta() - -19.300).abs(); + assert!(diff < 0.01); + } + + #[test] + fn put_theta_test() { + let v = get_example_option(); + + let theta = v.put().theta(); + let diff = (theta - -15.319).abs(); + assert!(diff < 0.01); + } } \ No newline at end of file diff --git a/pyfinlib/src/lib.rs b/pyfinlib/src/lib.rs index 173cf4c..285e481 100644 --- a/pyfinlib/src/lib.rs +++ b/pyfinlib/src/lib.rs @@ -25,6 +25,20 @@ mod pyfinlib { } } + #[pymodule] + mod options { + use super::*; + + #[pymodule_export] + use finlib::options::blackscholes::OptionVariables; + #[pymodule_export] + use finlib::options::blackscholes::CallOption; + #[pymodule_export] + use finlib::options::blackscholes::PutOption; + #[pymodule_export] + use finlib::options::blackscholes::OptionGreeks; + } + #[pymodule] mod risk { use super::*;