diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index a0b9be0..706006c 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -56,6 +56,83 @@ 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 + + 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.4.nupkg + buildWASM: name: Build WASM runs-on: ubuntu-latest diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a02c793..b0e6646 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/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 c5c71e9..e114ba6 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 c7e30a4..f2b69d9 100644 --- a/FinLib.NET/FinLib/FinLib.csproj +++ b/FinLib.NET/FinLib/FinLib.csproj @@ -1,25 +1,25 @@ <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.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> 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/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..b162007 --- /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 { + pub delta: f64, + pub gamma: f64, + pub vega: f64, + pub theta: f64, + pub 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::*;