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/lib.rs b/finlib/src/lib.rs index 7dc8d0d..5024e04 100644 --- a/finlib/src/lib.rs +++ b/finlib/src/lib.rs @@ -5,6 +5,7 @@ pub mod stats; pub mod util; pub mod risk; pub mod ffi; +pub mod options; #[cfg(feature = "parallel")] use rayon::prelude::*; 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 new file mode 100644 index 0000000..7235352 --- /dev/null +++ b/finlib/src/options/blackscholes/mod.rs @@ -0,0 +1,372 @@ +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, + strike_price: f64, + volatility: f64, + risk_free_interest_rate: f64, + dividend: f64, + time_to_expiration: f64, + d1: std::option::Option<f64>, + d2: std::option::Option<f64> +} + +impl OptionVariables { + + pub fn from(underlying_price: f64, + strike_price: f64, + volatility: f64, + risk_free_interest_rate: f64, + dividend: f64, + time_to_expiration: f64) -> Self { + Self { + underlying_price, + strike_price, + volatility, + risk_free_interest_rate, + dividend, + time_to_expiration, + d1: None, + d2: None + } + } + + pub fn call(mut self) -> CallOption { + 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); + + let second = self.strike_price * (-self.risk_free_interest_rate * self.time_to_expiration).exp() * n.cdf(d2); + + CallOption::from(first - second, self) + } + + + pub fn put(mut self) -> PutOption { + 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); + + let second = self.underlying_price * (-self.dividend * self.time_to_expiration).exp() * n.cdf(-d1); + + PutOption::from(first - second, self) + } + + pub fn d1_d2(&self) -> (f64, f64) { + let d1 = self.d1(); + + (d1, self.d2(d1)) + } + + pub fn d1(&self) -> f64 { + + let first = (self.underlying_price / self.strike_price).log(std::f64::consts::E); + + let second = self.time_to_expiration * (self.risk_free_interest_rate - self.dividend + (f64::powi(self.volatility, 2) / 2.)); + + let denominator = self.volatility * f64::sqrt(self.time_to_expiration); + + (first + second) / denominator + } + + pub fn d2(&self, d1: f64) -> f64 { + d1 - (self.volatility * f64::sqrt(self.time_to_expiration)) + } +} + +pub trait Option { + fn delta(&self) -> f64; + fn gamma(&self) -> f64; + 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 greeks: std::option::Option<OptionGreeks> +} + +impl CallOption { + pub fn from(price: f64, variables: OptionVariables) -> Self { + Self { price, variables, greeks: None } + } +} + +impl Option for CallOption { + fn delta(&self) -> f64 { + let n = Normal::new(0., 1.0).unwrap(); + + (-self.variables.dividend * self.variables.time_to_expiration).exp() * n.cdf(self.variables.d1.unwrap()) + } + + fn gamma(&self) -> f64 { + gamma(&self.variables) + } + + fn vega(&self) -> f64 { + vega(&self.variables) + } + + fn theta(&self) -> f64 { + 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 { + 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 greeks: std::option::Option<OptionGreeks> +} + +impl PutOption { + pub fn from(price: f64, variables: OptionVariables) -> Self { + Self { price, variables, greeks: None } + } +} + +impl Option for PutOption { + fn delta(&self) -> f64 { + let n = Normal::new(0., 1.0).unwrap(); + + (-self.variables.dividend * self.variables.time_to_expiration).exp() * (n.cdf(self.variables.d1.unwrap()) - 1.) + } + + fn gamma(&self) -> f64 { + gamma(&self.variables) + } + + fn vega(&self) -> f64 { + vega(&self.variables) + } + + fn theta(&self) -> f64 { + 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 { + 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 { + let n = Normal::new(0., 1.0).unwrap(); + + 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.pdf(v.d1.unwrap()) +} + +pub fn vega(v: &OptionVariables) -> f64 { + let n = Normal::new(0., 1.0).unwrap(); + + let numerator = (-v.dividend * v.time_to_expiration).exp(); + + 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) + } + + #[test] + fn call_test() { + let v = get_example_option(); + + let diff = (v.call().price - 3.019).abs(); + + assert!(diff < 0.01); + } + + #[test] + fn put_test() { + let v = get_example_option(); + + let diff = (v.put().price - 2.691).abs(); + assert!(diff < 0.01); + } + + #[test] + fn call_delta_test() { + let v = get_example_option(); + + let diff = (v.call().delta() - 0.532).abs(); + assert!(diff < 0.01); + } + + #[test] + fn put_delta_test() { + let v = get_example_option(); + + let delta = v.put().delta(); + let diff = (delta - -0.467).abs(); + assert!(diff < 0.01); + } + + #[test] + fn gamma_test() { + let v = get_example_option(); + + let gamma = v.put().gamma(); + let diff = (gamma - 0.055).abs(); + assert!(diff < 0.01); + } + + #[test] + fn vega_test() { + let v = get_example_option(); + + let vega = v.put().vega(); + let diff = (vega - 11.390).abs(); + assert!(diff < 0.01); + } + + #[test] + fn call_rho_test() { + let v = get_example_option(); + + let diff = (v.call().rho() - 4.126).abs(); + assert!(diff < 0.01); + } + + #[test] + fn put_rho_test() { + let v = get_example_option(); + + let rho = v.put().rho(); + 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/finlib/src/options/mod.rs b/finlib/src/options/mod.rs new file mode 100644 index 0000000..56dedb3 --- /dev/null +++ b/finlib/src/options/mod.rs @@ -0,0 +1 @@ +pub mod blackscholes; \ 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::*;