对齐次日交易信号基线与基准收益起点
This commit is contained in:
@@ -91,6 +91,9 @@ impl Default for ProcessEventRetention {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct DailyEquityPoint {
|
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")]
|
#[serde(with = "date_format")]
|
||||||
pub date: NaiveDate,
|
pub date: NaiveDate,
|
||||||
pub cash: f64,
|
pub cash: f64,
|
||||||
@@ -109,6 +112,14 @@ pub struct DailyEquityPoint {
|
|||||||
pub diagnostics: String,
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct BacktestResult {
|
pub struct BacktestResult {
|
||||||
pub strategy_name: String,
|
pub strategy_name: String,
|
||||||
@@ -334,7 +345,7 @@ impl BacktestResult {
|
|||||||
let mut previous_benchmark = self
|
let mut previous_benchmark = self
|
||||||
.equity_curve
|
.equity_curve
|
||||||
.first()
|
.first()
|
||||||
.map(|point| point.benchmark_prev_close)
|
.map(DailyEquityPoint::benchmark_reference_close)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
for point in &self.equity_curve {
|
for point in &self.equity_curve {
|
||||||
let point_nav = if point.unit_nav.is_finite() && point.unit_nav > 0.0 {
|
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();
|
previous_external_cash_flow_total = portfolio.external_cash_flow_total();
|
||||||
|
|
||||||
result.equity_curve.push(DailyEquityPoint {
|
result.equity_curve.push(DailyEquityPoint {
|
||||||
|
signal_baseline: true,
|
||||||
date: execution_date,
|
date: execution_date,
|
||||||
cash: aggregate_cash,
|
cash: aggregate_cash,
|
||||||
market_value: aggregate_market_value,
|
market_value: aggregate_market_value,
|
||||||
@@ -3369,6 +3381,7 @@ where
|
|||||||
previous_external_cash_flow_total = portfolio.external_cash_flow_total();
|
previous_external_cash_flow_total = portfolio.external_cash_flow_total();
|
||||||
|
|
||||||
result.equity_curve.push(DailyEquityPoint {
|
result.equity_curve.push(DailyEquityPoint {
|
||||||
|
signal_baseline: false,
|
||||||
date: execution_date,
|
date: execution_date,
|
||||||
cash: aggregate_cash,
|
cash: aggregate_cash,
|
||||||
market_value: aggregate_market_value,
|
market_value: aggregate_market_value,
|
||||||
|
|||||||
@@ -108,13 +108,7 @@ pub fn compute_backtest_metrics(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let trade_days = equity_curve.len();
|
let trade_days = equity_curve.len();
|
||||||
let benchmark_start = if first_point.benchmark_prev_close.is_finite()
|
let benchmark_start = first_point.benchmark_reference_close();
|
||||||
&& first_point.benchmark_prev_close > f64::EPSILON
|
|
||||||
{
|
|
||||||
first_point.benchmark_prev_close
|
|
||||||
} else {
|
|
||||||
first_point.benchmark_close
|
|
||||||
};
|
|
||||||
let explicit_unit_nav = equity_curve.iter().any(|point| {
|
let explicit_unit_nav = equity_curve.iter().any(|point| {
|
||||||
point.external_cash_flow.abs() > f64::EPSILON
|
point.external_cash_flow.abs() > f64::EPSILON
|
||||||
|| (point.unit_nav.is_finite()
|
|| (point.unit_nav.is_finite()
|
||||||
@@ -780,6 +774,7 @@ mod tests {
|
|||||||
benchmark_prev_close: f64,
|
benchmark_prev_close: f64,
|
||||||
) -> DailyEquityPoint {
|
) -> DailyEquityPoint {
|
||||||
DailyEquityPoint {
|
DailyEquityPoint {
|
||||||
|
signal_baseline: false,
|
||||||
date: NaiveDate::parse_from_str(date, "%Y-%m-%d").unwrap(),
|
date: NaiveDate::parse_from_str(date, "%Y-%m-%d").unwrap(),
|
||||||
cash: total_equity,
|
cash: total_equity,
|
||||||
market_value: 0.0,
|
market_value: 0.0,
|
||||||
@@ -804,11 +799,21 @@ mod tests {
|
|||||||
assert!((metrics.benchmark_cumulative_return - expected).abs() < 1e-12);
|
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]
|
#[test]
|
||||||
fn external_cash_flow_is_excluded_from_return_and_reported_separately() {
|
fn external_cash_flow_is_excluded_from_return_and_reported_separately() {
|
||||||
let curve = vec![
|
let curve = vec![
|
||||||
equity_point("2025-01-02", 100.0, 100.0, 100.0),
|
equity_point("2025-01-02", 100.0, 100.0, 100.0),
|
||||||
DailyEquityPoint {
|
DailyEquityPoint {
|
||||||
|
signal_baseline: false,
|
||||||
date: NaiveDate::from_ymd_opt(2025, 1, 3).unwrap(),
|
date: NaiveDate::from_ymd_opt(2025, 1, 3).unwrap(),
|
||||||
cash: 220.0,
|
cash: 220.0,
|
||||||
market_value: 0.0,
|
market_value: 0.0,
|
||||||
|
|||||||
Reference in New Issue
Block a user