From 3c0ced89bf874cc8c101e0001f4ead49b9b92dd0 Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 23 Apr 2026 00:14:11 -0700 Subject: [PATCH] Align open auction volume semantics --- crates/fidc-core/src/broker.rs | 2 +- crates/fidc-core/src/strategy_ai.rs | 2 +- crates/fidc-core/tests/explicit_order_flow.rs | 100 ++++++++++++++++++ 3 files changed, 102 insertions(+), 2 deletions(-) diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index 5b4405b..af14e76 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -1444,7 +1444,7 @@ where } let mut max_fill = requested_qty; - if self.liquidity_limit { + if self.liquidity_limit && !self.is_open_auction_matching() { let top_level_liquidity = match side { OrderSide::Buy => snapshot.liquidity_for_buy(), OrderSide::Sell => snapshot.liquidity_for_sell(), diff --git a/crates/fidc-core/src/strategy_ai.rs b/crates/fidc-core/src/strategy_ai.rs index 3d06264..69e0867 100644 --- a/crates/fidc-core/src/strategy_ai.rs +++ b/crates/fidc-core/src/strategy_ai.rs @@ -108,7 +108,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual { }, ManualSection { title: "execution.matching_type / execution.slippage".to_string(), - detail: "设置撮合模式和滑点。支持 execution.matching_type(\"next_tick_last\" | \"current_bar_close\" | \"next_bar_open\" | \"open_auction\"),其中 open_auction 使用当日集合竞价开盘价 day_open 进行撮合,且不额外施加滑点;滑点支持 execution.slippage(\"none\") / execution.slippage(\"price_ratio\", 0.001) / execution.slippage(\"tick_size\", 1)。".to_string(), + detail: "设置撮合模式和滑点。支持 execution.matching_type(\"next_tick_last\" | \"current_bar_close\" | \"next_bar_open\" | \"open_auction\"),其中 open_auction 使用当日集合竞价开盘价 day_open 进行撮合,且不额外施加滑点,并按竞价成交量而不是盘口一档流动性限制成交;滑点支持 execution.slippage(\"none\") / execution.slippage(\"price_ratio\", 0.001) / execution.slippage(\"tick_size\", 1)。".to_string(), }, ManualSection { title: "when / unless / else".to_string(), diff --git a/crates/fidc-core/tests/explicit_order_flow.rs b/crates/fidc-core/tests/explicit_order_flow.rs index c8ca788..62a74ab 100644 --- a/crates/fidc-core/tests/explicit_order_flow.rs +++ b/crates/fidc-core/tests/explicit_order_flow.rs @@ -210,6 +210,106 @@ fn broker_uses_day_open_price_for_open_auction_matching() { assert!((report.account_events[0].total_equity - expected_total_equity).abs() < 1e-6); } +#[test] +fn broker_open_auction_uses_auction_volume_without_quote_liquidity() { + let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap(); + let data = DataSet::from_components( + vec![Instrument { + symbol: "000002.SZ".to_string(), + name: "Test".to_string(), + board: "SZ".to_string(), + round_lot: 100, + listed_at: None, + delisted_at: None, + status: "active".to_string(), + }], + vec![DailyMarketSnapshot { + date, + symbol: "000002.SZ".to_string(), + timestamp: Some("2024-01-10 09:25:00".to_string()), + day_open: 9.8, + open: 10.0, + high: 10.1, + low: 9.7, + close: 10.0, + last_price: 10.0, + bid1: 0.0, + ask1: 0.0, + prev_close: 10.0, + volume: 1_000, + tick_volume: 1_000, + bid1_volume: 0, + ask1_volume: 0, + trading_phase: Some("open_auction".to_string()), + paused: false, + upper_limit: 11.0, + lower_limit: 9.0, + price_tick: 0.01, + }], + vec![DailyFactorSnapshot { + date, + symbol: "000002.SZ".to_string(), + market_cap_bn: 50.0, + free_float_cap_bn: 45.0, + pe_ttm: 15.0, + turnover_ratio: Some(2.0), + effective_turnover_ratio: Some(1.8), + extra_factors: BTreeMap::new(), + }], + vec![CandidateEligibility { + date, + symbol: "000002.SZ".to_string(), + is_st: false, + is_new_listing: false, + is_paused: false, + allow_buy: true, + allow_sell: true, + is_kcb: false, + is_one_yuan: false, + }], + vec![BenchmarkSnapshot { + date, + benchmark: "000300.SH".to_string(), + open: 100.0, + close: 100.0, + prev_close: 99.0, + volume: 1_000_000, + }], + ) + .expect("dataset"); + let mut portfolio = PortfolioState::new(1_000_000.0); + let broker = BrokerSimulator::new_with_execution_price( + ChinaAShareCostModel::default(), + ChinaEquityRuleHooks::default(), + PriceField::DayOpen, + ) + .with_volume_percent(0.25); + + let report = broker + .execute( + date, + &mut portfolio, + &data, + &StrategyDecision { + rebalance: false, + target_weights: BTreeMap::new(), + exit_symbols: BTreeSet::new(), + order_intents: vec![OrderIntent::Value { + symbol: "000002.SZ".to_string(), + value: 9_800.0, + reason: "open_auction_volume".to_string(), + }], + notes: Vec::new(), + diagnostics: Vec::new(), + }, + ) + .expect("broker execution"); + + assert_eq!(report.fill_events.len(), 1); + assert_eq!(report.fill_events[0].quantity, 200); + assert_eq!(report.fill_events[0].price, 9.8); +} + #[test] fn broker_applies_price_ratio_slippage_on_snapshot_fills() { let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();