补齐权威下行风险指标

This commit is contained in:
boris
2026-09-03 15:02:44 +08:00
parent a3a077fa87
commit 1a79dc483c
+22
View File
@@ -50,6 +50,7 @@ pub struct BacktestMetrics {
pub max_drawdown_duration_days: usize, pub max_drawdown_duration_days: usize,
pub total_trade_days: usize, pub total_trade_days: usize,
pub sortino: f64, pub sortino: f64,
pub downside_risk: f64,
pub information_ratio: f64, pub information_ratio: f64,
pub tracking_error: f64, pub tracking_error: f64,
pub volatility: 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); 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 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 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( let information_ratio = annualized_sharpe(
&excess_returns, &excess_returns,
&zero_risk_free_rates, &zero_risk_free_rates,
@@ -296,6 +299,7 @@ pub fn compute_backtest_metrics(
max_drawdown_duration_days, max_drawdown_duration_days,
total_trade_days, total_trade_days,
sortino, sortino,
downside_risk,
information_ratio, information_ratio,
tracking_error, tracking_error,
volatility, 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::<f64>()
/ returns.len() as f64;
downside_mean_square.sqrt() * periods_per_year.sqrt()
}
fn annualized_std(values: &[f64], periods_per_year: f64) -> f64 { fn annualized_std(values: &[f64], periods_per_year: f64) -> f64 {
std_dev(values) * periods_per_year.sqrt() std_dev(values) * periods_per_year.sqrt()
} }
@@ -863,6 +884,7 @@ mod tests {
let expected_sortino = mean(&adjusted) / downside * TRADING_DAYS_PER_YEAR.sqrt(); let expected_sortino = mean(&adjusted) / downside * TRADING_DAYS_PER_YEAR.sqrt();
assert!((metrics.sharpe - expected_sharpe).abs() < 1e-12); assert!((metrics.sharpe - expected_sharpe).abs() < 1e-12);
assert!((metrics.sortino - expected_sortino).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_source, "test");
assert_eq!(metrics.risk_free_rate_tenor, "3M"); assert_eq!(metrics.risk_free_rate_tenor, "3M");
assert_eq!(metrics.risk_free_rate_observation_count, 4); assert_eq!(metrics.risk_free_rate_observation_count, 4);