对齐次日交易信号基线与基准收益起点
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user