Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
670 changes: 472 additions & 198 deletions Cargo.lock

Large diffs are not rendered by default.

561 changes: 415 additions & 146 deletions Cargo.lock.MSRV

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,22 @@ rand = "0.9.2"
rand_distr = "0.5.1"
rayon = "1.10.0"
statrs = "0.18.0"
ndarray = "0.17.1"
ndarray-stats = "0.7.0"
csv = "=1.3.1"
reqwest = { version = "0.12.23", features = ["json"] }
serde = "1.0.219"
serde-aux = "4.7.0"
<<<<<<< HEAD
ndarray = "0.16.1"
ndarray-stats = "0.6.0"
<<<<<<< HEAD
csv = "=1.3.1"
=======
csv = "=1.3.0"
>>>>>>> 39c208f (chore(deps): bump tokio from 1.47.1 to 1.48.0 (#90))
=======
>>>>>>> 8ae270c (fix: code review changes + allocated arrays)
tokio = "1.48.0"

[dev-dependencies]
Expand Down
6 changes: 6 additions & 0 deletions bindings/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
[![codecov-quantrs][codecov-badge]][codecov-url]
![Crates.io MSRV][crates-msrv-badge]
![Crates.io downloads][crates-download-badge]
<<<<<<< HEAD
[![PyPI version][pypi-version]][pypi-url]
=======
>>>>>>> 459f544 (feat(bindings): add python bindings for fixed_income (#79))

[actions-test-badge]: https://github.com/carlobortolan/quantrs/actions/workflows/ci.yml/badge.svg
[crates-badge]: https://img.shields.io/crates/v/quantrs.svg
Expand All @@ -19,10 +22,13 @@
[codecov-url]: https://codecov.io/gh/carlobortolan/quantrs
[crates-msrv-badge]: https://img.shields.io/crates/msrv/quantrs
[crates-download-badge]: https://img.shields.io/crates/d/quantrs
<<<<<<< HEAD
[pypi-version]: https://img.shields.io/pypi/v/quantrs.svg
[pypi-url]: https://pypi.org/project/quantrs
<!-- [![Python versions](https://img.shields.io/pypi/pyversions/quantrs.svg)](https://pypi.org/project/quantrs/) -->
<!-- [![PyPI downloads](https://img.shields.io/pypi/dm/quantrs.svg)](https://pypi.org/project/quantrs/) -->
=======
>>>>>>> 459f544 (feat(bindings): add python bindings for fixed_income (#79))

Quantrs is a tiny quantitative finance library for Rust. These are the Python bindings for it.
It is designed to be as intuitive and easy to use as possible so that you can work with derivatives without the need to write complex code or have a PhD in reading QuantLib documentation.
Expand Down
5,788 changes: 5,788 additions & 0 deletions examples/data/ETFprices.csv

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions examples/portfolio_optimization.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use quantrs::portfolio::{Portfolio, ReturnsCalculation};

#[warn(unused_variables)]
fn main() {
let data_path = "examples/data/asset_prices.csv";
let risk_free_rate = 0.01; // 1%
let expected_return = 0.1; // 10%
let returns_calc = ReturnsCalculation::Log;
let portfolio = Portfolio::new(data_path, risk_free_rate, expected_return, returns_calc);

println!("{}", portfolio);
}
10 changes: 10 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,19 @@ mod macros {
pub mod data;
pub mod fixed_income;
pub mod options;
<<<<<<< HEAD
pub mod portfolio;
<<<<<<< HEAD
=======
=======
>>>>>>> 459f544 (feat(bindings): add python bindings for fixed_income (#79))

pub use fixed_income::*;
pub use options::*;

#[cfg(feature = "python")]
pub mod python;
<<<<<<< HEAD
=======
>>>>>>> af72ef0 (feat(bindings): add python bindings for fixed_income (#79))
>>>>>>> 459f544 (feat(bindings): add python bindings for fixed_income (#79))
5 changes: 5 additions & 0 deletions src/portfolio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//! Module for Portfolio Optimization techniques

pub use mean_variance::{Portfolio, ReturnsCalculation};

mod mean_variance;
206 changes: 206 additions & 0 deletions src/portfolio/mean_variance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
//! Module implementing Mean-Variance Portfolio Optimization
//!
//! Current Support:
//! - Portfolio struct to hold assets, mean returns, covariance matrix
//! - Calculation of mean returns and covariance matrix from CSV data
//! - Support for Simple and Log returns calculation methods
//!
//! Next Steps:
//! - Implement optimization algorithms (e.g., Markowitz optimization)

use csv::ReaderBuilder;
use ndarray::{s, Array2, Axis, Zip};
use ndarray_stats::CorrelationExt;
use std::fmt;

#[derive(Debug)]
pub enum ReturnsCalculation {
Simple,
Log,
}

#[derive(Debug)]
/// Struct representing a portfolio of assets.
pub struct Portfolio {
/// List of asset tickers in the portfolio.
tickers: Vec<String>,
/// Mean returns of the assets.
mean_returns: Vec<f64>,
/// Covariance matrix of the asset returns.
covariance_matrix: Array2<f64>,
/// Risk-free rate for the market
risk_free_rate: f64,
/// Expected return for the portfolio
expected_return: f64,
/// Method used to calculate returns (Simple or Log)
returns_calculation: ReturnsCalculation,
/// Weights of the assets in the portfolio (if calculated)
/// None if not yet calculated
weights: Option<Vec<f64>>,
/// Internal storage of returns data
_returns: Array2<f64>,
}

impl Portfolio {
pub fn new(
data_path: &str,
risk_free_rate: f64,
expected_return: f64,
returns_calc: ReturnsCalculation,
) -> Self {
// Read data from CSV file given file path
let mut rdr = ReaderBuilder::new()
.has_headers(true)
.from_path(data_path)
.expect("Failed to read CSV file");

// Extract tickers from headers
let tickers: Vec<String> = rdr
.headers()
.expect("Failed to read headers")
.iter()
.map(|s| s.to_string())
.collect();

// Read records and parse to f64, defaulting to 0.0 on parse failure
let records: Vec<Vec<f64>> = rdr
.records()
.map(|result| {
result
.expect("Failed to read record")
.iter()
.map(|s| s.parse::<f64>().unwrap_or(0.0))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed an edge case: If there is a missing or malformed value in the CSV, it currently defaults to 0.0, but this would cause issues afterwards:

  • In calculate_simple_returns, if prv is 0.0, the calculation (nxt - prv) / prv will result in inf or NaN (division by zero)
  • In calculate_log_returns, 0.0.ln() will result in -inf

If these inf or NaN values feed into the cov matrix, they will most likely ruin the portfolio optimization results. I think we should handle bad data differently, e.g., by filtering out bad rows entirely or carrying forward the previous day's price (or maybe you also have any other ideas?).

.collect()
})
.collect();

let n = tickers.len();
let m = records.len();

// Create ndarray from records to store prices
let prices = Array2::from_shape_vec((m, n), records.into_iter().flatten().collect())
.expect("Failed to create prices array");

// Calculate returns based on specified method
let returns = match returns_calc {
ReturnsCalculation::Simple => Self::calculate_simple_returns(&prices),
ReturnsCalculation::Log => Self::calculate_log_returns(&prices),
};

// Calculate mean returns
let mean_returns = returns.mean_axis(Axis(0)).unwrap().to_vec();

// Calculate covariance matrix
// Using unbiased estimator (N-1 in denominator) for sample covariance
// ddof = 1.
// If you want population covariance, use ddof = 0
// Transpose of returns is used as ndarray-stats expects variables in rows
let covariance_matrix = returns
.t()
.cov(1.0)
.expect("Failed to compute covariance matrix");

Self {
tickers,
mean_returns,
covariance_matrix,
risk_free_rate,
expected_return,
returns_calculation: returns_calc,
weights: None,
_returns: returns,
}
}

/// Function to calculate log returns
fn calculate_log_returns(prices: &Array2<f64>) -> Array2<f64> {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wanted to check if this is what you meant by in-place operations

Yes exactly! This looks perfect 👍

let (n, m) = prices.dim();
assert!(n >= 2, "Need at least 2 rows to compute returns");

let prev = prices.slice(s![..-1, ..]);
let next = prices.slice(s![1.., ..]);

let mut out = Array2::<f64>::zeros((n - 1, m));

Zip::from(&mut out)
.and(&next)
.and(&prev)
.for_each(|o, &nxt, &prv| {
*o = nxt.ln() - prv.ln();
});

out
}

/// Function to calculate simple returns
fn calculate_simple_returns(prices: &Array2<f64>) -> Array2<f64> {
let (n, m) = prices.dim();
assert!(n >= 2, "Need at least 2 rows to compute returns");

let prev = prices.slice(s![..-1, ..]); // P_t
let next = prices.slice(s![1.., ..]); // P_{t+1}

let mut out = Array2::<f64>::zeros((n - 1, m));

Zip::from(&mut out)
.and(&next)
.and(&prev)
.for_each(|o, &nxt, &prv| {
*o = (nxt - prv) / prv;
});

out
}
}

/// Implement Display trait for pretty-printing the Portfolio
impl fmt::Display for Portfolio {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Portfolio")?;
writeln!(f, "Tickers: {:?}", self.tickers)?;
writeln!(f, "Risk-Free Rate: {:.4}", self.risk_free_rate)?;
writeln!(f, "Expected Return: {:.4}", self.expected_return)?;
writeln!(f, "Returns Calculation: {}", self.returns_calculation)?;
writeln!(f, "Weights: {:?}", self.weights)?;

// Print mean returns as percentages
writeln!(f, "\nMean Returns (%):")?;
for (i, &mean_return) in self.mean_returns.iter().enumerate() {
writeln!(f, "{:>8}: {:>8.4}%", self.tickers[i], mean_return * 100.0)?;
}

// Print covariance matrix in a readable format
writeln!(
f,
"\nCovariance Matrix ({} x {}):",
self.covariance_matrix.nrows(),
self.covariance_matrix.ncols()
)?;
// Print header with ticker names
write!(f, " ")?; // spacing for row labels
for ticker in &self.tickers {
write!(f, "{:>10}", ticker)?;
}
writeln!(f)?;

// Print each row with ticker name as label
for (i, row) in self.covariance_matrix.outer_iter().enumerate() {
write!(f, "{:>8} ", self.tickers[i])?; // row label
for value in row {
write!(f, "{:>10.6}", value)?;
}
writeln!(f)?;
}
Ok(())
}
}

/// Implement Display trait for ReturnsCalculation enum
impl fmt::Display for ReturnsCalculation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ReturnsCalculation::Simple => write!(f, "Simple"),
ReturnsCalculation::Log => write!(f, "Log"),
}
}
}