diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 2891d5e..89bd362 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -97,6 +97,7 @@ jobs: name: Publish .NET runs-on: ubuntu-latest needs: [ buildNET ] # for ignoring bad builds + if: github.event_name == 'push' && github.ref == 'refs/heads/master' steps: - name: Checkout uses: actions/checkout@v4 @@ -124,11 +125,11 @@ jobs: - name: Pack working-directory: ./FinLib.NET/FinLib - run: dotnet pack + run: dotnet pack -p:PackageVersion=$(pushd ../../finlib > /dev/null && cargo pkgid | awk -F '[,#]' '{print $2}' && popd > /dev/null) - name: Push working-directory: ./FinLib.NET/FinLib - run: dotnet nuget push ./bin/Release/FinLib.NET.0.0.5.nupkg --api-key ${{ secrets.DOCKERHUB_TOKEN }} --source https://gitea.sheep-ghoul.ts.net/api/packages/sarsoo/nuget/index.json + run: dotnet nuget push ./bin/Release/FinLib.NET.$(pushd ../../finlib > /dev/null && cargo pkgid | awk -F '[,#]' '{print $2}' && popd > /dev/null).nupkg --api-key ${{ secrets.DOCKERHUB_TOKEN }} --source https://gitea.sheep-ghoul.ts.net/api/packages/sarsoo/nuget/index.json buildWASM: name: Build WASM diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b0e6646..b16e740 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -99,6 +99,42 @@ jobs: working-directory: ./FinLib.NET run: dotnet test --no-restore + publishNET: + name: Publish .NET + runs-on: ubuntu-latest + needs: [ buildNET ] # for ignoring bad builds + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + 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: Pack + working-directory: ./FinLib.NET/FinLib + run: dotnet pack -p:PackageVersion=$(pushd ../../finlib > /dev/null && cargo pkgid | awk -F '[,#]' '{print $2}' && popd > /dev/null) -P:PackageId="Sarsoo.FinLib.NET" + + - name: Push + working-directory: ./FinLib.NET/FinLib + run: dotnet nuget push ./bin/Release/Sarsoo.FinLib.NET.$(pushd ../../finlib > /dev/null && cargo pkgid | awk -F '[,#]' '{print $2}' && popd > /dev/null).nupkg --api-key ${{ secrets.NUGET_TOKEN }} --source https://api.nuget.org/v3/index.json + buildWASM: name: Build WASM runs-on: ubuntu-latest @@ -125,7 +161,7 @@ jobs: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest - needs: [ build, buildPy, buildWASM ] # for ignoring bad builds + needs: [ build, buildPy, buildWASM, buildNET ] # for ignoring bad builds if: github.event_name == 'push' && github.ref == 'refs/heads/master' steps: - name: Checkout @@ -228,4 +264,33 @@ jobs: uses: actions-rs/cargo@v1 with: command: publish - args: --package finlib \ No newline at end of file + args: --package finlib + + publishPy: + runs-on: ubuntu-latest + name: Publish Python Library + needs: [ buildPy ] # for ignoring bad builds + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Install Python 3 + uses: actions/setup-python@v4 + with: + python-version: ${{ env.python-version }} + + - name: Install Maturin + working-directory: ./pyfinlib + run: python3 -m venv .venv && source .venv/bin/activate && pip3 install -r requirements.txt + + - name: Publish + working-directory: ./pyfinlib + run: source .venv/bin/activate && maturin publish + env: + MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 6cbfbb1..bb049d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "0.6.18" @@ -106,6 +112,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cbindgen" version = "0.28.0" @@ -140,6 +152,33 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clap" version = "4.5.29" @@ -194,6 +233,42 @@ dependencies = [ "web-sys", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -219,6 +294,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + [[package]] name = "csbindgen" version = "1.9.3" @@ -259,14 +340,16 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "finlib" -version = "0.0.5" +version = "0.0.6" dependencies = [ + "criterion", "getrandom 0.2.15", "log", "nalgebra", "ndarray", "ndarray-stats", "pyo3", + "rand", "rayon", "statrs", "wasm-bindgen", @@ -274,7 +357,7 @@ dependencies = [ [[package]] name = "finlib-ffi" -version = "0.0.5" +version = "0.0.6" dependencies = [ "cbindgen", "csbindgen", @@ -283,7 +366,7 @@ dependencies = [ [[package]] name = "finlib-wasm" -version = "0.0.5" +version = "0.0.6" dependencies = [ "console_error_panic_hook", "console_log", @@ -318,6 +401,16 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -336,6 +429,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "indexmap" version = "2.7.1" @@ -352,12 +451,32 @@ version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" +[[package]] +name = "is-terminal" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -493,7 +612,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17ebbe97acce52d06aebed4cd4a87c0941f4b2519b59b82b4feb5bd0ce003dfd" dependencies = [ "indexmap", - "itertools", + "itertools 0.13.0", "ndarray", "noisy_float", "num-integer", @@ -565,12 +684,46 @@ version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +[[package]] +name = "oorandom" +version = "11.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" + [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "portable-atomic" version = "1.10.0" @@ -606,7 +759,7 @@ dependencies = [ [[package]] name = "pyfinlib" -version = "0.0.5" +version = "0.0.6" dependencies = [ "finlib", "log", @@ -944,6 +1097,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "toml" version = "0.8.20" diff --git a/Cargo.toml b/Cargo.toml index 926e95c..80c86e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ default-members = [ ] [workspace.package] -version = "0.0.5" +version = "0.0.6" authors = ["sarsoo <andy@sarsoo.xyz>"] description = "Quant finance functions implemented in Rust" edition = "2021" diff --git a/FinLib.NET/FinLib/FinLib.csproj b/FinLib.NET/FinLib/FinLib.csproj index ca3c365..e162194 100644 --- a/FinLib.NET/FinLib/FinLib.csproj +++ b/FinLib.NET/FinLib/FinLib.csproj @@ -2,7 +2,6 @@ <PropertyGroup> <PackageId>FinLib.NET</PackageId> - <Version>0.0.5</Version> <Authors>sarsoo</Authors> </PropertyGroup> diff --git a/README.md b/README.md index b9eeacd..7d1d4f2 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,54 @@ # finlib +[](https://github.com/Sarsoo/finlib/actions/workflows/build.yml) + +Some quantitative finance functionality written in Rust and consumable from many higher-level languages. + +## Derivatives Pricing +- Options + - Black-Scholes + - Prices + - Greeks + +## Risk +- Value-at-Risk + - Historical + - Variance-Covariance (Parametric) + - Single Asset + - Portfolio + # FFI -## Python +- C++ + - FFI header files for C++ are generated automatically during build by [cbindgen](https://github.com/mozilla/cbindgen). +- .NET + - FFI wrapper code for C# tareting .NET Standard 2.0 is generated automatically using [csbindgen](https://github.com/Cysharp/csbindgen/). +- Python + - An adapter library for Python is generated usign [PyO3](https://github.com/PyO3/pyo3) +- WASM (Js) + - A Javascript library is generated using [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) +## [.NET](./finlib-ffi) + +```bash +cargo build +cd FinLib.NET +dotnet build ``` + +## [Python](./pyfinlib) + +```bash cd pyfinlib +python -m venv .venv source .venv/bin/activate +pip install -r requirements.txt maturin develop ``` -## WASM +## [WASM](finlib-wasm) -``` +```bash cd finlib-wasm wasm-pack build ``` \ No newline at end of file diff --git a/finlib/Cargo.toml b/finlib/Cargo.toml index a318566..c08b782 100644 --- a/finlib/Cargo.toml +++ b/finlib/Cargo.toml @@ -13,16 +13,27 @@ license.workspace = true [dependencies] wasm-bindgen = { workspace = true, optional = true } pyo3 = { workspace = true, optional = true } -rayon = { workspace = true, optional = true } +rayon = { workspace = true } ndarray = { workspace = true } ndarray-stats = { workspace = true } nalgebra = { workspace = true } statrs = { workspace = true } log = { workspace = true } getrandom = "~0" +rand = "0.8.5" + +[dev-dependencies] +criterion = "0.5.1" + +[[bench]] +name = "rayon_roc" +harness = false + +[[bench]] +name = "rayon_options" +harness = false [features] py = ["dep:pyo3"] wasm = ["getrandom/js", "dep:wasm-bindgen"] -parallel = ["dep:rayon"] ffi = [] \ No newline at end of file diff --git a/finlib/benches/rayon_options.rs b/finlib/benches/rayon_options.rs new file mode 100644 index 0000000..d841a1d --- /dev/null +++ b/finlib/benches/rayon_options.rs @@ -0,0 +1,43 @@ +use criterion::{criterion_group, criterion_main, AxisScale, BatchSize, BenchmarkId, Criterion, PlotConfiguration, Throughput}; +use finlib::options::blackscholes::option_surface::OptionSurface; +use finlib::options::blackscholes::{generate_options, par_generate_options}; + +pub fn bench_generate_options(c: &mut Criterion) { + let mut group = c.benchmark_group("Options::generate_options"); + + let plot_config = PlotConfiguration::default() + .summary_scale(AxisScale::Logarithmic); + group.plot_config(plot_config); + + for i in [1, 10, 100, 1000].into_iter() { + + let surface = OptionSurface::from( + 0 .. 10, + (100., 200.), + 0 .. i, + (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 variables = surface.walk(); + + group.throughput(Throughput::Elements(variables.len() as u64)); + group.bench_function(BenchmarkId::new("Sequential", variables.len()), |b| { + b.iter_batched(|| variables.clone(), |p| { generate_options(&p); }, BatchSize::SmallInput) + }); + group.bench_function(BenchmarkId::new("Parallel", variables.len()), |b| { + b.iter_batched(|| variables.clone(), |p| { par_generate_options(&p); }, BatchSize::SmallInput) + }); + } + group.finish(); +} + +criterion_group!(benches, bench_generate_options); +criterion_main!(benches); \ No newline at end of file diff --git a/finlib/benches/rayon_roc.rs b/finlib/benches/rayon_roc.rs new file mode 100644 index 0000000..9f75743 --- /dev/null +++ b/finlib/benches/rayon_roc.rs @@ -0,0 +1,65 @@ +use criterion::{criterion_group, criterion_main, AxisScale, BatchSize, BenchmarkId, Criterion, PlotConfiguration, Throughput}; +use finlib::risk::portfolio::{Portfolio, PortfolioAsset}; +use rand::Rng; + +pub fn bench_apply_rates_of_change_values(c: &mut Criterion) { + let mut group = c.benchmark_group("Portfolio::apply_rates_of_change/values"); + let mut rng = rand::thread_rng(); + + let plot_config = PlotConfiguration::default() + .summary_scale(AxisScale::Logarithmic); + group.plot_config(plot_config); + + for i in [10, 100, 1000, 10_000, 100_000, 1_000_000].into_iter() { + + let portfolio = Portfolio::from(vec![ + PortfolioAsset::new(0.1, "a".to_string(), (0 .. i).map(|_| rng.gen::<f64>()).collect()), + PortfolioAsset::new(0.1, "a".to_string(), (0 .. i).map(|_| rng.gen::<f64>()).collect()), + PortfolioAsset::new(0.1, "a".to_string(), (0 .. i).map(|_| rng.gen::<f64>()).collect()), + PortfolioAsset::new(0.1, "a".to_string(), (0 .. i).map(|_| rng.gen::<f64>()).collect()), + PortfolioAsset::new(0.1, "a".to_string(), (0 .. i).map(|_| rng.gen::<f64>()).collect()), + PortfolioAsset::new(0.1, "a".to_string(), (0 .. i).map(|_| rng.gen::<f64>()).collect()), + PortfolioAsset::new(0.1, "a".to_string(), (0 .. i).map(|_| rng.gen::<f64>()).collect()), + PortfolioAsset::new(0.1, "a".to_string(), (0 .. i).map(|_| rng.gen::<f64>()).collect()), + PortfolioAsset::new(0.1, "a".to_string(), (0 .. i).map(|_| rng.gen::<f64>()).collect()), + PortfolioAsset::new(0.1, "a".to_string(), (0 .. i).map(|_| rng.gen::<f64>()).collect()) + ]); + + group.throughput(Throughput::Elements((i * 10) as u64)); + group.bench_function(BenchmarkId::new("Sequential", i), |b| { + b.iter_batched(|| portfolio.clone(), |mut p| { p.apply_rates_of_change(); }, BatchSize::SmallInput) + }); + group.bench_function(BenchmarkId::new("Parallel", i), |b| { + b.iter_batched(|| portfolio.clone(), |mut p| { p.par_apply_rates_of_change(); }, BatchSize::SmallInput) + }); + } + group.finish(); +} + +pub fn bench_apply_rates_of_change_assets(c: &mut Criterion) { + let mut group = c.benchmark_group("Portfolio::apply_rates_of_change/assets"); + let mut rng = rand::thread_rng(); + + let plot_config = PlotConfiguration::default() + .summary_scale(AxisScale::Logarithmic); + group.plot_config(plot_config); + + for i in [10, 100, 1000, 10_000].into_iter() { + + let portfolio = Portfolio::from((0 .. i).map(|_| { + PortfolioAsset::new(0.1, "a".to_string(), (0 .. 10000).map(|_| rng.gen::<f64>()).collect()) + }).collect()); + + group.throughput(Throughput::Elements(i as u64)); + group.bench_function(BenchmarkId::new("Sequential", i), |b| { + b.iter_batched(|| portfolio.clone(), |mut p| { p.apply_rates_of_change(); }, BatchSize::SmallInput) + }); + group.bench_function(BenchmarkId::new("Parallel", i), |b| { + b.iter_batched(|| portfolio.clone(), |mut p| { p.par_apply_rates_of_change(); }, BatchSize::SmallInput) + }); + } + group.finish(); +} + +criterion_group!(benches, bench_apply_rates_of_change_values, bench_apply_rates_of_change_assets); +criterion_main!(benches); \ No newline at end of file diff --git a/finlib/src/lib.rs b/finlib/src/lib.rs index 5024e04..1e01328 100644 --- a/finlib/src/lib.rs +++ b/finlib/src/lib.rs @@ -5,30 +5,4 @@ pub mod stats; pub mod util; pub mod risk; pub mod ffi; -pub mod options; - -#[cfg(feature = "parallel")] -use rayon::prelude::*; - -#[macro_export] -macro_rules! gated_iter { - ($x:expr) => { - { - $x.iter() - } - }; -} - -#[macro_export] -macro_rules! gated_iter_mut { - ($x:expr) => { - { - if cfg!(feature = "parallel") { - $x.par_iter_mut() - } - else { - $x.iter_mut() - } - } - }; -} \ No newline at end of file +pub mod options; \ No newline at end of file diff --git a/finlib/src/options/blackscholes/OptionSurface.rs b/finlib/src/options/blackscholes/OptionSurface.rs deleted file mode 100644 index faf761f..0000000 --- a/finlib/src/options/blackscholes/OptionSurface.rs +++ /dev/null @@ -1,174 +0,0 @@ -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 - .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/generate.rs b/finlib/src/options/blackscholes/generate.rs new file mode 100644 index 0000000..d36104d --- /dev/null +++ b/finlib/src/options/blackscholes/generate.rs @@ -0,0 +1,32 @@ +use rayon::prelude::*; +use crate::options::blackscholes::{CallOption, Option, OptionVariables, PutOption}; + +pub fn generate_options(option_variables: &Vec<OptionVariables>) -> Vec<(CallOption, PutOption)> { + option_variables + .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)>>() +} + +pub fn par_generate_options(option_variables: &Vec<OptionVariables>) -> Vec<(CallOption, PutOption)> { + option_variables + .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)>>() +} \ No newline at end of file diff --git a/finlib/src/options/blackscholes/mod.rs b/finlib/src/options/blackscholes/mod.rs index b162007..ba5cad4 100644 --- a/finlib/src/options/blackscholes/mod.rs +++ b/finlib/src/options/blackscholes/mod.rs @@ -1,4 +1,6 @@ -mod OptionSurface; +pub mod option_surface; +pub mod generate; +pub use generate::*; use statrs::distribution::{Continuous, ContinuousCDF, Normal}; #[cfg(feature = "wasm")] diff --git a/finlib/src/options/blackscholes/option_surface.rs b/finlib/src/options/blackscholes/option_surface.rs new file mode 100644 index 0000000..cca2100 --- /dev/null +++ b/finlib/src/options/blackscholes/option_surface.rs @@ -0,0 +1,113 @@ +use core::ops::Range; +use std::sync::{Arc, Mutex}; +use crate::options::blackscholes::{CallOption, Option, OptionVariables, PutOption}; + +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> { + + 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::{generate_options, 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 = generate_options(&a); + + let a1 = a.first(); + } +} \ No newline at end of file diff --git a/finlib/src/risk/portfolio.rs b/finlib/src/risk/portfolio.rs index 5bb44f6..64231ed 100644 --- a/finlib/src/risk/portfolio.rs +++ b/finlib/src/risk/portfolio.rs @@ -5,7 +5,6 @@ use ndarray_stats::CorrelationExt; use wasm_bindgen::prelude::*; #[cfg(feature = "py")] use pyo3::prelude::*; -#[cfg(feature = "parallel")] use rayon::prelude::*; use statrs::distribution::{ContinuousCDF, Normal}; use crate::risk::forecast::{mean_investment, std_dev_investment}; @@ -96,18 +95,16 @@ impl Portfolio { /// Convert a portfolio of assets with absolute values to the percentage change in values pub fn apply_rates_of_change(&mut self) { - #[cfg(feature = "parallel")] - { - self.assets.par_iter_mut().for_each(|asset| { - asset.apply_rates_of_change(); - }); - } - #[cfg(not(feature = "parallel"))] - { - self.assets.iter_mut().for_each(|asset| { - asset.apply_rates_of_change(); - }); - } + self.assets.iter_mut().for_each(|asset| { + asset.apply_rates_of_change(); + }); + } + + #[deprecated(note = "a lot slower than the sequential method, sans par prefix")] + pub fn par_apply_rates_of_change(&mut self) { + self.assets.par_iter_mut().for_each(|asset| { + asset.apply_rates_of_change(); + }); } /// Do all the assets in the portfolio have the same number of values (required to perform matrix operations) @@ -155,29 +152,33 @@ impl Portfolio { let column_count = self.assets.len(); let row_count = self.assets[0].values.len(); - #[cfg(feature = "parallel")] - { - let matrix = Array2::from_shape_vec((column_count, row_count), - self.assets - .par_iter() - .map(|a| a.values.clone()) - .flatten() - .collect::<Vec<f64>>() - ).unwrap(); - Some(matrix.into_owned()) - } - #[cfg(not(feature = "parallel"))] - { - let matrix = Array2::from_shape_vec((column_count, row_count), - self.assets - .iter() - .map(|a| a.values.clone()) - .flatten() - .collect::<Vec<f64>>() - ).unwrap(); - Some(matrix.into_owned()) + let matrix = Array2::from_shape_vec((column_count, row_count), + self.assets + .iter() + .map(|a| a.values.clone()) + .flatten() + .collect::<Vec<f64>>() + ).unwrap(); + Some(matrix.into_owned()) + } + + /// Format the asset values in the portfolio as a matrix such that statistical operations can be applied to it + pub fn par_get_matrix(&self) -> Option<Array2<f64>> { + if self.assets.is_empty() || !self.valid_sizes() { + return None; } + let column_count = self.assets.len(); + let row_count = self.assets[0].values.len(); + + let matrix = Array2::from_shape_vec((column_count, row_count), + self.assets + .par_iter() + .map(|a| a.values.clone()) + .flatten() + .collect::<Vec<f64>>() + ).unwrap(); + Some(matrix.into_owned()) } /// Calculate the mean and the standard deviation of a portfolio, taking into account the relative weights and covariance of the portfolio's assets diff --git a/finlib/src/risk/var/historical.rs b/finlib/src/risk/var/historical.rs index 13a203d..f8d9b7e 100644 --- a/finlib/src/risk/var/historical.rs +++ b/finlib/src/risk/var/historical.rs @@ -1,6 +1,4 @@ use crate::util::roc::rates_of_change; - -#[cfg(feature = "parallel")] use rayon::prelude::*; // https://www.simtrade.fr/blog_simtrade/historical-method-var-calculation/ @@ -8,7 +6,6 @@ use rayon::prelude::*; pub fn value_at_risk(values: &[f64], confidence: f64) -> f64 { let mut roc = rates_of_change(values).collect::<Vec<_>>(); - // roc.par_sort_by(|x, y| x.partial_cmp(y).unwrap()); roc.sort_by(|x, y| x.partial_cmp(y).unwrap()); let threshold = (confidence * roc.len() as f64).floor() as usize; @@ -16,6 +13,16 @@ pub fn value_at_risk(values: &[f64], confidence: f64) -> f64 { roc[threshold] } +pub fn par_value_at_risk(values: &[f64], confidence: f64) -> f64 { + let mut roc = rates_of_change(values).collect::<Vec<_>>(); + + roc.par_sort_by(|x, y| x.partial_cmp(y).unwrap()); + + let threshold = (confidence * roc.len() as f64).floor() as usize; + + roc[threshold] +} + #[cfg(test)] mod tests { use super::*; diff --git a/finlib/src/risk/var/varcovar.rs b/finlib/src/risk/var/varcovar.rs index c121692..0be44ad 100644 --- a/finlib/src/risk/var/varcovar.rs +++ b/finlib/src/risk/var/varcovar.rs @@ -1,7 +1,6 @@ use crate::stats; use crate::util::roc::rates_of_change; -#[cfg(feature = "parallel")] use rayon::prelude::*; use statrs::distribution::{ContinuousCDF, Normal}; // https://medium.com/@serdarilarslan/value-at-risk-var-and-its-implementation-in-python-5c9150f73b0e diff --git a/finlib/src/stats/covariance.rs b/finlib/src/stats/covariance.rs index c4a4729..db2f6fe 100644 --- a/finlib/src/stats/covariance.rs +++ b/finlib/src/stats/covariance.rs @@ -1,5 +1,3 @@ -#[cfg(feature = "parallel")] -use rayon::prelude::*; use super::mean; pub fn covariance(slice: &[f64], slice_two: &[f64]) -> Option<f64> @@ -10,10 +8,8 @@ pub fn covariance(slice: &[f64], slice_two: &[f64]) -> Option<f64> let mean_2 = mean(slice_two); Some(slice - // .par_iter() .iter() .zip(slice_two - // .par_iter() .iter() ) .map(|(x, y)| (x - mean_1) * (y - mean_2)) diff --git a/finlib/src/util/roc.rs b/finlib/src/util/roc.rs index f9920c4..29deb74 100644 --- a/finlib/src/util/roc.rs +++ b/finlib/src/util/roc.rs @@ -1,17 +1,13 @@ -#[cfg(feature = "parallel")] -use rayon::prelude::*; pub fn changes(values: &[f64]) -> impl Iterator<Item = f64> + use<'_> { values .windows(2) - // .par_windows(2) .map(|x| x[1] - x[0]) } pub fn rates_of_change(values: &[f64]) -> impl Iterator<Item = f64> + use<'_> { values .windows(2) - // .par_windows(2) .map(|x| (x[1] - x[0])/x[0]) }