From 232e9ae1546842224d7a21aa07d3c0696ece4b11 Mon Sep 17 00:00:00 2001 From: boris Date: Mon, 14 Sep 2026 17:30:00 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=9D=E7=95=99=E6=89=8B=E5=B7=A5=E9=80=90?= =?UTF-8?q?=E6=97=A5=E8=BF=9B=E5=BA=A6=E5=B9=B6=E5=9C=A8=E4=BA=A4=E4=BB=98?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E5=A4=B1=E8=B4=A5=E6=97=B6=E7=BB=88=E6=AD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/engine.rs | 77 +++++++++++++++++++++++-- docs/manual-execution-clock-20260914.md | 3 + 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index 443939d..ed5851e 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -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 BacktestEngine { } pub fn with_observed_manual_executions( - mut self, + self, replay: crate::manual_execution::ManualExecutionReplay, ) -> Result + 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, + ) -> Result where S: Strategy, { @@ -639,7 +651,7 @@ impl BacktestEngine { &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 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( + &mut self, + include_progress_details: bool, + include_progress_diagnostics: bool, + mut on_progress: F, + ) -> Result + 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 { 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); diff --git a/docs/manual-execution-clock-20260914.md b/docs/manual-execution-clock-20260914.md index a7fa0ca..26f4aae 100644 --- a/docs/manual-execution-clock-20260914.md +++ b/docs/manual-execution-clock-20260914.md @@ -46,3 +46,6 @@ Core872通过(9项原ignore不计通过),交易工作区619普通测试通 ## 2026-09-14 运行级仓位配置补充 v3 手工输入独立携带审计仓位/权重时间线与旧日级前缀,不覆盖原策略或股票池。仅已成交证券产生独立行情需求;补充范围不会成为选股候选。恢复跟随回到原规则,未来事件不能被伪称为截止时刻前已观察事实。Core 878 项本机通过,尚未部署;PG、期间隔离、权限与剩余联合验收见 `../../fidc-trading-platform/docs/shadow-manual-input-20260914.md`。本节不替代前述时钟证据,也不宣称全部矩阵完成。 +## 逐日手工交付补充 + +手工观察输入可通过Arc与进度投影共享;默认紧凑进度保留当日手工应用及独立累计计数,原生明细开关不改。新增可失败进度回调,投影来源/计数错误会终止本次回测,不忽略错误后返回成功。Core879本机通过,当前完整版本Linux及发布验收未完成;共享最终/逐日投影与真实本机WebSocket证据见Service `docs/manual-stream-projection-20260914.md`。