diff --git a/FinLib.NET/FinLib.Bench/Benchmarks/HistoricalVAR.cs b/FinLib.NET/FinLib.Bench/Benchmarks/HistoricalVAR.cs new file mode 100644 index 0000000..186943f --- /dev/null +++ b/FinLib.NET/FinLib.Bench/Benchmarks/HistoricalVAR.cs @@ -0,0 +1,34 @@ +using System.Security.Cryptography; +using BenchmarkDotNet.Attributes; +using FinLib.Bench.Comparisons; + +namespace FinLib.Bench.Benchmarks; + +public class HistoricalVar +{ + private const int N = 10000; + + // [Params(0.05d, 0.1d)] + public double confidence = 0.05; + [Params(10, 1000, 10000)] + public int length; + private double[] data; + + [GlobalSetup] + public void Setup() + { + data = new double[length]; + var random = new Random(42); + + for (int i = 0; i < length; i++) + { + data[i] = random.NextDouble(); + } + } + + [Benchmark] + public double FinLibDotnet() => HistoricalVARDotnet.ValueAtRisk(data.ToList(), confidence); + + [Benchmark] + public double FinLibRust() => FinLib.Risk.ValueAtRisk.Historical(data, confidence); +} \ No newline at end of file diff --git a/FinLib.NET/FinLib.Bench/Comparisons/HistoricalVARDotnet.cs b/FinLib.NET/FinLib.Bench/Comparisons/HistoricalVARDotnet.cs new file mode 100644 index 0000000..2abf9a3 --- /dev/null +++ b/FinLib.NET/FinLib.Bench/Comparisons/HistoricalVARDotnet.cs @@ -0,0 +1,37 @@ +namespace FinLib.Bench.Comparisons; + +public static class HistoricalVARDotnet +{ + public static IEnumerable<(T, T)> Pairwise<T>(this IEnumerable<T> enumerable) + { + var previous = default(T); + + using (var e = enumerable.GetEnumerator()) + { + if (e.MoveNext()) + previous = e.Current; + + while (e.MoveNext()) + { + previous = e.Current; + yield return (previous, e.Current); + } + } + } + + public static IEnumerable<double> RatesOfChange(this IEnumerable<double> values) + { + foreach (var value in values.Pairwise()) + { + yield return (value.Item2 - value.Item1) / value.Item1; + } + } + + public static double ValueAtRisk(List<double> values, double confidence) + { + var roc = values.RatesOfChange().Order().ToArray(); + var threshold = (int) Math.Floor(confidence * roc.Length); + + return roc[threshold]; + } +} \ No newline at end of file diff --git a/FinLib.NET/FinLib.Bench/FinLib.Bench.csproj b/FinLib.NET/FinLib.Bench/FinLib.Bench.csproj new file mode 100644 index 0000000..2c65660 --- /dev/null +++ b/FinLib.NET/FinLib.Bench/FinLib.Bench.csproj @@ -0,0 +1,18 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net9.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="BenchmarkDotNet" Version="0.14.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\FinLib\FinLib.csproj" /> + </ItemGroup> + +</Project> diff --git a/FinLib.NET/FinLib.Bench/Program.cs b/FinLib.NET/FinLib.Bench/Program.cs new file mode 100644 index 0000000..6a48140 --- /dev/null +++ b/FinLib.NET/FinLib.Bench/Program.cs @@ -0,0 +1,13 @@ +using BenchmarkDotNet.Running; +using FinLib.Bench.Benchmarks; + +namespace FinLib.Bench +{ + public class Program + { + public static void Main(string[] args) + { + var summary = BenchmarkRunner.Run<HistoricalVar>(); + } + } +} \ No newline at end of file diff --git a/FinLib.NET/FinLib.Test/Basic.cs b/FinLib.NET/FinLib.Test/Basic.cs index b1e46a7..4c38a95 100644 --- a/FinLib.NET/FinLib.Test/Basic.cs +++ b/FinLib.NET/FinLib.Test/Basic.cs @@ -11,24 +11,24 @@ public class Tests [Test] public void TestCompoundInterest() { - FinLib.CompoundInterest(100, 0.05, 1, 1).Should().Be(105); + Interest.Interest.Compound(100, 0.05, 1, 1).Should().Be(105); } [Test] public void TestCompoundInterestMonthly() { - Math.Round(FinLib.CompoundInterest(100, 0.05, 1, 12), 2).Should().Be(105.12); + Math.Round(Interest.Interest.Compound(100, 0.05, 1, 12), 2).Should().Be(105.12); } [Test] public void Covariance() { - FinLib.Covariance([1d, 2d, 3d, 4], [1d, 2, 3, 4]).Should().Be(1.6666666666666667); + Stats.Stats.Covariance([1d, 2d, 3d, 4], [1d, 2, 3, 4]).Should().Be(1.6666666666666667); } [Test] public void CovarianceBreaking() { - FinLib.Covariance([1d, 2d, 3d, 4], [1d]).Should().BeNull(); + Stats.Stats.Covariance([1d, 2d, 3d, 4], [1d]).Should().BeNull(); } } \ No newline at end of file diff --git a/FinLib.NET/FinLib.sln b/FinLib.NET/FinLib.sln index 524b55e..2c8fb4a 100644 --- a/FinLib.NET/FinLib.sln +++ b/FinLib.NET/FinLib.sln @@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinLib", "FinLib\FinLib.csp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinLib.Test", "FinLib.Test\FinLib.Test.csproj", "{148E024C-07D0-4DC0-A3E2-A146E709897B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinLib.Bench", "FinLib.Bench\FinLib.Bench.csproj", "{F4171146-0A63-4611-972B-BAFB9A090D2B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,5 +20,9 @@ Global {148E024C-07D0-4DC0-A3E2-A146E709897B}.Debug|Any CPU.Build.0 = Debug|Any CPU {148E024C-07D0-4DC0-A3E2-A146E709897B}.Release|Any CPU.ActiveCfg = Release|Any CPU {148E024C-07D0-4DC0-A3E2-A146E709897B}.Release|Any CPU.Build.0 = Release|Any CPU + {F4171146-0A63-4611-972B-BAFB9A090D2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F4171146-0A63-4611-972B-BAFB9A090D2B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4171146-0A63-4611-972B-BAFB9A090D2B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F4171146-0A63-4611-972B-BAFB9A090D2B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/FinLib.NET/FinLib/FinLib.Interest.cs b/FinLib.NET/FinLib/FinLib.Interest.cs new file mode 100644 index 0000000..a6bba3f --- /dev/null +++ b/FinLib.NET/FinLib/FinLib.Interest.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +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); + } +} \ No newline at end of file diff --git a/FinLib.NET/FinLib/FinLib.Risk.cs b/FinLib.NET/FinLib/FinLib.Risk.cs new file mode 100644 index 0000000..347955a --- /dev/null +++ b/FinLib.NET/FinLib/FinLib.Risk.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace FinLib.Risk; + +public static class ValueAtRisk +{ + public static double Historical(IEnumerable<double> values, double confidence) + { + unsafe { + var valueArr = values.ToArray(); + fixed (double* ptrOne = valueArr) { + var ret = NativeMethods.value_at_risk(ptrOne, (UIntPtr)valueArr.Length, confidence); + + return *ret; + } + } + } +} \ No newline at end of file diff --git a/FinLib.NET/FinLib/FinLib.cs b/FinLib.NET/FinLib/FinLib.cs index 00d5183..1483ee4 100644 --- a/FinLib.NET/FinLib/FinLib.cs +++ b/FinLib.NET/FinLib/FinLib.cs @@ -3,15 +3,10 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -namespace FinLib; +namespace FinLib.Stats; -public static class FinLib +public static class Stats { - public static double CompoundInterest(double principal, double rate, double time, double n) - { - return NativeMethods.interest_compound(principal, rate, time, n); - } - public static double? Covariance(IEnumerable<double> valuesOne, IEnumerable<double> valuesTwo) { unsafe { diff --git a/FinLib.NET/FinLib/NativeMethods.g.cs b/FinLib.NET/FinLib/NativeMethods.g.cs index 8544bd6..195eca3 100644 --- a/FinLib.NET/FinLib/NativeMethods.g.cs +++ b/FinLib.NET/FinLib/NativeMethods.g.cs @@ -22,6 +22,9 @@ namespace FinLib [DllImport(__DllName, EntryPoint = "covariance", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] internal static extern double* covariance(double* arr, nuint len, double* arr_two, nuint len_two); + [DllImport(__DllName, EntryPoint = "value_at_risk", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern double* value_at_risk(double* arr, nuint len, double confidence); + } diff --git a/finlib-cpp/finlib-native.h b/finlib-cpp/include/finlib-native.h similarity index 86% rename from finlib-cpp/finlib-native.h rename to finlib-cpp/include/finlib-native.h index efcf02a..a0f37af 100644 --- a/finlib-cpp/finlib-native.h +++ b/finlib-cpp/include/finlib-native.h @@ -21,6 +21,8 @@ const double *covariance(const double *arr, size_t len, const double *arr_two, s double interest_compound(double principal, double rate, double time, double n); +const double *value_at_risk(const double *arr, size_t len, double confidence); + } // extern "C" } // namespace finlib diff --git a/finlib-ffi/build.rs b/finlib-ffi/build.rs index 949bf2d..4f30ff9 100644 --- a/finlib-ffi/build.rs +++ b/finlib-ffi/build.rs @@ -14,7 +14,7 @@ fn main() { .with_config(config) .generate() .expect("Unable to generate bindings") - .write_to_file("../finlib-cpp/finlib-native.h"); + .write_to_file("../finlib-cpp/include/finlib-native.h"); csbindgen::Builder::default() .input_extern_file("src/lib.rs") diff --git a/finlib-ffi/src/lib.rs b/finlib-ffi/src/lib.rs index 5c79ccb..8865bfe 100644 --- a/finlib-ffi/src/lib.rs +++ b/finlib-ffi/src/lib.rs @@ -23,4 +23,14 @@ pub unsafe extern "C" fn covariance(arr: *const f64, len: usize, arr_two: *const Some(v) => Box::into_raw(Box::new(v)) } +} + +#[no_mangle] +pub unsafe extern "C" fn value_at_risk(arr: *const f64, len: usize, confidence: f64) -> *const f64 { + let input_array = unsafe { + assert!(!arr.is_null()); + slice::from_raw_parts(arr, len) + }; + + Box::into_raw(Box::new(finlib::risk::var::historical::value_at_risk(input_array, confidence))) } \ No newline at end of file diff --git a/finlib/Cargo.toml b/finlib/Cargo.toml index f1d735c..f080a0e 100644 --- a/finlib/Cargo.toml +++ b/finlib/Cargo.toml @@ -6,7 +6,8 @@ edition.workspace = true [dependencies] pyo3 = { workspace = true, optional = true } -rayon = { workspace = true } +rayon = { workspace = true, optional = true } [features] -py = ["dep:pyo3"] \ No newline at end of file +py = ["dep:pyo3"] +parallel = ["dep::rayon"] \ No newline at end of file diff --git a/finlib/src/lib.rs b/finlib/src/lib.rs index 40ee7ce..71f9f98 100644 --- a/finlib/src/lib.rs +++ b/finlib/src/lib.rs @@ -1,2 +1,4 @@ pub mod interest; -pub mod stats; \ No newline at end of file +pub mod stats; +pub mod util; +pub mod risk; \ No newline at end of file diff --git a/finlib/src/risk/mod.rs b/finlib/src/risk/mod.rs new file mode 100644 index 0000000..cd03cb3 --- /dev/null +++ b/finlib/src/risk/mod.rs @@ -0,0 +1 @@ +pub mod var; \ No newline at end of file diff --git a/finlib/src/risk/var/historical.rs b/finlib/src/risk/var/historical.rs new file mode 100644 index 0000000..62bb273 --- /dev/null +++ b/finlib/src/risk/var/historical.rs @@ -0,0 +1,27 @@ +use crate::util::roc::rates_of_change; + +#[cfg(feature = "parallel")] +use rayon::prelude::*; + +// https://www.simtrade.fr/blog_simtrade/historical-method-var-calculation/ + +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; + + roc[threshold] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn var_test() { + let result = value_at_risk(&[1f64, 2f64, 4f64, 5f64], 0.01f64); + assert_eq!(result, 0.25f64); + } +} \ No newline at end of file diff --git a/finlib/src/risk/var/mod.rs b/finlib/src/risk/var/mod.rs new file mode 100644 index 0000000..1fd4453 --- /dev/null +++ b/finlib/src/risk/var/mod.rs @@ -0,0 +1 @@ +pub mod historical; \ No newline at end of file diff --git a/finlib/src/stats/covariance.rs b/finlib/src/stats/covariance.rs index 70a3735..9f1c86c 100644 --- a/finlib/src/stats/covariance.rs +++ b/finlib/src/stats/covariance.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "parallel")] use rayon::prelude::*; use super::mean; @@ -8,7 +9,13 @@ pub fn covariance(slice: &[f64], slice_two: &[f64]) -> Option<f64> let mean_1 = mean(slice); let mean_2 = mean(slice_two); - Some(slice.par_iter().zip(slice_two.par_iter()) + Some(slice + // .par_iter() + .iter() + .zip(slice_two + // .par_iter() + .iter() + ) .map(|(x, y)| (x - mean_1) * (y - mean_2)) .sum::<f64>() / ((slice.len() - 1) as f64)) diff --git a/finlib/src/stats/mod.rs b/finlib/src/stats/mod.rs index 090f32e..c2bb3ac 100644 --- a/finlib/src/stats/mod.rs +++ b/finlib/src/stats/mod.rs @@ -1,29 +1,37 @@ mod covariance; pub use covariance::*; +#[cfg(feature = "parallel")] use rayon::prelude::*; pub fn mean(slice: &[f64]) -> f64 { - slice.par_iter().sum::<f64>() / slice.len() as f64 + slice + // .par_iter() + .iter() + .sum::<f64>() / slice.len() as f64 } pub fn population_variance(slice: &[f64]) -> f64 { let mean = mean(slice); - slice.par_iter() + slice + // .par_iter() + .iter() .map(|x| f64::powi(x - mean, 2)) .sum::<f64>() - / slice.len() as f64 + / slice.len() as f64 } pub fn sample_variance(slice: &[f64]) -> f64 { let mean = mean(slice); - slice.par_iter() + slice + // .par_iter() + .iter() .map(|x| f64::powi(x - mean, 2)) .sum::<f64>() - / ((slice.len() - 1) as f64) + / ((slice.len() - 1) as f64) } pub fn population_std_dev(slice: &[f64]) -> f64 diff --git a/finlib/src/util/mod.rs b/finlib/src/util/mod.rs new file mode 100644 index 0000000..e313a5b --- /dev/null +++ b/finlib/src/util/mod.rs @@ -0,0 +1 @@ +pub mod roc; \ No newline at end of file diff --git a/finlib/src/util/roc.rs b/finlib/src/util/roc.rs new file mode 100644 index 0000000..f9920c4 --- /dev/null +++ b/finlib/src/util/roc.rs @@ -0,0 +1,33 @@ +#[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]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn change_test() { + let result = changes(&[1f64, 2f64, 4f64, 5f64]).collect::<Vec<_>>(); + assert_eq!(result, vec![1f64, 2f64, 1f64]); + } + + #[test] + fn roc_test() { + let result = rates_of_change(&[1f64, 2f64, 4f64, 5f64]).collect::<Vec<_>>(); + assert_eq!(result, vec![1f64, 1f64, 0.25f64]); + } +} \ No newline at end of file diff --git a/pyfinlib/src/lib.rs b/pyfinlib/src/lib.rs index e0c923b..312bb5a 100644 --- a/pyfinlib/src/lib.rs +++ b/pyfinlib/src/lib.rs @@ -12,7 +12,19 @@ pub fn covariance(slice: Vec<f64>, slice_two: Vec<f64>) -> PyResult<Option<f64>> #[pymodule] fn pyfinlib(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(compound, m)?)?; - m.add_function(wrap_pyfunction!(covariance, m)?)?; + register_interest_module(m); + register_stats_module(m); Ok(()) +} + +fn register_interest_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { + let child_module = PyModule::new(parent_module.py(), "interest")?; + child_module.add_function(wrap_pyfunction!(compound, &child_module)?)?; + parent_module.add_submodule(&child_module) +} + +fn register_stats_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { + let child_module = PyModule::new(parent_module.py(), "stats")?; + child_module.add_function(wrap_pyfunction!(covariance, &child_module)?)?; + parent_module.add_submodule(&child_module) } \ No newline at end of file