对齐次日交易信号基线与基准收益起点

This commit is contained in:
boris
2026-09-09 07:43:29 +08:00
parent 2811886a52
commit e66460c4e9
2 changed files with 26 additions and 8 deletions
+14 -1
View File
@@ -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,
+12 -7
View File
@@ -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,