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/6] 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/6] 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/6] 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::*; From 30803bbee72b54ddbb720e532623f7c63c3bdcc2 Mon Sep 17 00:00:00 2001 From: Andy Pack <andy@sarsoo.xyz> Date: Mon, 17 Feb 2025 22:15:08 +0000 Subject: [PATCH 4/6] adding dotnet building to ci, wildcard dotnet dll copying --- .gitea/workflows/build.yml | 37 +++++++++++++++++++++++++++++++++ .github/workflows/build.yml | 35 +++++++++++++++++++++++++++++++ FinLib.NET/FinLib/FinLib.csproj | 4 ++-- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index b07b7b4..26516b9 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -56,6 +56,43 @@ jobs: working-directory: ./pyfinlib run: maturin build + buildNET: + name: Build .NET + runs-on: ubuntu-latest + needs: [ build ] # for ignoring bad builds + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + github-server-url: https://gitea.sheep-ghoul.ts.net + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Cargo Build + uses: actions-rs/cargo@v1 + with: + command: build + + - name: Setup .NET Core SDK 9.0.x + uses: actions/setup-dotnet@v3.0.3 + with: + dotnet-version: 9.0.x + + - name: Install Dependencies + working-directory: ./FinLib.NET + run: dotnet restore + + - name: Build + working-directory: ./FinLib.NET + run: dotnet build --configuration Debug --no-restore + + - name: Test + working-directory: ./FinLib.NET + run: dotnet test --no-restore + buildWASM: name: Build WASM runs-on: ubuntu-latest diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index da953ff..3709335 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,6 +64,41 @@ jobs: working-directory: ./pyfinlib run: maturin build + buildNET: + name: Build .NET + runs-on: ubuntu-latest + needs: [ build ] # for ignoring bad builds + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Cargo Build + uses: actions-rs/cargo@v1 + with: + command: build + + - name: Setup .NET Core SDK 9.0.x + uses: actions/setup-dotnet@v3.0.3 + with: + dotnet-version: 9.0.x + + - name: Install Dependencies + working-directory: ./FinLib.NET + run: dotnet restore + + - name: Build + working-directory: ./FinLib.NET + run: dotnet build --configuration Debug --no-restore + + - name: Test + working-directory: ./FinLib.NET + run: dotnet test --no-restore + buildWASM: name: Build WASM runs-on: ubuntu-latest diff --git a/FinLib.NET/FinLib/FinLib.csproj b/FinLib.NET/FinLib/FinLib.csproj index c7e30a4..28d7541 100644 --- a/FinLib.NET/FinLib/FinLib.csproj +++ b/FinLib.NET/FinLib/FinLib.csproj @@ -13,13 +13,13 @@ </PropertyGroup> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <Content Include="..\..\target\debug\libfinlib_ffi.dylib"> + <Content Include="..\..\target\debug\libfinlib_ffi.*"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup> <ItemGroup Condition=" '$(Configuration)' == 'Release' "> - <Content Include="..\..\target\release\libfinlib_ffi.dylib"> + <Content Include="..\..\target\release\libfinlib_ffi.*"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup> From 10e429306b651bcd8a02d6f743a6a3067ca68171 Mon Sep 17 00:00:00 2001 From: Andy Pack <andy@sarsoo.xyz> Date: Mon, 17 Feb 2025 22:32:33 +0000 Subject: [PATCH 5/6] publishing .net library --- .gitea/workflows/build.yml | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 26516b9..0aa4d26 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -93,6 +93,46 @@ jobs: working-directory: ./FinLib.NET run: dotnet test --no-restore + publishNET: + name: Build .NET + runs-on: ubuntu-latest + needs: [ buildNET ] # for ignoring bad builds + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + github-server-url: https://gitea.sheep-ghoul.ts.net + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Cargo Build + uses: actions-rs/cargo@v1 + with: + command: build + + - name: Setup .NET Core SDK 9.0.x + uses: actions/setup-dotnet@v3.0.3 + with: + dotnet-version: 9.0.x + + - name: Add Gitea Repo + run: dotnet nuget add source --name gitea --api-key ${{ secrets.DOCKERHUB_TOKEN }} https://gitea.sheep-ghoul.ts.net/api/packages/sarsoo/nuget/index.json + + - name: Install Dependencies + working-directory: ./FinLib.NET + run: dotnet restore + + - name: Pack + working-directory: ./FinLib.NET/FinLib + run: dotnet pack + + - name: Push + working-directory: ./FinLib.NET/FinLib + run: dotnet nuget push --source gitea ./bin/Release/FinLib.NET.0.0.3.nupkg + buildWASM: name: Build WASM runs-on: ubuntu-latest From e74c266a6eda30eff03ddc8b781c3b8d3cf4ae73 Mon Sep 17 00:00:00 2001 From: Andy Pack <andy@sarsoo.xyz> Date: Mon, 17 Feb 2025 23:22:35 +0000 Subject: [PATCH 6/6] tweaking ffi bindings, bumping version --- .gitea/workflows/build.yml | 2 +- Cargo.lock | 8 +++--- Cargo.toml | 2 +- FinLib.NET/FinLib/FinLib.Interest.cs | 5 +--- FinLib.NET/FinLib/FinLib.Risk.cs | 2 ++ FinLib.NET/FinLib/FinLib.csproj | 12 ++++---- FinLib.NET/FinLib/NativeMethods.g.cs | 39 ++++++++++++++++++++++++++ finlib-cpp/include/finlib-native.h | 2 ++ finlib-ffi/build.rs | 11 ++++++++ finlib-ffi/cbindgen.toml | 8 +++--- finlib-ffi/src/lib.rs | 6 ++++ finlib/src/options/blackscholes/mod.rs | 10 +++---- 12 files changed, 82 insertions(+), 25 deletions(-) diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 0aa4d26..6913d7c 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -131,7 +131,7 @@ jobs: - name: Push working-directory: ./FinLib.NET/FinLib - run: dotnet nuget push --source gitea ./bin/Release/FinLib.NET.0.0.3.nupkg + run: dotnet nuget push --source gitea ./bin/Release/FinLib.NET.0.0.4.nupkg buildWASM: name: Build WASM diff --git a/Cargo.lock b/Cargo.lock index 08b4261..265f179 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,7 +259,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "finlib" -version = "0.0.3" +version = "0.0.4" dependencies = [ "getrandom 0.2.15", "log", @@ -274,7 +274,7 @@ dependencies = [ [[package]] name = "finlib-ffi" -version = "0.0.3" +version = "0.0.4" dependencies = [ "cbindgen", "csbindgen", @@ -283,7 +283,7 @@ dependencies = [ [[package]] name = "finlib-wasm" -version = "0.0.3" +version = "0.0.4" dependencies = [ "console_error_panic_hook", "console_log", @@ -606,7 +606,7 @@ dependencies = [ [[package]] name = "pyfinlib" -version = "0.0.3" +version = "0.0.4" dependencies = [ "finlib", "log", diff --git a/Cargo.toml b/Cargo.toml index 5c81626..2383a13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ default-members = [ ] [workspace.package] -version = "0.0.3" +version = "0.0.4" authors = ["sarsoo <andy@sarsoo.xyz>"] description = "Quant finance functions implemented in Rust" edition = "2021" diff --git a/FinLib.NET/FinLib/FinLib.Interest.cs b/FinLib.NET/FinLib/FinLib.Interest.cs index a6bba3f..efa04ef 100644 --- a/FinLib.NET/FinLib/FinLib.Interest.cs +++ b/FinLib.NET/FinLib/FinLib.Interest.cs @@ -7,8 +7,5 @@ namespace FinLib.Interest; public static class Interest { - public static double Compound(double principal, double rate, double time, double n) - { - return NativeMethods.interest_compound(principal, rate, time, n); - } + public static double Compound(double principal, double rate, double time, double n) => NativeMethods.interest_compound(principal, rate, time, n); } \ No newline at end of file diff --git a/FinLib.NET/FinLib/FinLib.Risk.cs b/FinLib.NET/FinLib/FinLib.Risk.cs index 7813bdb..bba9bee 100644 --- a/FinLib.NET/FinLib/FinLib.Risk.cs +++ b/FinLib.NET/FinLib/FinLib.Risk.cs @@ -30,4 +30,6 @@ public static class ValueAtRisk } } } + + public static double ScaleValueAtRisk(double initialValue, nint timeCycles) => NativeMethods.scale_value_at_risk(initialValue, timeCycles); } \ No newline at end of file diff --git a/FinLib.NET/FinLib/FinLib.csproj b/FinLib.NET/FinLib/FinLib.csproj index 28d7541..f2b69d9 100644 --- a/FinLib.NET/FinLib/FinLib.csproj +++ b/FinLib.NET/FinLib/FinLib.csproj @@ -1,17 +1,17 @@ <Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <PackageId>FinLib.NET</PackageId> + <Version>0.0.4</Version> + <Authors>sarsoo</Authors> + </PropertyGroup> + <PropertyGroup> <TargetFrameworks>netstandard2.0</TargetFrameworks> <LangVersion>latest</LangVersion> <AllowUnsafeBlocks>true</AllowUnsafeBlocks> </PropertyGroup> - <PropertyGroup> - <PackageId>FinLib.NET</PackageId> - <Version>0.0.1</Version> - <Authors>sarsoo</Authors> - </PropertyGroup> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <Content Include="..\..\target\debug\libfinlib_ffi.*"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> diff --git a/FinLib.NET/FinLib/NativeMethods.g.cs b/FinLib.NET/FinLib/NativeMethods.g.cs index 1d1065f..fcbb16b 100644 --- a/FinLib.NET/FinLib/NativeMethods.g.cs +++ b/FinLib.NET/FinLib/NativeMethods.g.cs @@ -28,9 +28,48 @@ namespace FinLib [DllImport(__DllName, EntryPoint = "varcovar_value_at_risk", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] internal static extern double* varcovar_value_at_risk(double* arr, nuint len, double confidence); + [DllImport(__DllName, EntryPoint = "scale_value_at_risk", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern double scale_value_at_risk(double initial_value, nint time_cycles); + } + [StructLayout(LayoutKind.Sequential)] + internal unsafe partial struct Portfolio + { + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe partial struct PortfolioAsset + { + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe partial struct OptionVariables + { + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe partial struct CallOption + { + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe partial struct PutOption + { + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe partial struct OptionGreeks + { + } + + + internal enum ValueType : byte + { + Absolute, + RateOfChange, + } } diff --git a/finlib-cpp/include/finlib-native.h b/finlib-cpp/include/finlib-native.h index 1dd2773..dea90d7 100644 --- a/finlib-cpp/include/finlib-native.h +++ b/finlib-cpp/include/finlib-native.h @@ -23,6 +23,8 @@ const double *historical_value_at_risk(const double *arr, size_t len, double con double interest_compound(double principal, double rate, double time, double n); +double scale_value_at_risk(double initial_value, ptrdiff_t time_cycles); + const double *varcovar_value_at_risk(const double *arr, size_t len, double confidence); } // extern "C" diff --git a/finlib-ffi/build.rs b/finlib-ffi/build.rs index 4f30ff9..08e65f4 100644 --- a/finlib-ffi/build.rs +++ b/finlib-ffi/build.rs @@ -19,7 +19,18 @@ fn main() { csbindgen::Builder::default() .input_extern_file("src/lib.rs") .input_extern_file("../finlib/src/lib.rs") + .input_extern_file("../finlib/src/risk/portfolio.rs") + .input_extern_file("../finlib/src/options/blackscholes/mod.rs") .csharp_dll_name("libfinlib_ffi") + .always_included_types([ + "Portfolio", + "ValueType", + "PortfolioAsset", + "OptionVariables", + "CallOption", + "PutOption", + "OptionGreeks", + ]) .csharp_namespace("FinLib") .generate_csharp_file("../FinLib.NET/FinLib/NativeMethods.g.cs") .unwrap(); diff --git a/finlib-ffi/cbindgen.toml b/finlib-ffi/cbindgen.toml index cd8eb52..dc8e222 100644 --- a/finlib-ffi/cbindgen.toml +++ b/finlib-ffi/cbindgen.toml @@ -144,11 +144,11 @@ bitflags = false ############## Options for How Your Rust library Should Be Parsed ############## [parse] -parse_deps = false -# include = [] -exclude = [] +parse_deps = true +include = ["finlib"] +#exclude = [] clean = false -extra_bindings = [] +extra_bindings = ["finlib"] diff --git a/finlib-ffi/src/lib.rs b/finlib-ffi/src/lib.rs index ddf2a73..a52cbcf 100644 --- a/finlib-ffi/src/lib.rs +++ b/finlib-ffi/src/lib.rs @@ -44,3 +44,9 @@ pub unsafe extern "C" fn varcovar_value_at_risk(arr: *const f64, len: usize, con Box::into_raw(Box::new(finlib::risk::var::varcovar::value_at_risk_percent(input_array, confidence))) } + +#[no_mangle] +pub unsafe extern "C" fn scale_value_at_risk(initial_value: f64, time_cycles: isize) -> f64 { + + finlib::risk::var::varcovar::scale_value_at_risk(initial_value, time_cycles) +} diff --git a/finlib/src/options/blackscholes/mod.rs b/finlib/src/options/blackscholes/mod.rs index 7235352..b162007 100644 --- a/finlib/src/options/blackscholes/mod.rs +++ b/finlib/src/options/blackscholes/mod.rs @@ -255,11 +255,11 @@ pub fn vega(v: &OptionVariables) -> f64 { #[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 + pub delta: f64, + pub gamma: f64, + pub vega: f64, + pub theta: f64, + pub rho: f64 } impl OptionGreeks {