From e66460c4e9684ec1c5d05bccd421e525283ce398 Mon Sep 17 00:00:00 2001 From: boris Date: Wed, 9 Sep 2026 07:43:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E9=BD=90=E6=AC=A1=E6=97=A5=E4=BA=A4?= =?UTF-8?q?=E6=98=93=E4=BF=A1=E5=8F=B7=E5=9F=BA=E7=BA=BF=E4=B8=8E=E5=9F=BA?= =?UTF-8?q?=E5=87=86=E6=94=B6=E7=9B=8A=E8=B5=B7=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/engine.rs | 15 ++++++++++++++- crates/fidc-core/src/metrics.rs | 19 ++++++++++++------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index 3ec4ce3..cd683f0 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -91,6 +91,9 @@ impl Default for ProcessEventRetention { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DailyEquityPoint { + /// Close-of-signal-day cash baseline before lagged trading begins. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub signal_baseline: bool, #[serde(with = "date_format")] pub date: NaiveDate, pub cash: f64, @@ -109,6 +112,14 @@ pub struct DailyEquityPoint { pub diagnostics: String, } +impl DailyEquityPoint { + pub fn benchmark_reference_close(&self) -> f64 { + if self.signal_baseline { self.benchmark_close } + else if self.benchmark_prev_close.is_finite() && self.benchmark_prev_close > f64::EPSILON { self.benchmark_prev_close } + else { self.benchmark_close } + } +} + #[derive(Debug, Clone)] pub struct BacktestResult { pub strategy_name: String, @@ -334,7 +345,7 @@ impl BacktestResult { let mut previous_benchmark = self .equity_curve .first() - .map(|point| point.benchmark_prev_close) + .map(DailyEquityPoint::benchmark_reference_close) .unwrap_or_default(); for point in &self.equity_curve { let point_nav = if point.unit_nav.is_finite() && point.unit_nav > 0.0 { @@ -2202,6 +2213,7 @@ where previous_external_cash_flow_total = portfolio.external_cash_flow_total(); result.equity_curve.push(DailyEquityPoint { + signal_baseline: true, date: execution_date, cash: aggregate_cash, market_value: aggregate_market_value, @@ -3369,6 +3381,7 @@ where previous_external_cash_flow_total = portfolio.external_cash_flow_total(); result.equity_curve.push(DailyEquityPoint { + signal_baseline: false, date: execution_date, cash: aggregate_cash, market_value: aggregate_market_value, diff --git a/crates/fidc-core/src/metrics.rs b/crates/fidc-core/src/metrics.rs index d686daf..8af7255 100644 --- a/crates/fidc-core/src/metrics.rs +++ b/crates/fidc-core/src/metrics.rs @@ -108,13 +108,7 @@ pub fn compute_backtest_metrics( }; let trade_days = equity_curve.len(); - let benchmark_start = if first_point.benchmark_prev_close.is_finite() - && first_point.benchmark_prev_close > f64::EPSILON - { - first_point.benchmark_prev_close - } else { - first_point.benchmark_close - }; + let benchmark_start = first_point.benchmark_reference_close(); let explicit_unit_nav = equity_curve.iter().any(|point| { point.external_cash_flow.abs() > f64::EPSILON || (point.unit_nav.is_finite() @@ -780,6 +774,7 @@ mod tests { benchmark_prev_close: f64, ) -> DailyEquityPoint { DailyEquityPoint { + signal_baseline: false, date: NaiveDate::parse_from_str(date, "%Y-%m-%d").unwrap(), cash: total_equity, market_value: 0.0, @@ -804,11 +799,21 @@ mod tests { assert!((metrics.benchmark_cumulative_return - expected).abs() < 1e-12); } + #[test] + fn signal_baseline_uses_same_close_for_strategy_and_benchmark() { + let mut baseline=equity_point("2026-09-04",100.0,4548.0499,4552.5784); + baseline.signal_baseline=true; + let curve=vec![baseline,equity_point("2026-09-08",104.0,4558.7371,4575.0245)]; + let metrics=compute_backtest_metrics(&curve,&[],&[],&[],100.0,None).unwrap(); + assert!((metrics.benchmark_cumulative_return-(4558.7371/4548.0499-1.0)).abs()<1e-12); + } + #[test] fn external_cash_flow_is_excluded_from_return_and_reported_separately() { let curve = vec![ equity_point("2025-01-02", 100.0, 100.0, 100.0), DailyEquityPoint { + signal_baseline: false, date: NaiveDate::from_ymd_opt(2025, 1, 3).unwrap(), cash: 220.0, market_value: 0.0,