From 1a79dc483cb70d92976e926bfcddaa10e493bfe2 Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 3 Sep 2026 15:02:44 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E6=9D=83=E5=A8=81=E4=B8=8B?= =?UTF-8?q?=E8=A1=8C=E9=A3=8E=E9=99=A9=E6=8C=87=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/metrics.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/fidc-core/src/metrics.rs b/crates/fidc-core/src/metrics.rs index ba986a2..d686daf 100644 --- a/crates/fidc-core/src/metrics.rs +++ b/crates/fidc-core/src/metrics.rs @@ -50,6 +50,7 @@ pub struct BacktestMetrics { pub max_drawdown_duration_days: usize, pub total_trade_days: usize, pub sortino: f64, + pub downside_risk: f64, pub information_ratio: f64, pub tracking_error: f64, pub volatility: f64, @@ -174,6 +175,8 @@ pub fn compute_backtest_metrics( effective_annual_risk_free_rate(&daily_risk_free_rates, TRADING_DAYS_PER_YEAR); let sharpe = annualized_sharpe(&returns, &daily_risk_free_rates, TRADING_DAYS_PER_YEAR); let sortino = annualized_sortino(&returns, &daily_risk_free_rates, TRADING_DAYS_PER_YEAR); + let downside_risk = + annualized_downside_risk(&returns, &daily_risk_free_rates, TRADING_DAYS_PER_YEAR); let information_ratio = annualized_sharpe( &excess_returns, &zero_risk_free_rates, @@ -296,6 +299,7 @@ pub fn compute_backtest_metrics( max_drawdown_duration_days, total_trade_days, sortino, + downside_risk, information_ratio, tracking_error, volatility, @@ -498,6 +502,23 @@ fn annualized_sortino( } } +fn annualized_downside_risk( + returns: &[f64], + daily_risk_free_rates: &[f64], + periods_per_year: f64, +) -> f64 { + if returns.is_empty() || returns.len() != daily_risk_free_rates.len() { + return 0.0; + } + let downside_mean_square = returns + .iter() + .zip(daily_risk_free_rates) + .map(|(value, risk_free)| (value - risk_free).min(0.0).powi(2)) + .sum::() + / returns.len() as f64; + downside_mean_square.sqrt() * periods_per_year.sqrt() +} + fn annualized_std(values: &[f64], periods_per_year: f64) -> f64 { std_dev(values) * periods_per_year.sqrt() } @@ -863,6 +884,7 @@ mod tests { let expected_sortino = mean(&adjusted) / downside * TRADING_DAYS_PER_YEAR.sqrt(); assert!((metrics.sharpe - expected_sharpe).abs() < 1e-12); assert!((metrics.sortino - expected_sortino).abs() < 1e-12); + assert!((metrics.downside_risk - downside * TRADING_DAYS_PER_YEAR.sqrt()).abs() < 1e-12); assert_eq!(metrics.risk_free_rate_source, "test"); assert_eq!(metrics.risk_free_rate_tenor, "3M"); assert_eq!(metrics.risk_free_rate_observation_count, 4);