//! Statistical moment calculations for portfolio analysis. //! //! Calculates first four moments (mean, variance, skewness, kurtosis) //! and their cross-asset equivalents (covariance, co-skewness, co-kurtosis). use quantumport_shared::{ AssetReturns, CokurtosisTensor, CoskewnessTensor, CovarianceMatrix, MomentStatistics, }; /// Moment calculator for portfolio returns. #[derive(Debug)] pub struct MomentCalculator { /// Annualization factor (252 trading days) annualization_factor: f64, } impl Default for MomentCalculator { fn default() -> Self { Self::new() } } impl MomentCalculator { /// Create a new moment calculator. #[must_use] pub fn new() -> Self { Self { annualization_factor: 252.0, } } /// Calculate individual asset statistics. #[must_use] pub fn calculate_asset_stats(&self, returns: &[f64]) -> MomentStatistics { let n = returns.len() as f64; if n == 0.0 { return MomentStatistics { mean: 0.0, variance: 0.0, skewness: 0.0, kurtosis: 3.0, }; } // Mean (annualized) let mean: f64 = returns.iter().sum::() / n * self.annualization_factor; // Variance (annualized) let daily_mean = returns.iter().sum::() / n; let variance: f64 = returns .iter() .map(|r| (r - daily_mean).powi(2)) .sum::() / (n - 1.0) * self.annualization_factor; // Standard deviation for normalization let std = (returns .iter() .map(|r| (r - daily_mean).powi(2)) .sum::() / (n - 1.0)) .sqrt(); // Skewness let skewness = if std > 0.0 { let m3: f64 = returns .iter() .map(|r| ((r - daily_mean) / std).powi(3)) .sum::(); m3 * n / ((n - 1.0) * (n - 2.0)) } else { 0.0 }; // Kurtosis (excess) let kurtosis = if std > 0.0 { let m4: f64 = returns .iter() .map(|r| ((r - daily_mean) / std).powi(4)) .sum::(); (m4 / n) - 3.0 } else { 0.0 }; MomentStatistics { mean, variance, skewness, kurtosis, } } /// Calculate covariance matrix for multiple assets. #[must_use] pub fn calculate_covariance(&self, returns: &[AssetReturns]) -> CovarianceMatrix { let n = returns.len(); let symbols: Vec = returns.iter().map(|r| r.symbol.clone()).collect(); // Calculate means let means: Vec = returns .iter() .map(|r| r.returns.iter().sum::() / r.returns.len() as f64) .collect(); // Calculate covariance matrix let mut data = vec![0.0; n * n]; let num_obs = returns[0].returns.len() as f64; for i in 0..n { for j in 0..n { let mut cov = 0.0; for t in 0..returns[i].returns.len() { let dev_i = returns[i].returns[t] - means[i]; let dev_j = returns[j].returns[t] - means[j]; cov += dev_i * dev_j; } // Annualize data[i * n + j] = cov / (num_obs - 1.0) * self.annualization_factor; } } CovarianceMatrix { symbols, data, dimension: n, } } /// Calculate co-skewness tensor. #[must_use] pub fn calculate_coskewness(&self, returns: &[AssetReturns]) -> CoskewnessTensor { let n = returns.len(); let symbols: Vec = returns.iter().map(|r| r.symbol.clone()).collect(); // Calculate means and standard deviations let stats: Vec<(f64, f64)> = returns .iter() .map(|r| { let mean = r.returns.iter().sum::() / r.returns.len() as f64; let var = r.returns.iter().map(|x| (x - mean).powi(2)).sum::() / (r.returns.len() - 1) as f64; (mean, var.sqrt()) }) .collect(); // Calculate co-skewness tensor let mut data = vec![0.0; n * n * n]; let num_obs = returns[0].returns.len() as f64; for i in 0..n { for j in 0..n { for k in 0..n { let mut coskew = 0.0; for t in 0..returns[i].returns.len() { let z_i = if stats[i].1 > 0.0 { (returns[i].returns[t] - stats[i].0) / stats[i].1 } else { 0.0 }; let z_j = if stats[j].1 > 0.0 { (returns[j].returns[t] - stats[j].0) / stats[j].1 } else { 0.0 }; let z_k = if stats[k].1 > 0.0 { (returns[k].returns[t] - stats[k].0) / stats[k].1 } else { 0.0 }; coskew += z_i * z_j * z_k; } data[i * n * n + j * n + k] = coskew / num_obs; } } } CoskewnessTensor { symbols, data, dimension: n, } } /// Calculate co-kurtosis tensor. #[must_use] pub fn calculate_cokurtosis(&self, returns: &[AssetReturns]) -> CokurtosisTensor { let n = returns.len(); let symbols: Vec = returns.iter().map(|r| r.symbol.clone()).collect(); // Calculate means and standard deviations let stats: Vec<(f64, f64)> = returns .iter() .map(|r| { let mean = r.returns.iter().sum::() / r.returns.len() as f64; let var = r.returns.iter().map(|x| (x - mean).powi(2)).sum::() / (r.returns.len() - 1) as f64; (mean, var.sqrt()) }) .collect(); // Calculate co-kurtosis tensor (simplified - diagonal elements only for demo) let mut data = vec![0.0; n * n * n * n]; let num_obs = returns[0].returns.len() as f64; // Calculate diagonal elements (i,i,i,i) for i in 0..n { let mut cokurt = 0.0; for t in 0..returns[i].returns.len() { let z_i = if stats[i].1 > 0.0 { (returns[i].returns[t] - stats[i].0) / stats[i].1 } else { 0.0 }; cokurt += z_i.powi(4); } data[i * n * n * n + i * n * n + i * n + i] = cokurt / num_obs - 3.0; } CokurtosisTensor { symbols, data, dimension: n, } } /// Calculate portfolio moments given weights. #[must_use] pub fn calculate_portfolio_moments( &self, weights: &[f64], returns: &[AssetReturns], ) -> MomentStatistics { let n = returns[0].returns.len(); // Calculate portfolio returns let mut portfolio_returns = vec![0.0; n]; for (i, ret) in returns.iter().enumerate() { for (t, r) in ret.returns.iter().enumerate() { portfolio_returns[t] += weights[i] * r; } } self.calculate_asset_stats(&portfolio_returns) } } #[cfg(test)] mod tests { use super::*; fn create_test_returns() -> Vec { vec![ AssetReturns { symbol: "A".to_string(), returns: vec![ 0.01, -0.02, 0.015, 0.005, -0.01, 0.02, -0.005, 0.01, 0.008, -0.012, ], start_date: "2024-01-01".to_string(), end_date: "2024-01-10".to_string(), }, AssetReturns { symbol: "B".to_string(), returns: vec![ 0.002, 0.001, 0.003, -0.001, 0.002, 0.001, 0.002, -0.001, 0.003, 0.001, ], start_date: "2024-01-01".to_string(), end_date: "2024-01-10".to_string(), }, ] } #[test] fn test_calculator_creation() { let calc = MomentCalculator::new(); assert!((calc.annualization_factor - 252.0).abs() < 1e-10); } #[test] fn test_asset_stats() { let calc = MomentCalculator::new(); let returns = vec![0.01, -0.02, 0.015, 0.005, -0.01]; let stats = calc.calculate_asset_stats(&returns); assert!(stats.mean.abs() < 1.0); // Reasonable annualized return assert!(stats.variance > 0.0); } #[test] fn test_covariance_matrix() { let calc = MomentCalculator::new(); let returns = create_test_returns(); let cov = calc.calculate_covariance(&returns); assert_eq!(cov.dimension, 2); assert_eq!(cov.data.len(), 4); // Variance should be positive assert!(cov.get(0, 0).unwrap() > 0.0); assert!(cov.get(1, 1).unwrap() > 0.0); // Covariance should be symmetric assert!((cov.get(0, 1).unwrap() - cov.get(1, 0).unwrap()).abs() < 1e-10); } #[test] fn test_coskewness() { let calc = MomentCalculator::new(); let returns = create_test_returns(); let coskew = calc.calculate_coskewness(&returns); assert_eq!(coskew.dimension, 2); assert!(!coskew.data.is_empty()); } #[test] fn test_cokurtosis() { let calc = MomentCalculator::new(); let returns = create_test_returns(); let cokurt = calc.calculate_cokurtosis(&returns); assert_eq!(cokurt.dimension, 2); assert!(!cokurt.data.is_empty()); } #[test] fn test_portfolio_moments() { let calc = MomentCalculator::new(); let returns = create_test_returns(); let weights = vec![0.6, 0.4]; let stats = calc.calculate_portfolio_moments(&weights, &returns); assert!(stats.variance > 0.0); } #[test] fn test_empty_returns() { let calc = MomentCalculator::new(); let stats = calc.calculate_asset_stats(&[]); assert!((stats.mean).abs() < 1e-10); assert!((stats.variance).abs() < 1e-10); } }