use nalgebra::{DMatrix, DVector};
use crate::{PortfolioError, Result};
pub fn returns_from_prices(prices: &DMatrix<f64>) -> Result<DMatrix<f64>> {
let (rows, cols) = prices.shape();
if rows < 2 {
return Err(PortfolioError::InvalidArgument(
"need at least two rows of prices to compute returns".into(),
));
}
let mut out = DMatrix::<f64>::zeros(rows - 1, cols);
for j in 0..cols {
for i in 1..rows {
let prev = prices[(i - 1, j)];
let curr = prices[(i, j)];
out[(i - 1, j)] = curr / prev - 1.0;
}
}
Ok(out)
}
pub fn log_returns_from_prices(prices: &DMatrix<f64>) -> Result<DMatrix<f64>> {
let (rows, cols) = prices.shape();
if rows < 2 {
return Err(PortfolioError::InvalidArgument(
"need at least two rows of prices to compute log returns".into(),
));
}
let mut out = DMatrix::<f64>::zeros(rows - 1, cols);
for j in 0..cols {
for i in 1..rows {
out[(i - 1, j)] = (prices[(i, j)] / prices[(i - 1, j)]).ln();
}
}
Ok(out)
}
pub fn column_means(m: &DMatrix<f64>) -> DVector<f64> {
let (rows, cols) = m.shape();
let mut out = DVector::<f64>::zeros(cols);
if rows == 0 {
return out;
}
for j in 0..cols {
let mut acc = 0.0;
for i in 0..rows {
acc += m[(i, j)];
}
out[j] = acc / rows as f64;
}
out
}
pub fn sample_covariance(returns: &DMatrix<f64>) -> Result<DMatrix<f64>> {
let (rows, cols) = returns.shape();
if rows < 2 {
return Err(PortfolioError::InvalidArgument(
"need at least two observations for sample covariance".into(),
));
}
let means = column_means(returns);
let mut centered = returns.clone();
for j in 0..cols {
let mu = means[j];
for i in 0..rows {
centered[(i, j)] -= mu;
}
}
let cov = centered.transpose() * ¢ered / (rows as f64 - 1.0);
Ok(cov)
}
pub(crate) fn assert_square(m: &DMatrix<f64>, label: &str) -> Result<usize> {
let (r, c) = m.shape();
if r != c {
return Err(PortfolioError::DimensionMismatch(format!(
"{label}: expected square matrix, got {r}x{c}"
)));
}
Ok(r)
}
pub fn clean_weights(weights: &DVector<f64>, cutoff: f64, rounding: Option<u32>) -> DVector<f64> {
let mut cleaned = weights.clone();
for v in cleaned.iter_mut() {
if v.abs() < cutoff {
*v = 0.0;
}
}
let total: f64 = cleaned.iter().sum();
if total.abs() > 1e-12 {
for v in cleaned.iter_mut() {
*v /= total;
}
}
if let Some(places) = rounding {
let factor = 10f64.powi(places as i32);
for v in cleaned.iter_mut() {
*v = (*v * factor).round() / factor;
}
}
cleaned
}
pub fn symmetrise(m: &mut DMatrix<f64>) {
let n = m.nrows();
debug_assert_eq!(m.nrows(), m.ncols());
for i in 0..n {
for j in (i + 1)..n {
let avg = 0.5 * (m[(i, j)] + m[(j, i)]);
m[(i, j)] = avg;
m[(j, i)] = avg;
}
}
}