From 98732bbc465bdc6ccf5e84af3c65a3abd5695fa0 Mon Sep 17 00:00:00 2001 From: boris Date: Sat, 19 Sep 2026 22:47:03 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=AC=A1=E6=97=A5=E5=BC=80?= =?UTF-8?q?=E7=9B=98=E8=A7=82=E6=B5=8B=E8=A2=AB=E6=97=A5=E7=BA=BF=E4=BB=B7?= =?UTF-8?q?=E6=A0=BC=E5=88=86=E6=94=AF=E8=B7=B3=E8=BF=87=E8=A1=8C=E6=83=85?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/engine.rs | 42 +++++++++++++++++++++ docs/stock-pool-ratio-precision-20260919.md | 2 + 2 files changed, 44 insertions(+) diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index ca54890..d32b8e7 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -1298,6 +1298,7 @@ where submission_time, ); if self.broker.execution_price_field() != PriceField::Last + && !self.broker.matching_type_uses_intraday_quotes() && !decision_has_algo_execution(decision) && post_close_window.is_none() { @@ -8941,6 +8942,47 @@ mod tests { ) } + #[test] + fn next_open_observation_loads_quotes_even_when_execution_price_is_open() { + use crate::execution_capacity::VolumeCapacityMode; + let date = d(2025, 1, 3); + let signal = d(2025, 1, 2); + let open = NaiveTime::from_hms_opt(9, 30, 0).unwrap(); + for mode in [VolumeCapacityMode::ExecutionObservation, VolumeCapacityMode::SessionCapacityAudit] { + let mut broker = BrokerSimulator::new_with_execution_price( + ChinaAShareCostModel::default(), ChinaEquityRuleHooks, PriceField::Open, + ).with_matching_type(MatchingType::NextBarOpen) + .with_volume_limit(true).with_volume_capacity_mode(mode).with_liquidity_limit(false); + if mode == VolumeCapacityMode::ExecutionObservation { + broker = broker.with_intraday_execution_start_time(open); + } + let calls = Arc::new(Mutex::new(Vec::new())); + let captured = calls.clone(); + let mut engine = BacktestEngine::new(dataset(), BuyWhenDecisionDateStrategy { decision_date: signal }, broker, + BacktestConfig { initial_cash: 100_000., benchmark_code: "000852.SH".into(), + start_date: Some(signal), end_date: Some(date), decision_lag_trading_days: 1, + execution_price_field: PriceField::Open }) + .with_execution_quote_loader(move |request| { + captured.lock().unwrap().push(request.clone()); + Ok(clock_probe_data(request.date, &[(9,30,10.)]).snapshot_components().execution_quotes) + }); + let decision = StrategyDecision { order_intents: vec![OrderIntent::Shares { + symbol: SYMBOL.into(), quantity: 100, reason: "next-open-loader-regression".into(), + }], ..Default::default() }; + engine.ensure_execution_quotes_for_decision(date, signal, &PortfolioState::new(100_000.), &[], &decision, None, None).unwrap(); + let calls = calls.lock().unwrap(); + if mode == VolumeCapacityMode::ExecutionObservation { + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].date, date); + assert_eq!(calls[0].start_time, Some(open)); + assert_eq!(calls[0].symbols, BTreeSet::from([SYMBOL.to_string()])); + assert_eq!(engine.data.execution_quotes_on(date, SYMBOL).len(), 1); + } else { + assert!(calls.is_empty(), "daily audit must not silently become an opening-liquidity model"); + } + } + } + #[test] fn full_minute_coverage_rejects_missing_active_bars_but_allows_paused_or_zero_volume() { let first = d(2025, 1, 2); diff --git a/docs/stock-pool-ratio-precision-20260919.md b/docs/stock-pool-ratio-precision-20260919.md index 0a9d70c..fbc768c 100644 --- a/docs/stock-pool-ratio-precision-20260919.md +++ b/docs/stock-pool-ratio-precision-20260919.md @@ -9,3 +9,5 @@ 执行状态新增 `last_target_weight_ratios`,首次记录精确比例升级schema2。旧schema1可读但不得携带新比例字段;旧消费者应拒绝新状态,回滚不能删除或降精度重写状态。回测、Paper、Live及Strategy Runtime都必须共同消费该比例,ETF顺延目标也携带相同比例。 新增临界100股补仓、调序不改变等权金额、停牌/候补、显式部分预算、状态序列化回读回归。当前为候选:本机Rust语法检查通过,类型/运行测试受Xcode许可阻断,转177验证;未通过Linux测试前不得发布。 + +后续真实Source回放补充:原始报价门禁打开后,Engine仍因PriceField::Open提前跳过行情加载;不能把此错误标成原始数据缺失。补充按实际撮合是否需要盘中观测判断加载路径,测试验证NextBarOpen加载执行日09:30报价、日终审计保持不加载。实际发布、回放及缺数清单以工作区`docs/fidc/stock-pool-precision-correction-20260919.md`为准。