portfolio covariance VAR

This commit is contained in:
Andy Pack 2025-02-15 20:09:13 +00:00
parent c1113f3296
commit f9a7fb4b2f
Signed by: sarsoo
GPG Key ID: A55BA3536A5E0ED7
14 changed files with 734 additions and 9 deletions

14
Cargo.lock generated

@ -183,6 +183,17 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "console_log"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f"
dependencies = [
"log",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
@ -258,6 +269,7 @@ dependencies = [
"pyo3",
"rayon",
"statrs",
"wasm-bindgen",
]
[[package]]
@ -274,7 +286,9 @@ name = "finlib-wasm"
version = "0.0.1"
dependencies = [
"console_error_panic_hook",
"console_log",
"finlib",
"log",
"wasm-bindgen",
"wasm-bindgen-test",
]

@ -30,6 +30,7 @@ ndarray-stats = "0.6.0"
nalgebra = "0.33.2"
statrs = "0.18.0"
log = "0.4.25"
wasm-bindgen = "0.2.100"
pyo3 = { version = "0.23.4", features = ["extension-module", "abi3-py38"] }
pyo3-log = "0.12.1"

@ -13,7 +13,7 @@ readme.workspace = true
crate-type = ["cdylib"]
[dependencies]
finlib = { path = "../finlib" }
finlib = { path = "../finlib", features = ["ffi"] }
[build-dependencies]
cbindgen = "0.28.0"

@ -16,8 +16,10 @@ crate-type = ["cdylib", "rlib"]
default = ["console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2.100"
wasm-bindgen = { workspace = true }
finlib = { path = "../finlib", features = ["wasm"] }
log = { workspace = true }
console_log = { version = "1.0.0", features = ["color"] }
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires

@ -1,4 +1,13 @@
use wasm_bindgen::prelude::wasm_bindgen;
use console_log;
use log::Level;
#[wasm_bindgen(start)]
fn start() {
if let Err(_) = console_log::init_with_level(Level::Debug) {
}
}
#[wasm_bindgen]
pub struct Interest { }

@ -11,6 +11,7 @@ documentation.workspace = true
readme.workspace = true
[dependencies]
wasm-bindgen = { workspace = true, optional = true }
pyo3 = { workspace = true, optional = true }
rayon = { workspace = true, optional = true }
ndarray = { workspace = true }
@ -22,5 +23,6 @@ getrandom = "*"
[features]
py = ["dep:pyo3"]
wasm = ["getrandom/js"]
parallel = ["dep:rayon"]
wasm = ["getrandom/js", "dep:wasm-bindgen"]
parallel = ["dep:rayon"]
ffi = []

@ -1,4 +1,6 @@
pub mod interest;
pub mod stats;
pub mod util;
pub mod risk;
pub mod risk;
#[cfg(feature = "py")]
pub mod py;

30
finlib/src/py/mod.rs Normal file

@ -0,0 +1,30 @@
use pyo3::prelude::*;
use crate::risk::portfolio::{Portfolio, PortfolioAsset};
#[pymethods]
impl Portfolio {
#[new]
pub fn init(assets: Vec<PortfolioAsset>) -> Self {
Portfolio::from(assets)
}
#[pyo3(name = "is_valid")]
pub fn is_valid_py(&self) -> bool {
self.is_valid()
}
#[pyo3(name = "value_at_risk")]
pub fn value_at_risk_py(&mut self, confidence: f64) -> PyResult<Option<f64>> {
Ok(self.value_at_risk(confidence))
}
}
#[pymethods]
impl PortfolioAsset {
#[new]
pub fn init(portfolio_weight: f64, name: String, values: Vec<f64>) -> Self {
PortfolioAsset::new(portfolio_weight, name, values)
}
}

@ -1 +1,2 @@
pub mod var;
pub mod var;
pub mod portfolio;

@ -0,0 +1,201 @@
use ndarray::prelude::*;
use ndarray_stats::CorrelationExt;
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
#[cfg(feature = "py")]
use pyo3::prelude::*;
use crate::risk::var::varcovar::portfolio_value_at_risk;
use crate::stats;
use crate::util::roc::rates_of_change;
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[cfg_attr(feature = "py", pyclass)]
#[cfg_attr(feature = "ffi", repr(C))]
#[derive(Clone)]
pub struct Portfolio {
assets: Vec<PortfolioAsset>
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[cfg_attr(feature = "py", pyclass)]
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum ValueType {
Absolute,
RateOfChange
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[cfg_attr(feature = "py", pyclass)]
#[cfg_attr(feature = "ffi", repr(C))]
#[derive(Clone)]
pub struct PortfolioAsset {
portfolio_weight: f64,
name: String,
values: Vec<f64>,
value_type: ValueType
}
impl PortfolioAsset {
pub fn new(portfolio_weight: f64, name: String, values: Vec<f64>) -> PortfolioAsset {
PortfolioAsset {
portfolio_weight, name, values, value_type: ValueType::Absolute
}
}
pub fn apply_rates_of_change(&mut self) {
match self.value_type {
ValueType::Absolute => {
self.values = rates_of_change(&self.values).collect();
self.value_type = ValueType::RateOfChange;
}
_ => {}
}
}
pub fn get_mean_and_std(&self) -> Option<(f64, f64)> {
match self.value_type {
ValueType::Absolute => {
let roc = rates_of_change(&self.values).collect::<Vec<f64>>();
Some((stats::mean(&roc), stats::sample_std_dev(&roc)))
}
ValueType::RateOfChange => {
Some((stats::mean(&self.values), stats::sample_std_dev(&self.values)))
}
}
}
}
impl Portfolio {
pub fn from(assets: Vec<PortfolioAsset>) -> Portfolio {
Portfolio {
assets
}
}
pub fn get_asset_weight(&self) -> impl Iterator<Item=f64> + use<'_> {
self.assets
.iter()
.map(|x| x.portfolio_weight)
}
pub fn apply_rates_of_change(&mut self) {
for asset in self.assets.iter_mut() {
asset.apply_rates_of_change();
}
}
pub fn valid_sizes(&self) -> bool {
let mut last_value_length: Option<usize> = None;
for asset in &self.assets {
match last_value_length {
None => {
last_value_length = Some(asset.values.len());
}
Some(l) => {
if l != asset.values.len() {
return false;
}
last_value_length = Some(asset.values.len());
}
}
}
true
}
pub fn valid_weights(&self) -> bool {
let mut weight = 1 as f64;
for asset in &self.assets {
weight -= asset.portfolio_weight;
}
f64::abs(weight) < 0.01
}
pub fn is_valid(&self) -> bool {
self.valid_sizes() && self.valid_weights()
}
pub fn 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
.iter()
.map(|a| a.values.clone())
.flatten()
.collect::<Vec<f64>>()
).unwrap();
Some(matrix.into_owned())
}
pub fn get_mean_and_std(&mut self) -> Option<(f64, f64)> {
if !self.valid_sizes() {
return None;
}
self.apply_rates_of_change();
let m = self.get_matrix();
if m.is_none() {
return None;
}
let m = m.unwrap();
let cov = m.cov(1.);
if cov.is_err() {
return None;
}
let cov = cov.unwrap();
let mean_return = m.mean_axis(Axis(1));
if mean_return.is_none() {
return None;
}
let mean_return = mean_return.unwrap();
let asset_weights = Array::from_vec(
self.get_asset_weight().collect::<Vec<f64>>()
).to_owned();
let porfolio_mean_return = mean_return.dot(&asset_weights);
let portfolio_stddev = f64::sqrt(asset_weights.t().dot(&cov).dot(&asset_weights));
Some((porfolio_mean_return, portfolio_stddev))
}
pub fn value_at_risk(&mut self, confidence: f64) -> Option<f64> {
portfolio_value_at_risk(self, confidence)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn var_test() {
let assets = vec![
PortfolioAsset::new(0.3, "awdad".to_string(), vec![2f64, 3f64, 4f64]),
PortfolioAsset::new(0.7, "awdad".to_string(), vec![1f64, 6f64, 8f64]),
];
let m = Portfolio::from(assets).get_matrix().unwrap();
println!("matrix 0; {:?}", m);
let col = m.row(0);
println!("column 0; {:?}", col);
let cov = m.cov(1.);
println!("cov 0; {:?}", cov);
col.len();
}
}

@ -1,2 +1,2 @@
pub mod historical;
pub mod varcovar;
pub mod varcovar;

@ -1,10 +1,11 @@
use crate::util::roc::rates_of_change;
use crate::stats;
use crate::util::roc::rates_of_change;
use ndarray_stats::CorrelationExt;
use crate::risk::portfolio::Portfolio;
#[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
pub fn value_at_risk(values: &[f64], confidence: f64) -> f64 {
@ -16,4 +17,44 @@ pub fn value_at_risk(values: &[f64], confidence: f64) -> f64 {
let n = Normal::new(0.0, 1.0).unwrap();
mean + std_dev * n.inverse_cdf(confidence)
}
pub fn portfolio_value_at_risk(portfolio: &mut Portfolio, confidence: f64) -> Option<f64> {
match portfolio.get_mean_and_std() {
None => None,
Some((mean, std_dev)) => {
let n = Normal::new(0.0, 1.0).unwrap();
Some(mean + std_dev * n.inverse_cdf(confidence))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::risk::portfolio::PortfolioAsset;
#[test]
fn var_test() {
let assets = vec![
PortfolioAsset::new(0.3, "awdad".to_string(), vec![2f64, 3f64, 4f64]),
PortfolioAsset::new(0.7, "awdad".to_string(), vec![1f64, 6f64, 8f64]),
];
let mut portfolio = Portfolio::from(assets);
portfolio_value_at_risk(&mut portfolio, 0.1);
}
#[test]
fn var_test_one_asset() {
let assets = vec![
PortfolioAsset::new(0.3, "awdad".to_string(), vec![2f64, 3f64, 4f64])
];
let mut portfolio = Portfolio::from(assets);
portfolio_value_at_risk(&mut portfolio, 0.1);
}
}

File diff suppressed because one or more lines are too long

@ -4,6 +4,11 @@ use pyo3::prelude::*;
mod pyfinlib {
use super::*;
#[pymodule_export]
use finlib::risk::portfolio::Portfolio;
#[pymodule_export]
use finlib::risk::portfolio::PortfolioAsset;
#[pymodule_init]
fn init(m: &Bound<'_, PyModule>) -> PyResult<()> {
pyo3_log::init();