From 664fa06b78945c64fc64177b83960a56918676ac Mon Sep 17 00:00:00 2001 From: Andy Pack <andy@sarsoo.xyz> Date: Sun, 16 Feb 2025 23:36:55 +0000 Subject: [PATCH 1/3] working on black scholes options modelling --- finlib/src/lib.rs | 1 + finlib/src/options/blackscholes/mod.rs | 245 +++++++++++++++++++++++++ finlib/src/options/mod.rs | 1 + 3 files changed, 247 insertions(+) create mode 100644 finlib/src/options/blackscholes/mod.rs create mode 100644 finlib/src/options/mod.rs 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/mod.rs b/finlib/src/options/blackscholes/mod.rs new file mode 100644 index 0000000..ec2af63 --- /dev/null +++ b/finlib/src/options/blackscholes/mod.rs @@ -0,0 +1,245 @@ +use statrs::distribution::{ContinuousCDF, Normal}; + +#[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 +} + +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, + } + } + + pub fn call(self) -> CallOption { + let n = Normal::new(0., 1.0).unwrap(); + let (d1, d2) = self.d1_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(self) -> PutOption { + let n = Normal::new(0., 1.0).unwrap(); + let (d1, d2) = self.d1_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; +} + +#[derive(Debug, Copy, Clone, Default, PartialEq, PartialOrd)] +pub struct CallOption { + pub price: f64, + pub variables: OptionVariables +} + +impl CallOption { + pub fn from(price: f64, variables: OptionVariables) -> Self { + Self { price, variables } + } +} + +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()) + } + + fn gamma(&self) -> f64 { + gamma(&self.variables) + } + + fn vega(&self) -> f64 { + vega(&self.variables) + } + + fn theta(&self) -> f64 { + todo!() + } + + fn rho(&self) -> f64 { + todo!() + } +} + +#[derive(Debug, Copy, Clone, Default, PartialEq, PartialOrd)] +pub struct PutOption { + pub price: f64, + pub variables: OptionVariables +} + +impl PutOption { + pub fn from(price: f64, variables: OptionVariables) -> Self { + Self { price, variables } + } +} + +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()) - 1.) + } + + fn gamma(&self) -> f64 { + gamma(&self.variables) + } + + fn vega(&self) -> f64 { + vega(&self.variables) + } + + fn theta(&self) -> f64 { + todo!() + } + + fn rho(&self) -> f64 { + todo!() + } +} + +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.cdf(v.d1()) +} + +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.cdf(v.d1()) / 100. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn call_test() { + let v = OptionVariables::from(100., 100., 0.25, 0.05, 0.01, 30./365.25); + + let diff = (v.call().price - 3.019).abs(); + + assert!(diff < 0.01); + } + + #[test] + fn put_test() { + let v = OptionVariables::from(100., 100.,0.25, 0.05, 0.01, 30./365.25); + + let diff = (v.put().price - 2.691).abs(); + assert!(diff < 0.01); + } + + #[test] + fn call_delta_test() { + let v = OptionVariables::from(100., 100.,0.25, 0.05, 0.01, 30./365.25); + + let diff = (v.call().delta() - 0.532).abs(); + assert!(diff < 0.01); + } + + #[test] + fn put_delta_test() { + let v = OptionVariables::from(100., 100.,0.25, 0.05, 0.01, 30./365.25); + + let delta = v.put().delta(); + let diff = (delta - -0.467).abs(); + assert!(diff < 0.01); + } + + #[test] + fn gamma_test() { + let v = OptionVariables::from(100., 100.,0.25, 0.05, 0.01, 30./365.25); + + let gamma = v.put().gamma(); + let diff = (gamma - 0.055).abs(); + assert!(diff < 0.01); + } + + #[test] + fn vega_test() { + let v = OptionVariables::from(100., 100.,0.25, 0.05, 0.01, 30./365.25); + + let vega = v.put().vega(); + let diff = (vega - 11.390).abs(); + assert!(diff < 0.01); + } + + #[test] + fn call_rho_test() { + let v = OptionVariables::from(100., 100.,0.25, 0.05, 0.01, 30./365.25); + + let diff = (v.call().rho() - 4.126).abs(); + assert!(diff < 0.01); + } + + #[test] + fn put_rho_test() { + let v = OptionVariables::from(100., 100.,0.25, 0.05, 0.01, 30./365.25); + + let rho = v.put().rho(); + let diff = (rho - -4.060).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 From a0a672cf0d8cf6f8c8090c707796c99359576b03 Mon Sep 17 00:00:00 2001 From: Andy Pack <andy@sarsoo.xyz> Date: Mon, 17 Feb 2025 00:08:51 +0000 Subject: [PATCH 2/3] tweaking options variable handling --- finlib/src/options/blackscholes/mod.rs | 38 ++++++++++++++++---------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/finlib/src/options/blackscholes/mod.rs b/finlib/src/options/blackscholes/mod.rs index ec2af63..a45eba5 100644 --- a/finlib/src/options/blackscholes/mod.rs +++ b/finlib/src/options/blackscholes/mod.rs @@ -7,7 +7,8 @@ pub struct OptionVariables { volatility: f64, risk_free_interest_rate: f64, dividend: f64, - time_to_expiration: f64 + time_to_expiration: f64, + d1: std::option::Option<f64> } impl OptionVariables { @@ -25,12 +26,14 @@ impl OptionVariables { risk_free_interest_rate, dividend, time_to_expiration, + d1: None } } - pub fn call(self) -> CallOption { + pub fn call(mut self) -> CallOption { let n = Normal::new(0., 1.0).unwrap(); let (d1, d2) = self.d1_d2(); + self.d1 = Some(d1); let first = self.underlying_price * (-self.dividend * self.time_to_expiration).exp() * n.cdf(d1); @@ -40,9 +43,10 @@ impl OptionVariables { } - pub fn put(self) -> PutOption { + pub fn put(mut self) -> PutOption { let n = Normal::new(0., 1.0).unwrap(); let (d1, d2) = self.d1_d2(); + self.d1 = Some(d1); let first = self.strike_price * (-self.risk_free_interest_rate * self.time_to_expiration).exp() * n.cdf(-d2); @@ -97,7 +101,7 @@ 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()) + (-self.variables.dividend * self.variables.time_to_expiration).exp() * n.cdf(self.variables.d1.unwrap()) } fn gamma(&self) -> f64 { @@ -133,7 +137,7 @@ 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()) - 1.) + (-self.variables.dividend * self.variables.time_to_expiration).exp() * (n.cdf(self.variables.d1.unwrap()) - 1.) } fn gamma(&self) -> f64 { @@ -159,7 +163,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()) + (numerator / denominator) * n.cdf(v.d1.unwrap()) } pub fn vega(v: &OptionVariables) -> f64 { @@ -167,16 +171,20 @@ 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()) / 100. + v.underlying_price * numerator * f64::sqrt(v.time_to_expiration) * n.cdf(v.d1.unwrap()) / 100. } #[cfg(test)] mod tests { use super::*; + fn get_example_option() -> OptionVariables { + OptionVariables::from(100., 100., 0.25, 0.05, 0.01, 30./365.25) + } + #[test] fn call_test() { - let v = OptionVariables::from(100., 100., 0.25, 0.05, 0.01, 30./365.25); + let v = get_example_option(); let diff = (v.call().price - 3.019).abs(); @@ -185,7 +193,7 @@ mod tests { #[test] fn put_test() { - let v = OptionVariables::from(100., 100.,0.25, 0.05, 0.01, 30./365.25); + let v = get_example_option(); let diff = (v.put().price - 2.691).abs(); assert!(diff < 0.01); @@ -193,7 +201,7 @@ mod tests { #[test] fn call_delta_test() { - let v = OptionVariables::from(100., 100.,0.25, 0.05, 0.01, 30./365.25); + let v = get_example_option(); let diff = (v.call().delta() - 0.532).abs(); assert!(diff < 0.01); @@ -201,7 +209,7 @@ mod tests { #[test] fn put_delta_test() { - let v = OptionVariables::from(100., 100.,0.25, 0.05, 0.01, 30./365.25); + let v = get_example_option(); let delta = v.put().delta(); let diff = (delta - -0.467).abs(); @@ -210,7 +218,7 @@ mod tests { #[test] fn gamma_test() { - let v = OptionVariables::from(100., 100.,0.25, 0.05, 0.01, 30./365.25); + let v = get_example_option(); let gamma = v.put().gamma(); let diff = (gamma - 0.055).abs(); @@ -219,7 +227,7 @@ mod tests { #[test] fn vega_test() { - let v = OptionVariables::from(100., 100.,0.25, 0.05, 0.01, 30./365.25); + let v = get_example_option(); let vega = v.put().vega(); let diff = (vega - 11.390).abs(); @@ -228,7 +236,7 @@ mod tests { #[test] fn call_rho_test() { - let v = OptionVariables::from(100., 100.,0.25, 0.05, 0.01, 30./365.25); + let v = get_example_option(); let diff = (v.call().rho() - 4.126).abs(); assert!(diff < 0.01); @@ -236,7 +244,7 @@ mod tests { #[test] fn put_rho_test() { - let v = OptionVariables::from(100., 100.,0.25, 0.05, 0.01, 30./365.25); + let v = get_example_option(); let rho = v.put().rho(); let diff = (rho - -4.060).abs(); From 46617620a4a7ffd273b6982e91af04542e122e3b Mon Sep 17 00:00:00 2001 From: Andy Pack <andy@sarsoo.xyz> Date: Mon, 17 Feb 2025 22:13:50 +0000 Subject: [PATCH 3/3] fixing and finishing greeks --- finlib/src/ffi/py/mod.rs | 3 +- finlib/src/ffi/py/options.rs | 21 +++ .../src/options/blackscholes/OptionSurface.rs | 174 ++++++++++++++++++ finlib/src/options/blackscholes/mod.rs | 147 +++++++++++++-- pyfinlib/src/lib.rs | 14 ++ 5 files changed, 344 insertions(+), 15 deletions(-) create mode 100644 finlib/src/ffi/py/options.rs create mode 100644 finlib/src/options/blackscholes/OptionSurface.rs 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::*;