Compare commits

...

3 Commits

Author SHA1 Message Date
a647271273
adding py publish to gitea 2025-02-17 00:09:26 +00:00
f344cd9d65
adding comments bit of parralel 2025-02-16 22:15:10 +00:00
80ebde1e0b
fixing var bug, rearranging 2025-02-16 16:57:07 +00:00
20 changed files with 268 additions and 490 deletions

1
.gitattributes vendored Normal file

@ -0,0 +1 @@
notebooks/** -linguist-detectable

@ -157,4 +157,26 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: publish
args: --package finlib --registry sargit
args: --package finlib --registry sargit
publishPy:
runs-on: ubuntu-latest
name: Publish Python Library
needs: [ build ] # for ignoring bad builds
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
github-server-url: https://gitea.sheep-ghoul.ts.net
- name: Install Python 3
uses: actions/setup-python@v4
with:
python-version: ${{ env.python-version }}
- uses: PyO3/maturin-action@v1
with:
command: publish
args: -r https://gitea.sheep-ghoul.ts.net/api/packages/sarsoo/pypi/simple -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_TOKEN }}
working-directory: ./pyfinlib

@ -105,7 +105,7 @@ jobs:
toolchain: stable
- name: Build Docs
run: cargo doc --no-deps --document-private-items
run: cargo doc --no-deps --document-private-items -F py,wasm,ffi
- name: Add redirect
run: echo '<meta http-equiv="refresh" content="0;url=finlib/index.html">' > target/doc/index.html
@ -160,6 +160,7 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: 22
registry-url: 'https://registry.npmjs.org' # This is just the default registry URL
- name: Publish
working-directory: ./finlib-wasm/pkg

8
Cargo.lock generated

@ -259,7 +259,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "finlib"
version = "0.0.2"
version = "0.0.3"
dependencies = [
"getrandom 0.2.15",
"log",
@ -274,7 +274,7 @@ dependencies = [
[[package]]
name = "finlib-ffi"
version = "0.0.2"
version = "0.0.3"
dependencies = [
"cbindgen",
"csbindgen",
@ -283,7 +283,7 @@ dependencies = [
[[package]]
name = "finlib-wasm"
version = "0.0.2"
version = "0.0.3"
dependencies = [
"console_error_panic_hook",
"console_log",
@ -606,7 +606,7 @@ dependencies = [
[[package]]
name = "pyfinlib"
version = "0.0.2"
version = "0.0.3"
dependencies = [
"finlib",
"log",

@ -14,7 +14,7 @@ default-members = [
]
[workspace.package]
version = "0.0.2"
version = "0.0.3"
authors = ["sarsoo <andy@sarsoo.xyz>"]
description = "Quant finance functions implemented in Rust"
edition = "2021"

@ -1,8 +1,10 @@
import { ValueAtRisk, Portfolio, PortfolioAsset } from "finlib";
import { ValueAtRisk, Portfolio, PortfolioAsset, init_logging } from "finlib";
init_logging();
console.log(ValueAtRisk.varcovar([1, 2, 3, 4], 0.1));
console.log(ValueAtRisk.varcovar([1, 2, 3, 4], 0.05));
let portfolio = new Portfolio([new PortfolioAsset(1.0, "test", [1.0, 2.0, 3.0])]);
console.log(portfolio.isValid());
console.log(portfolio.valueAtRisk(0.1));
console.log(portfolio.valueAtRisk(0.1, 1000000));

@ -2,8 +2,13 @@ use wasm_bindgen::prelude::wasm_bindgen;
use console_log;
use log::Level;
#[wasm_bindgen(start)]
fn start() {
// #[wasm_bindgen(start)]
// fn start() {
//
// }
#[wasm_bindgen]
pub fn init_logging() {
if let Err(_) = console_log::init_with_level(Level::Debug) {
}

@ -1,3 +1,5 @@
//! FFI specific functionality to define the struct function interfaces in Python and WASM
#[cfg(feature = "py")]
pub mod py;
#[cfg(feature = "wasm")]

@ -1,3 +1,4 @@
//! Compound interest etc
pub fn compound_32(principal: f32, rate: f32, time: f32, n: f32) -> f32 {
principal * f32::powf( 1f32 + (rate / n), time * n)

@ -1,5 +1,33 @@
//! # Quant finance functionality for Rust with FFIs to C/C++, C#, Python and WASM
pub mod interest;
pub mod stats;
pub mod util;
pub mod risk;
pub mod ffi;
pub mod ffi;
#[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()
}
}
};
}

@ -1,5 +1,3 @@
use statrs::distribution::{ContinuousCDF, Normal};
pub fn mean_investment(portfolio_mean_change: f64, initial_investment: f64) -> f64 {
(1. + portfolio_mean_change) * initial_investment
}

@ -1,3 +1,5 @@
//! Calculating risk for a given asset or portfolio using Value at Risk, [`var`]
pub mod var;
pub mod portfolio;
pub mod forecast;

@ -1,17 +1,23 @@
use log::{debug, error, info};
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, portfolio_value_at_risk_percent};
use crate::stats;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use statrs::distribution::{ContinuousCDF, Normal};
use crate::risk::forecast::{mean_investment, std_dev_investment};
use crate::risk::var::varcovar::{investment_value_at_risk};
use crate::{stats};
use crate::util::roc::rates_of_change;
/// Describes a Portfolio as a collection of [`PortfolioAsset`]s
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[cfg_attr(feature = "py", pyclass)]
#[cfg_attr(feature = "ffi", repr(C))]
#[derive(Clone)]
#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub struct Portfolio {
assets: Vec<PortfolioAsset>
}
@ -25,10 +31,11 @@ pub enum ValueType {
RateOfChange
}
/// Describes a single instrument as a list of previous values with an associated portfolio proportion
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[cfg_attr(feature = "py", pyclass)]
#[cfg_attr(feature = "ffi", repr(C))]
#[derive(Clone)]
#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub struct PortfolioAsset {
portfolio_weight: f64,
name: String,
@ -43,6 +50,7 @@ impl PortfolioAsset {
}
}
/// If the asset's values have been given as absolute values, convert those to a percentage change between each
pub fn apply_rates_of_change(&mut self) {
match self.value_type {
ValueType::Absolute => {
@ -53,9 +61,13 @@ impl PortfolioAsset {
}
}
/// Get the mean and standard deviation of the rates of change of an asset
///
/// returns (mean, std_dev)
pub fn get_mean_and_std(&self) -> Option<(f64, f64)> {
match self.value_type {
ValueType::Absolute => {
info!("[{}] Asset's values are currently absolute, calculating rates of change first", self.name);
let roc = rates_of_change(&self.values).collect::<Vec<f64>>();
Some((stats::mean(&roc), stats::sample_std_dev(&roc)))
}
@ -73,18 +85,32 @@ impl Portfolio {
}
}
/// Return the proportions of a portfolio's assets
///
/// In a properly formed Portfolio these will add up to 1.0
pub fn get_asset_weight(&self) -> impl Iterator<Item=f64> + use<'_> {
self.assets
.iter()
.map(|x| x.portfolio_weight)
}
/// Convert a portfolio of assets with absolute values to the percentage change in values
pub fn apply_rates_of_change(&mut self) {
for asset in self.assets.iter_mut() {
asset.apply_rates_of_change();
#[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();
});
}
}
/// Do all the assets in the portfolio have the same number of values (required to perform matrix operations)
pub fn valid_sizes(&self) -> bool {
let mut last_value_length: Option<usize> = None;
@ -105,8 +131,9 @@ impl Portfolio {
true
}
/// Do the proportions of the assets in the portfolio add up to 100%?
pub fn valid_weights(&self) -> bool {
let mut weight = 1 as f64;
let mut weight = 1f64;
for asset in &self.assets {
weight -= asset.portfolio_weight;
@ -119,6 +146,7 @@ impl Portfolio {
self.valid_sizes() && self.valid_weights()
}
/// Format the asset values in the portfolio as a matrix such that statistical operations can be applied to it
pub fn get_matrix(&self) -> Option<Array2<f64>> {
if self.assets.is_empty() || !self.valid_sizes() {
return None;
@ -127,36 +155,57 @@ impl Portfolio {
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();
#[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())
}
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
///
/// returns (mean, std_dev)
pub fn get_mean_and_std(&mut self) -> Option<(f64, f64)> {
if !self.valid_sizes() {
error!("Can't get portfolio mean and std dev because asset value counts arent't the same");
return None;
}
self.apply_rates_of_change();
let m = self.get_matrix();
if m.is_none() {
error!("Couldn't format portfolio as matrix");
return None;
}
let m = m.unwrap();
let cov = m.cov(1.);
if cov.is_err() {
error!("Failed to calculate portfolio covariance");
return None;
}
let cov = cov.unwrap();
let mean_return = m.mean_axis(Axis(1));
if mean_return.is_none() {
error!("Failed to calculate portfolio mean");
return None;
}
let mean_return = mean_return.unwrap();
@ -170,12 +219,38 @@ impl Portfolio {
Some((porfolio_mean_return, portfolio_stddev))
}
/// For a given confidence rate (0.01, 0.05, 0.10) and initial investment value, calculate the parametric value at risk
///
/// https://www.interviewqs.com/blog/value-at-risk
pub fn value_at_risk(&mut self, confidence: f64, initial_investment: f64) -> Option<f64> {
portfolio_value_at_risk(self, confidence, initial_investment)
match self.get_mean_and_std() {
None => None,
Some((mean, std_dev)) => {
debug!("Portfolio percent movement mean[{}], std dev[{}]", mean, std_dev);
let investment_mean = mean_investment(mean, initial_investment);
let investment_std_dev = std_dev_investment(std_dev, initial_investment);
debug!("Investment[{}] mean[{}], std dev[{}]", initial_investment, mean, std_dev);
let investment_var = investment_value_at_risk(confidence, investment_mean, investment_std_dev);
debug!("Investment[{}] value at risk [{}]", initial_investment, investment_var);
Some(initial_investment - investment_var)
}
}
}
/// For a given confidence rate (0.01, 0.05, 0.10) calculate the percentage change in an investment
///
/// https://www.interviewqs.com/blog/value-at-risk
pub fn value_at_risk_percent(&mut self, confidence: f64) -> Option<f64> {
portfolio_value_at_risk_percent(self, confidence)
match self.get_mean_and_std() {
None => None,
Some((mean, std_dev)) => {
let n = Normal::new(mean, std_dev).unwrap();
Some(n.inverse_cdf(confidence))
}
}
}
}

@ -1,2 +1,4 @@
//! Calculate Value at Risk using either the [`historical`] or parametric [`varcovar`] methods for an asset or portfolio
pub mod historical;
pub mod varcovar;

@ -1,12 +1,9 @@
use log::info;
use crate::stats;
use crate::util::roc::rates_of_change;
use crate::risk::portfolio::Portfolio;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use statrs::distribution::{ContinuousCDF, Normal};
use crate::risk::forecast::{mean_investment, std_dev_investment};
// https://medium.com/@serdarilarslan/value-at-risk-var-and-its-implementation-in-python-5c9150f73b0e
pub fn value_at_risk_percent(values: &[f64], confidence: f64) -> f64 {
@ -20,34 +17,6 @@ pub fn value_at_risk_percent(values: &[f64], confidence: f64) -> f64 {
n.inverse_cdf(confidence)
}
pub fn portfolio_value_at_risk_percent(portfolio: &mut Portfolio, confidence: f64) -> Option<f64> {
match portfolio.get_mean_and_std() {
None => None,
Some((mean, std_dev)) => {
let n = Normal::new(mean, std_dev).unwrap();
Some(n.inverse_cdf(confidence))
}
}
}
pub fn portfolio_value_at_risk(portfolio: &mut Portfolio, confidence: f64, initial_investment: f64) -> Option<f64> {
match portfolio.get_mean_and_std() {
None => None,
Some((mean, std_dev)) => {
let investment_mean = mean_investment(mean, initial_investment);
let investment_std_dev = std_dev_investment(std_dev, std_dev);
info!("{:?}, {:?}", investment_mean, investment_std_dev);
let investment_var = investment_value_at_risk(confidence, investment_mean, investment_std_dev);
println!("{:?}", investment_var);
Some(initial_investment - investment_var)
}
}
}
pub fn investment_value_at_risk(confidence: f64, investment_mean: f64, investment_std_dev: f64) -> f64 {
let n = Normal::new(investment_mean, investment_std_dev).unwrap();
@ -61,6 +30,7 @@ pub fn scale_value_at_risk(initial_value: f64, time_cycles: isize) -> f64 {
#[cfg(test)]
mod tests {
use super::*;
use crate::risk::portfolio::Portfolio;
use crate::risk::portfolio::PortfolioAsset;
#[test]
@ -72,7 +42,7 @@ mod tests {
let mut portfolio = Portfolio::from(assets);
portfolio_value_at_risk_percent(&mut portfolio, 0.1);
portfolio.value_at_risk_percent(0.1);
}
@ -84,7 +54,7 @@ mod tests {
let mut portfolio = Portfolio::from(assets);
portfolio_value_at_risk_percent(&mut portfolio, 0.1);
portfolio.value_at_risk_percent(0.1);
}
#[test]
@ -96,8 +66,8 @@ mod tests {
let mut portfolio = Portfolio::from(assets);
println!("{:?}", portfolio_value_at_risk(&mut portfolio, 0.01, 1_000_000.));
println!("{:?}", portfolio_value_at_risk(&mut portfolio, 0.1, 1_000_000.));
println!("{:?}", portfolio_value_at_risk(&mut portfolio, 0.5, 1_000_000.));
println!("{:?}", portfolio.value_at_risk(0.01, 1_000_000.));
println!("{:?}", portfolio.value_at_risk(0.1, 1_000_000.));
println!("{:?}", portfolio.value_at_risk(0.5, 1_000_000.));
}
}

@ -1,13 +1,9 @@
mod covariance;
pub use covariance::*;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
pub fn mean(slice: &[f64]) -> f64
{
slice
// .par_iter()
.iter()
.sum::<f64>() / slice.len() as f64
}
@ -16,7 +12,6 @@ pub fn population_variance(slice: &[f64]) -> f64
{
let mean = mean(slice);
slice
// .par_iter()
.iter()
.map(|x| f64::powi(x - mean, 2))
.sum::<f64>()
@ -27,7 +22,6 @@ pub fn sample_variance(slice: &[f64]) -> f64
{
let mean = mean(slice);
slice
// .par_iter()
.iter()
.map(|x| f64::powi(x - mean, 2))
.sum::<f64>()

@ -1,5 +1,9 @@
use log::error;
pub fn dot_product(a: &[f64], b: &[f64]) -> f64 {
if a.len() != b.len() {
error!("Can't dot product two vectors of different lengths, a = {}, b = {}", a.len(), b.len());
}
assert_eq!(a.len(), b.len());
a.iter()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -10,7 +10,7 @@ mod pyfinlib {
use finlib::risk::portfolio::PortfolioAsset;
#[pymodule_init]
fn init(m: &Bound<'_, PyModule>) -> PyResult<()> {
fn init(_m: &Bound<'_, PyModule>) -> PyResult<()> {
pyo3_log::init();
Ok(())
}
@ -30,7 +30,7 @@ mod pyfinlib {
use super::*;
#[pymodule]
mod var {
mod value_at_risk {
use super::*;
#[pyfunction]
@ -42,6 +42,11 @@ mod pyfinlib {
fn varcovar(values: Vec<f64>, confidence: f64) -> PyResult<f64> {
Ok(finlib::risk::var::varcovar::value_at_risk_percent(&values, confidence))
}
#[pyfunction]
fn scale_value_at_risk(initial_value: f64, time_cycles: isize) -> PyResult<f64> {
Ok(finlib::risk::var::varcovar::scale_value_at_risk(initial_value, time_cycles))
}
}
}