use scirs2_core::ndarray::Array1;
use scirs2_core::numeric::Float;
use crate::error::{Result, TimeSeriesError};
#[inline(always)]
fn const_f64<F: Float + scirs2_core::numeric::FromPrimitive>(value: f64) -> F {
F::from(value).expect("Failed to convert constant to target float type")
}
pub fn var_historical<F: Float + Clone>(returns: &Array1<F>, confidence: f64) -> Result<F> {
if returns.is_empty() {
return Err(TimeSeriesError::InvalidInput(
"Returns cannot be empty".to_string(),
));
}
if confidence <= 0.0 || confidence >= 1.0 {
return Err(TimeSeriesError::InvalidParameter {
name: "confidence".to_string(),
message: "Confidence must be between 0 and 1".to_string(),
});
}
let mut sorted_returns = returns.to_vec();
sorted_returns.sort_by(|a, b| a.partial_cmp(b).expect("Float comparison failed"));
let index = ((1.0 - confidence) * sorted_returns.len() as f64) as usize;
let index = index.min(sorted_returns.len() - 1);
Ok(-sorted_returns[index])
}
pub fn expected_shortfall<F: Float + Clone + std::iter::Sum>(
returns: &Array1<F>,
confidence: f64,
) -> Result<F> {
let var = var_historical(returns, confidence)?;
let tail_returns: Vec<F> = returns.iter().filter(|&&x| x <= -var).cloned().collect();
if tail_returns.is_empty() {
return Ok(var);
}
let sum = tail_returns.iter().fold(F::zero(), |acc, &x| acc + x);
Ok(-(sum / F::from(tail_returns.len()).expect("Failed to convert length to float")))
}
pub fn parametric_var<F: Float + Clone + scirs2_core::numeric::FromPrimitive>(
returns: &Array1<F>,
confidence_level: F,
) -> Result<F> {
if returns.is_empty() {
return Err(TimeSeriesError::InvalidInput(
"Returns array cannot be empty".to_string(),
));
}
let n = returns.len();
let mean = returns.sum() / F::from(n).expect("Failed to convert to float");
let variance = returns
.iter()
.map(|&r| (r - mean).powi(2))
.fold(F::zero(), |acc, x| acc + x)
/ F::from(n - 1).expect("Failed to convert to float");
let std_dev = variance.sqrt();
let alpha = F::one() - confidence_level;
let z_score = normal_inverse_cdf(alpha);
let var = -(mean + z_score * std_dev);
Ok(var)
}
pub fn monte_carlo_var<F: Float + Clone>(
returns: &Array1<F>,
confidence_level: F,
num_simulations: usize,
) -> Result<F> {
if returns.is_empty() {
return Err(TimeSeriesError::InvalidInput(
"Returns array cannot be empty".to_string(),
));
}
let n = returns.len();
let mean = returns.sum() / F::from(n).expect("Failed to convert to float");
let variance = returns
.iter()
.map(|&r| (r - mean).powi(2))
.fold(F::zero(), |acc, x| acc + x)
/ F::from(n - 1).expect("Failed to convert to float");
let std_dev = variance.sqrt();
let mut simulated_returns = Vec::with_capacity(num_simulations);
let mut seed = 12345u64;
for _ in 0..num_simulations {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
let u1 = (seed as f64) / (u64::MAX as f64);
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
let u2 = (seed as f64) / (u64::MAX as f64);
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
let simulated_return = mean + std_dev * F::from(z).expect("Failed to convert to float");
simulated_returns.push(simulated_return);
}
simulated_returns.sort_by(|a, b| a.partial_cmp(b).expect("Float comparison failed"));
let var_index = ((F::one() - confidence_level)
* F::from(num_simulations).expect("Failed to convert to float"))
.to_usize()
.expect("Test/example failed");
let var = if var_index < simulated_returns.len() {
-simulated_returns[var_index]
} else {
-simulated_returns[0]
};
Ok(var)
}
pub fn sharpe_ratio<F: Float + Clone>(
returns: &Array1<F>,
risk_free_rate: F,
periods_per_year: usize,
) -> Result<F> {
if returns.is_empty() {
return Err(TimeSeriesError::InvalidInput(
"Returns cannot be empty".to_string(),
));
}
let annualized_rf =
risk_free_rate / F::from(periods_per_year).expect("Failed to convert to float");
let excess_returns: Array1<F> = returns.mapv(|r| r - annualized_rf);
let mean_excess = excess_returns.sum() / F::from(returns.len()).expect("Test/example failed");
let variance = excess_returns.mapv(|r| (r - mean_excess).powi(2)).sum()
/ F::from(returns.len() - 1).expect("Test/example failed");
let std_dev = variance.sqrt();
if std_dev == F::zero() {
Ok(F::infinity())
} else {
let annualized_excess =
mean_excess * F::from(periods_per_year).expect("Failed to convert to float");
let annualized_std = std_dev
* F::from(periods_per_year)
.expect("Failed to convert to float")
.sqrt();
Ok(annualized_excess / annualized_std)
}
}
pub fn sortino_ratio<F: Float + Clone>(
returns: &Array1<F>,
risk_free_rate: F,
periods_per_year: usize,
) -> Result<F> {
if returns.is_empty() {
return Err(TimeSeriesError::InvalidInput(
"Returns cannot be empty".to_string(),
));
}
let annualized_rf =
risk_free_rate / F::from(periods_per_year).expect("Failed to convert to float");
let excess_returns: Array1<F> = returns.mapv(|r| r - annualized_rf);
let mean_excess = excess_returns.sum() / F::from(returns.len()).expect("Test/example failed");
let downside_returns: Vec<F> = excess_returns
.iter()
.filter(|&&r| r < F::zero())
.cloned()
.collect();
if downside_returns.is_empty() {
return Ok(F::infinity());
}
let downside_variance = downside_returns
.iter()
.map(|&r| r.powi(2))
.fold(F::zero(), |acc, x| acc + x)
/ F::from(downside_returns.len()).expect("Test/example failed");
let downside_deviation = downside_variance.sqrt();
if downside_deviation == F::zero() {
Ok(F::infinity())
} else {
let annualized_excess =
mean_excess * F::from(periods_per_year).expect("Failed to convert to float");
let annualized_downside = downside_deviation
* F::from(periods_per_year)
.expect("Failed to convert to float")
.sqrt();
Ok(annualized_excess / annualized_downside)
}
}
pub fn information_ratio<F: Float + Clone>(
portfolio_returns: &Array1<F>,
benchmark_returns: &Array1<F>,
) -> Result<F> {
if portfolio_returns.len() != benchmark_returns.len() {
return Err(TimeSeriesError::DimensionMismatch {
expected: portfolio_returns.len(),
actual: benchmark_returns.len(),
});
}
if portfolio_returns.is_empty() {
return Err(TimeSeriesError::InvalidInput(
"Returns cannot be empty".to_string(),
));
}
let active_returns: Array1<F> = portfolio_returns
.iter()
.zip(benchmark_returns.iter())
.map(|(&p, &b)| p - b)
.collect();
let mean_active =
active_returns.sum() / F::from(active_returns.len()).expect("Test/example failed");
let variance = active_returns.mapv(|r| (r - mean_active).powi(2)).sum()
/ F::from(active_returns.len() - 1).expect("Test/example failed");
let tracking_error = variance.sqrt();
if tracking_error == F::zero() {
Ok(F::infinity())
} else {
Ok(mean_active / tracking_error)
}
}
pub fn beta<F: Float + Clone>(asset_returns: &Array1<F>, market_returns: &Array1<F>) -> Result<F> {
if asset_returns.len() != market_returns.len() {
return Err(TimeSeriesError::DimensionMismatch {
expected: asset_returns.len(),
actual: market_returns.len(),
});
}
if asset_returns.len() < 2 {
return Err(TimeSeriesError::InsufficientData {
message: "Need at least 2 observations for beta calculation".to_string(),
required: 2,
actual: asset_returns.len(),
});
}
let asset_mean =
asset_returns.sum() / F::from(asset_returns.len()).expect("Test/example failed");
let market_mean =
market_returns.sum() / F::from(market_returns.len()).expect("Test/example failed");
let mut covariance = F::zero();
let mut market_variance = F::zero();
for i in 0..asset_returns.len() {
let asset_dev = asset_returns[i] - asset_mean;
let market_dev = market_returns[i] - market_mean;
covariance = covariance + asset_dev * market_dev;
market_variance = market_variance + market_dev.powi(2);
}
let n = F::from(asset_returns.len() - 1).expect("Test/example failed");
covariance = covariance / n;
market_variance = market_variance / n;
if market_variance == F::zero() {
Err(TimeSeriesError::InvalidInput(
"Market returns have zero variance".to_string(),
))
} else {
Ok(covariance / market_variance)
}
}
pub fn treynor_ratio<F: Float + Clone>(
returns: &Array1<F>,
market_returns: &Array1<F>,
risk_free_rate: F,
periods_per_year: usize,
) -> Result<F> {
let portfolio_beta = beta(returns, market_returns)?;
if portfolio_beta == F::zero() {
return Ok(F::infinity());
}
let annualized_rf =
risk_free_rate / F::from(periods_per_year).expect("Failed to convert to float");
let mean_return = returns.sum() / F::from(returns.len()).expect("Test/example failed");
let excess_return = mean_return - annualized_rf;
let annualized_excess =
excess_return * F::from(periods_per_year).expect("Failed to convert to float");
Ok(annualized_excess / portfolio_beta)
}
pub fn jensens_alpha<F: Float + Clone>(
returns: &Array1<F>,
market_returns: &Array1<F>,
risk_free_rate: F,
periods_per_year: usize,
) -> Result<F> {
let portfolio_beta = beta(returns, market_returns)?;
let annualized_rf =
risk_free_rate / F::from(periods_per_year).expect("Failed to convert to float");
let mean_portfolio = returns.sum() / F::from(returns.len()).expect("Test/example failed");
let mean_market =
market_returns.sum() / F::from(market_returns.len()).expect("Test/example failed");
let portfolio_excess = mean_portfolio - annualized_rf;
let market_excess = mean_market - annualized_rf;
let expected_excess = portfolio_beta * market_excess;
Ok((portfolio_excess - expected_excess)
* F::from(periods_per_year).expect("Failed to convert to float"))
}
pub fn omega_ratio<F: Float + Clone>(returns: &Array1<F>, threshold: F) -> Result<F> {
if returns.is_empty() {
return Err(TimeSeriesError::InvalidInput(
"Returns cannot be empty".to_string(),
));
}
let mut gains = F::zero();
let mut losses = F::zero();
for &ret in returns.iter() {
let excess = ret - threshold;
if excess > F::zero() {
gains = gains + excess;
} else {
losses = losses - excess; }
}
if losses == F::zero() {
Ok(F::infinity())
} else {
Ok(gains / losses)
}
}
fn normal_inverse_cdf<F: Float + scirs2_core::numeric::FromPrimitive>(p: F) -> F {
let a0 = const_f64::<F>(2.515517);
let a1 = const_f64::<F>(0.802853);
let a2 = const_f64::<F>(0.010328);
let b1 = const_f64::<F>(1.432788);
let b2 = const_f64::<F>(0.189269);
let b3 = const_f64::<F>(0.001308);
let t = if p < const_f64::<F>(0.5) {
(-const_f64::<F>(2.0) * (p.ln())).sqrt()
} else {
(-const_f64::<F>(2.0) * ((F::one() - p).ln())).sqrt()
};
let numerator = a0 + a1 * t + a2 * t.powi(2);
let denominator = F::one() + b1 * t + b2 * t.powi(2) + b3 * t.powi(3);
let z = t - numerator / denominator;
if p < const_f64::<F>(0.5) {
-z
} else {
z
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::arr1;
#[test]
fn test_var_historical() {
let returns = arr1(&[-0.05, 0.02, -0.03, 0.01, -0.02, 0.03, -0.01, 0.04]);
let var = var_historical(&returns, 0.95).expect("Test/example failed");
assert!(var > 0.0);
assert!(var < 0.1);
}
#[test]
fn test_expected_shortfall() {
let returns = arr1(&[-0.05, 0.02, -0.03, 0.01, -0.02, 0.03, -0.01, 0.04]);
let var = var_historical(&returns, 0.95).expect("Test/example failed");
let es = expected_shortfall(&returns, 0.95).expect("Test/example failed");
assert!(es >= var);
assert!(es > 0.0);
}
#[test]
fn test_parametric_var() {
let returns = arr1(&[0.01, -0.02, 0.015, -0.01, 0.005]);
let var = parametric_var(&returns, 0.95).expect("Test/example failed");
assert!(var > 0.0);
}
#[test]
fn test_sharpe_ratio() {
let returns = arr1(&[0.01, -0.02, 0.015, -0.008, 0.012, 0.005, -0.003]);
let result = sharpe_ratio(&returns, 0.02, 252);
assert!(result.is_ok());
let sharpe = result.expect("Test/example failed");
assert!(sharpe.is_finite());
}
#[test]
fn test_sortino_ratio() {
let returns = arr1(&[0.01, -0.02, 0.015, -0.008, 0.012, 0.005, -0.003]);
let result = sortino_ratio(&returns, 0.02, 252);
assert!(result.is_ok());
let sortino = result.expect("Test/example failed");
assert!(sortino.is_finite() || sortino.is_infinite());
}
#[test]
fn test_beta() {
let asset_returns = arr1(&[0.01, -0.02, 0.015, -0.01, 0.005]);
let market_returns = arr1(&[0.008, -0.015, 0.012, -0.008, 0.004]);
let result = beta(&asset_returns, &market_returns);
assert!(result.is_ok());
let beta_coef = result.expect("Test/example failed");
assert!(beta_coef.abs() < 5.0);
}
#[test]
fn test_information_ratio() {
let portfolio_returns = arr1(&[0.01, -0.02, 0.015, -0.01, 0.005]);
let benchmark_returns = arr1(&[0.008, -0.018, 0.012, -0.009, 0.003]);
let result = information_ratio(&portfolio_returns, &benchmark_returns);
assert!(result.is_ok());
let ir = result.expect("Test/example failed");
assert!(ir.is_finite() || ir.is_infinite());
}
#[test]
fn test_jensens_alpha() {
let returns = arr1(&[0.01, -0.02, 0.015, -0.01, 0.005]);
let market_returns = arr1(&[0.008, -0.015, 0.012, -0.008, 0.004]);
let result = jensens_alpha(&returns, &market_returns, 0.02, 252);
assert!(result.is_ok());
let alpha = result.expect("Test/example failed");
assert!(alpha.is_finite());
}
#[test]
fn test_omega_ratio() {
let returns = arr1(&[0.01, -0.02, 0.015, -0.008, 0.012, 0.005, -0.003]);
let threshold = 0.0;
let result = omega_ratio(&returns, threshold);
assert!(result.is_ok());
let omega = result.expect("Test/example failed");
assert!(omega > 0.0);
}
#[test]
fn test_dimension_mismatch() {
let returns1 = arr1(&[0.01, -0.02]);
let returns2 = arr1(&[0.008, -0.015, 0.012]);
let result = beta(&returns1, &returns2);
assert!(result.is_err());
}
#[test]
fn test_insufficient_data() {
let returns = arr1(&[0.01]);
let market_returns = arr1(&[0.008]);
let result = beta(&returns, &market_returns);
assert!(result.is_err());
}
#[test]
fn test_invalid_confidence() {
let returns = arr1(&[0.01, -0.02, 0.015]);
let result = var_historical(&returns, 1.1);
assert!(result.is_err());
let result = var_historical(&returns, 0.0);
assert!(result.is_err());
}
}