保留手工逐日进度并在交付校验失败时终止

This commit is contained in:
boris
2026-09-14 17:30:00 +08:00
parent f8955bfb18
commit 232e9ae154
2 changed files with 74 additions and 6 deletions
+71 -6
View File
@@ -438,8 +438,10 @@ pub struct BacktestDayProgress {
pub total_return: f64,
pub benchmark_close: f64,
pub daily_fill_count: usize,
pub daily_manual_fill_count: usize,
pub daily_order_count: usize,
pub cumulative_trade_count: usize,
pub cumulative_manual_fill_count: usize,
pub holding_count: usize,
pub notes: String,
pub diagnostics: String,
@@ -628,9 +630,19 @@ impl<S, C, R> BacktestEngine<S, C, R> {
}
pub fn with_observed_manual_executions(
mut self,
self,
replay: crate::manual_execution::ManualExecutionReplay,
) -> Result<Self, BacktestError>
where
S: Strategy,
{
self.with_observed_manual_execution_source(std::sync::Arc::new(replay))
}
pub fn with_observed_manual_execution_source(
mut self,
replay: std::sync::Arc<crate::manual_execution::ManualExecutionReplay>,
) -> Result<Self, BacktestError>
where
S: Strategy,
{
@@ -639,7 +651,7 @@ impl<S, C, R> BacktestEngine<S, C, R> {
&replay.position_exposure_events,
&replay.legacy_position_exposure_bps,
)?;
self.manual_execution_source = Some(std::sync::Arc::new(replay));
self.manual_execution_source = Some(replay);
Ok(self)
}
@@ -2686,6 +2698,21 @@ where
) -> Result<BacktestResult, BacktestError>
where
F: FnMut(&BacktestDayProgress),
{
self.run_with_fallible_progress_options(include_progress_details, include_progress_diagnostics, |event| {
on_progress(event);
Ok(())
})
}
pub fn run_with_fallible_progress_options<F>(
&mut self,
include_progress_details: bool,
include_progress_diagnostics: bool,
mut on_progress: F,
) -> Result<BacktestResult, BacktestError>
where
F: FnMut(&BacktestDayProgress) -> Result<(), BacktestError>,
{
let mut portfolio = PortfolioState::new(self.config.initial_cash);
let mut manual_cursor = self.manual_execution_source.as_ref().map(|source|
@@ -2902,8 +2929,10 @@ where
total_return: latest.unit_nav - 1.0,
benchmark_close: latest.benchmark_close,
daily_fill_count,
daily_manual_fill_count: result.manual_executions.len() - day_manual_start,
daily_order_count,
cumulative_trade_count: result.fills.len() + result.manual_executions.len(),
cumulative_manual_fill_count: result.manual_executions.len(),
holding_count,
notes: include_progress_diagnostics
.then(|| latest.notes.clone())
@@ -2917,14 +2946,14 @@ where
fills: include_progress_details
.then(|| result.fills[day_fill_start..].to_vec())
.unwrap_or_default(),
manual_executions: include_progress_details.then(|| result.manual_executions[day_manual_start..].to_vec()).unwrap_or_default(),
manual_executions: result.manual_executions[day_manual_start..].to_vec(),
holdings: include_progress_details
.then(|| result.daily_holdings[holding_start..].to_vec())
.unwrap_or_default(),
process_events: include_progress_details
.then(|| result.process_events[progress_process_start..].to_vec())
.unwrap_or_default(),
});
})?;
stock_equity_by_date.insert(execution_date, portfolio.total_equity());
continue;
};
@@ -3943,8 +3972,10 @@ where
total_return: latest.unit_nav - 1.0,
benchmark_close: latest.benchmark_close,
daily_fill_count,
daily_manual_fill_count: result.manual_executions.len() - day_manual_start,
daily_order_count,
cumulative_trade_count: result.fills.len() + result.manual_executions.len(),
cumulative_manual_fill_count: result.manual_executions.len(),
holding_count,
notes: include_progress_diagnostics
.then(|| latest.notes.clone())
@@ -3958,14 +3989,14 @@ where
fills: include_progress_details
.then(|| result.fills[day_fill_start..].to_vec())
.unwrap_or_default(),
manual_executions: include_progress_details.then(|| result.manual_executions[day_manual_start..].to_vec()).unwrap_or_default(),
manual_executions: result.manual_executions[day_manual_start..].to_vec(),
holdings: include_progress_details
.then(|| result.daily_holdings[holding_start..].to_vec())
.unwrap_or_default(),
process_events: include_progress_details
.then(|| result.process_events[progress_process_start..].to_vec())
.unwrap_or_default(),
});
})?;
stock_equity_by_date.insert(execution_date, portfolio.total_equity());
}
@@ -8076,6 +8107,40 @@ mod tests {
);
}
#[test]
fn compact_manual_progress_keeps_exact_applications_and_fallible_delivery_stops_the_run() {
struct NoOrders;
impl Strategy for NoOrders {
fn name(&self) -> &str { "manual-progress" }
fn on_day(&mut self, _: &StrategyContext<'_>) -> Result<StrategyDecision, crate::BacktestError> { Ok(Default::default()) }
fn requires_minute_callbacks(&self) -> bool { false }
}
let date = d(2026, 6, 1);
let data = clock_probe_data(date, &[]);
let broker = BrokerSimulator::new_with_execution_price(ChinaAShareCostModel::default(), ChinaEquityRuleHooks, PriceField::Open).with_matching_type(MatchingType::NextBarOpen).with_volume_limit(false).with_liquidity_limit(false);
let config = BacktestConfig { initial_cash: 10000., benchmark_code: "000852.SH".into(), start_date: Some(date), end_date: Some(date), decision_lag_trading_days: 0, execution_price_field: PriceField::Open };
let source = std::sync::Arc::new(observed_manual_replay(date, &[(10, 0)]));
let mut engine = BacktestEngine::new(data, NoOrders, broker, config).with_observed_manual_execution_source(source.clone()).unwrap();
let mut days = vec![];
let result = engine.run_with_progress_options(false, false, |event| days.push(event.clone())).unwrap();
assert_eq!(days.len(), 1);
assert_eq!(days[0].daily_manual_fill_count, 1);
assert_eq!(days[0].cumulative_manual_fill_count, 1);
assert_eq!(days[0].manual_executions, result.manual_executions);
assert!(days[0].fills.is_empty() && days[0].orders.is_empty());
assert!(std::sync::Arc::ptr_eq(result.manual_execution_source.as_ref().unwrap(), &source));
for lag in [0, 1] {
let mut engine = engine_with_matching(MatchingType::CurrentBarClose, PriceField::Close, lag);
let mut delivered = 0;
let result = engine.run_with_fallible_progress_options(false, false, |_| {
delivered += 1;
Err(crate::BacktestError::Execution("progress source binding failed".into()))
});
assert!(result.unwrap_err().to_string().contains("progress source binding failed"));
assert_eq!(delivered, 1);
}
}
#[test]
fn compact_progress_keeps_counts_without_event_payload_clones() {
let mut engine = engine_with_matching(MatchingType::CurrentBarClose, PriceField::Close, 0);