use super::{PortfolioError, PortfolioResult};
use nalgebra::DMatrix;
pub(crate) fn check_returns_matrix(returns: &DMatrix<f64>) -> PortfolioResult<()> {
if returns.nrows() == 0 || returns.ncols() == 0 {
return Err(PortfolioError::Empty("returns".to_string()));
}
if returns.nrows() < 2 {
return Err(PortfolioError::InsufficientObservations);
}
if returns.ncols() < 1 {
return Err(PortfolioError::NoAssets);
}
if !returns.iter().all(|v| v.is_finite()) {
return Err(PortfolioError::NonFinite("returns".to_string()));
}
Ok(())
}
pub(crate) fn check_cov(cov: &DMatrix<f64>) -> PortfolioResult<()> {
if cov.nrows() != cov.ncols() {
return Err(PortfolioError::NonSquareCov(cov.nrows(), cov.ncols()));
}
if cov.nrows() < 1 {
return Err(PortfolioError::Empty("covariance".to_string()));
}
if !cov.iter().all(|v| v.is_finite()) {
return Err(PortfolioError::NonFinite("covariance".to_string()));
}
Ok(())
}
pub fn sample_cov(returns: &DMatrix<f64>) -> PortfolioResult<DMatrix<f64>> {
check_returns_matrix(returns)?;
let t = returns.nrows() as f64;
let n = returns.ncols();
let mean = returns.row_mean();
let mut centered = returns.clone();
for mut row in centered.row_iter_mut() {
row -= &mean;
}
let cov = (centered.transpose() * ¢ered) / (t - 1.0);
debug_assert_eq!(cov.nrows(), n);
Ok(cov)
}
pub fn ewma_cov(returns: &DMatrix<f64>, halflife: f64) -> PortfolioResult<DMatrix<f64>> {
check_returns_matrix(returns)?;
if halflife <= 0.0 {
return Err(PortfolioError::InvalidHalflife(halflife));
}
let t = returns.nrows();
let n = returns.ncols();
let lam = 0.5_f64.powf(1.0 / halflife);
let raw_w: Vec<f64> = (0..t).map(|i| lam.powi((t - 1 - i) as i32)).collect();
let w_sum: f64 = raw_w.iter().sum();
let w: Vec<f64> = raw_w.iter().map(|v| v / w_sum).collect();
let mut mean = vec![0.0_f64; n];
for (i, wi) in w.iter().enumerate() {
for j in 0..n {
mean[j] += wi * returns[(i, j)];
}
}
let mut cov = DMatrix::<f64>::zeros(n, n);
for (i, wi) in w.iter().enumerate() {
let centered: Vec<f64> = (0..n).map(|j| returns[(i, j)] - mean[j]).collect();
for a in 0..n {
for b in 0..n {
cov[(a, b)] += wi * centered[a] * centered[b];
}
}
}
let sum_w2: f64 = w.iter().map(|v| v * v).sum();
let denom = 1.0 - sum_w2;
if denom > 1e-12 {
cov /= denom;
}
if !cov.iter().all(|v| v.is_finite()) {
return Err(PortfolioError::NonFinite("ewma_cov result".to_string()));
}
Ok(cov)
}
pub fn ledoit_wolf_cov(returns: &DMatrix<f64>) -> PortfolioResult<DMatrix<f64>> {
check_returns_matrix(returns)?;
let t = returns.nrows();
let n = returns.ncols();
if n == 1 {
return sample_cov(returns);
}
let t_f = t as f64;
let n_f = n as f64;
let mean = returns.row_mean();
let mut x = returns.clone();
for mut row in x.row_iter_mut() {
row -= &mean;
}
let s_pop = (x.transpose() * &x) / t_f;
let mu = s_pop.trace() / n_f;
let diag_target: Vec<f64> = (0..n).map(|i| s_pop[(i, i)] - mu).collect();
let mut pi_sum = 0.0_f64;
let mut rho_sum = 0.0_f64;
for row in 0..t {
let xt: Vec<f64> = (0..n).map(|j| x[(row, j)]).collect();
let mut diag_diff = vec![0.0_f64; n];
for a in 0..n {
for b in 0..n {
let s_t_ab = xt[a] * xt[b];
let diff = s_t_ab - s_pop[(a, b)];
pi_sum += diff * diff;
if a == b {
diag_diff[a] = diff;
}
}
}
for i in 0..n {
rho_sum += diag_diff[i] * diag_target[i];
}
}
let pi_hat = pi_sum / t_f;
let rho_hat = rho_sum / t_f;
let mut gamma_hat = 0.0_f64;
for a in 0..n {
for b in 0..n {
let target = if a == b { mu } else { 0.0 };
let d = s_pop[(a, b)] - target;
gamma_hat += d * d;
}
}
let shrinkage = if gamma_hat < 1e-18 {
0.0
} else {
let kappa_hat = (pi_hat - rho_hat) / gamma_hat;
(kappa_hat / t_f).clamp(0.0, 1.0)
};
let s_sample = sample_cov(returns)?; let mut cov = s_sample * (1.0 - shrinkage);
for i in 0..n {
cov[(i, i)] += shrinkage * mu;
}
if !cov.iter().all(|v| v.is_finite()) {
return Err(PortfolioError::NonFinite(
"ledoit_wolf_cov result".to_string(),
));
}
Ok(cov)
}
#[cfg(test)]
mod tests {
use super::*;
fn toy_returns() -> DMatrix<f64> {
DMatrix::from_row_slice(
5,
3,
&[
0.01, 0.02, -0.01, 0.015, -0.005, 0.02, -0.02, 0.01, 0.005, 0.005, 0.03, -0.01,
0.0, 0.01, 0.015,
],
)
}
#[test]
fn sample_cov_symmetric_and_finite() {
let r = toy_returns();
let cov = sample_cov(&r).unwrap();
assert_eq!(cov.nrows(), 3);
for i in 0..3 {
for j in 0..3 {
assert!((cov[(i, j)] - cov[(j, i)]).abs() < 1e-12);
}
}
}
#[test]
fn sample_cov_matches_numpy_cov_ddof1_fixture() {
let r = toy_returns();
let cov = sample_cov(&r).unwrap();
let col0: Vec<f64> = (0..5).map(|i| r[(i, 0)]).collect();
let mean0 = col0.iter().sum::<f64>() / 5.0;
let var0 = col0.iter().map(|v| (v - mean0).powi(2)).sum::<f64>() / 4.0;
assert!((cov[(0, 0)] - var0).abs() < 1e-12);
}
#[test]
fn ewma_cov_positive_definite_diag() {
let r = toy_returns();
let cov = ewma_cov(&r, 3.0).unwrap();
for i in 0..3 {
assert!(cov[(i, i)] > 0.0);
}
}
#[test]
fn ewma_cov_rejects_nonpositive_halflife() {
let r = toy_returns();
let err = ewma_cov(&r, 0.0).unwrap_err();
assert_eq!(err, PortfolioError::InvalidHalflife(0.0));
}
#[test]
fn ledoit_wolf_shrinks_toward_identity_scaled_target() {
let r = toy_returns();
let lw = ledoit_wolf_cov(&r).unwrap();
let s = sample_cov(&r).unwrap();
for i in 0..3 {
assert!(lw[(i, i)] > 0.0);
assert!(lw[(i, i)].is_finite());
}
let _ = s;
}
#[test]
fn ledoit_wolf_single_asset_falls_back_to_sample_cov() {
let r = DMatrix::from_row_slice(4, 1, &[0.01, -0.02, 0.03, 0.0]);
let lw = ledoit_wolf_cov(&r).unwrap();
let s = sample_cov(&r).unwrap();
assert!((lw[(0, 0)] - s[(0, 0)]).abs() < 1e-12);
}
}