修正回测出入金现金流中性口径
This commit is contained in:
@@ -78,6 +78,13 @@ pub struct DailyEquityPoint {
|
||||
pub cash: f64,
|
||||
pub market_value: f64,
|
||||
pub total_equity: f64,
|
||||
/// External cash flow settled on this trading date (deposit positive,
|
||||
/// withdrawal negative). Trading cash movements are excluded.
|
||||
#[serde(default)]
|
||||
pub external_cash_flow: f64,
|
||||
/// Cash-flow-neutral unit NAV after all activity on this date.
|
||||
#[serde(default)]
|
||||
pub unit_nav: f64,
|
||||
pub benchmark_close: f64,
|
||||
pub benchmark_prev_close: f64,
|
||||
pub notes: String,
|
||||
@@ -230,27 +237,34 @@ impl BacktestResult {
|
||||
|
||||
pub fn analyzer_monthly_returns(&self) -> Vec<AnalyzerMonthlyReturnRow> {
|
||||
let mut month_points = BTreeMap::<(i32, u32), (f64, f64, f64, f64)>::new();
|
||||
let mut previous_equity = self.metrics.initial_cash;
|
||||
let mut previous_equity = 1.0;
|
||||
let mut previous_benchmark = self
|
||||
.equity_curve
|
||||
.first()
|
||||
.map(|point| point.benchmark_prev_close)
|
||||
.unwrap_or_default();
|
||||
for point in &self.equity_curve {
|
||||
let point_nav = if point.unit_nav.is_finite() && point.unit_nav > 0.0 {
|
||||
point.unit_nav
|
||||
} else if self.metrics.initial_cash.abs() > f64::EPSILON {
|
||||
point.total_equity / self.metrics.initial_cash
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let key = (point.date.year(), point.date.month());
|
||||
month_points
|
||||
.entry(key)
|
||||
.and_modify(|(_, _, end_equity, end_benchmark)| {
|
||||
*end_equity = point.total_equity;
|
||||
*end_equity = point_nav;
|
||||
*end_benchmark = point.benchmark_close;
|
||||
})
|
||||
.or_insert((
|
||||
previous_equity,
|
||||
previous_benchmark,
|
||||
point.total_equity,
|
||||
point_nav,
|
||||
point.benchmark_close,
|
||||
));
|
||||
previous_equity = point.total_equity;
|
||||
previous_equity = point_nav;
|
||||
previous_benchmark = point.benchmark_close;
|
||||
}
|
||||
month_points
|
||||
@@ -299,6 +313,8 @@ pub struct BacktestDayProgress {
|
||||
pub cash: f64,
|
||||
pub market_value: f64,
|
||||
pub total_equity: f64,
|
||||
#[serde(default)]
|
||||
pub external_cash_flow: f64,
|
||||
pub unit_nav: f64,
|
||||
pub total_return: f64,
|
||||
pub benchmark_close: f64,
|
||||
@@ -1729,6 +1745,7 @@ where
|
||||
metrics: BacktestMetrics::default(),
|
||||
};
|
||||
let mut stock_equity_by_date = BTreeMap::<NaiveDate, f64>::new();
|
||||
let mut previous_external_cash_flow_total = portfolio.external_cash_flow_total();
|
||||
|
||||
for (execution_idx, execution_date) in execution_dates.iter().copied().enumerate() {
|
||||
let mut corporate_action_notes = Vec::new();
|
||||
@@ -1740,7 +1757,7 @@ where
|
||||
execution_date,
|
||||
&mut portfolio,
|
||||
&mut corporate_action_notes,
|
||||
);
|
||||
)?;
|
||||
self.extend_result(
|
||||
&mut result,
|
||||
pending_cash_flow_report,
|
||||
@@ -1829,16 +1846,21 @@ where
|
||||
.join(" | ");
|
||||
let holdings_for_day = portfolio.holdings_summary(execution_date);
|
||||
let day_process_events = process_events.clone();
|
||||
let aggregate_initial_cash = self.aggregate_initial_cash();
|
||||
let aggregate_cash = self.aggregate_cash(&portfolio);
|
||||
let aggregate_market_value = self.aggregate_market_value(&portfolio);
|
||||
let aggregate_total_equity = self.aggregate_total_equity(&portfolio);
|
||||
let unit_nav = portfolio.unit_net_value();
|
||||
let external_cash_flow =
|
||||
portfolio.external_cash_flow_total() - previous_external_cash_flow_total;
|
||||
previous_external_cash_flow_total = portfolio.external_cash_flow_total();
|
||||
|
||||
result.equity_curve.push(DailyEquityPoint {
|
||||
date: execution_date,
|
||||
cash: aggregate_cash,
|
||||
market_value: aggregate_market_value,
|
||||
total_equity: aggregate_total_equity,
|
||||
external_cash_flow,
|
||||
unit_nav,
|
||||
benchmark_close: benchmark.close,
|
||||
benchmark_prev_close: benchmark.prev_close,
|
||||
notes,
|
||||
@@ -1854,16 +1876,9 @@ where
|
||||
cash: latest.cash,
|
||||
market_value: latest.market_value,
|
||||
total_equity: latest.total_equity,
|
||||
unit_nav: if aggregate_initial_cash.abs() < f64::EPSILON {
|
||||
0.0
|
||||
} else {
|
||||
latest.total_equity / aggregate_initial_cash
|
||||
},
|
||||
total_return: if aggregate_initial_cash.abs() < f64::EPSILON {
|
||||
0.0
|
||||
} else {
|
||||
(latest.total_equity / aggregate_initial_cash) - 1.0
|
||||
},
|
||||
external_cash_flow: latest.external_cash_flow,
|
||||
unit_nav: latest.unit_nav,
|
||||
total_return: latest.unit_nav - 1.0,
|
||||
benchmark_close: latest.benchmark_close,
|
||||
daily_fill_count,
|
||||
cumulative_trade_count: result.fills.len(),
|
||||
@@ -2851,16 +2866,21 @@ where
|
||||
.join(" | ");
|
||||
let holdings_for_day = portfolio.holdings_summary(execution_date);
|
||||
let day_process_events = process_events.clone();
|
||||
let aggregate_initial_cash = self.aggregate_initial_cash();
|
||||
let aggregate_cash = self.aggregate_cash(&portfolio);
|
||||
let aggregate_market_value = self.aggregate_market_value(&portfolio);
|
||||
let aggregate_total_equity = self.aggregate_total_equity(&portfolio);
|
||||
let unit_nav = portfolio.unit_net_value();
|
||||
let external_cash_flow =
|
||||
portfolio.external_cash_flow_total() - previous_external_cash_flow_total;
|
||||
previous_external_cash_flow_total = portfolio.external_cash_flow_total();
|
||||
|
||||
result.equity_curve.push(DailyEquityPoint {
|
||||
date: execution_date,
|
||||
cash: aggregate_cash,
|
||||
market_value: aggregate_market_value,
|
||||
total_equity: aggregate_total_equity,
|
||||
external_cash_flow,
|
||||
unit_nav,
|
||||
benchmark_close: benchmark.close,
|
||||
benchmark_prev_close: benchmark.prev_close,
|
||||
notes,
|
||||
@@ -2876,16 +2896,9 @@ where
|
||||
cash: latest.cash,
|
||||
market_value: latest.market_value,
|
||||
total_equity: latest.total_equity,
|
||||
unit_nav: if aggregate_initial_cash.abs() < f64::EPSILON {
|
||||
0.0
|
||||
} else {
|
||||
latest.total_equity / aggregate_initial_cash
|
||||
},
|
||||
total_return: if aggregate_initial_cash.abs() < f64::EPSILON {
|
||||
0.0
|
||||
} else {
|
||||
(latest.total_equity / aggregate_initial_cash) - 1.0
|
||||
},
|
||||
external_cash_flow: latest.external_cash_flow,
|
||||
unit_nav: latest.unit_nav,
|
||||
total_return: latest.unit_nav - 1.0,
|
||||
benchmark_close: latest.benchmark_close,
|
||||
daily_fill_count,
|
||||
cumulative_trade_count: result.fills.len(),
|
||||
@@ -3205,9 +3218,12 @@ where
|
||||
date: NaiveDate,
|
||||
portfolio: &mut PortfolioState,
|
||||
notes: &mut Vec<String>,
|
||||
) -> BrokerExecutionReport {
|
||||
) -> Result<BrokerExecutionReport, BacktestError> {
|
||||
let mut report = BrokerExecutionReport::default();
|
||||
for flow in portfolio.settle_pending_cash_flows(date) {
|
||||
for flow in portfolio
|
||||
.settle_pending_cash_flows(date)
|
||||
.map_err(BacktestError::Execution)?
|
||||
{
|
||||
let cash_before = portfolio.cash() - flow.amount;
|
||||
let note = format!(
|
||||
"deposit_withdraw_settled amount={:.2} payable_date={} reason={}",
|
||||
@@ -3222,7 +3238,7 @@ where
|
||||
note,
|
||||
});
|
||||
}
|
||||
report
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
fn settle_futures_expirations(&mut self, date: NaiveDate) -> BrokerExecutionReport {
|
||||
|
||||
Reference in New Issue
Block a user