diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index 146fff2..881ddbf 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -169,6 +169,10 @@ where self.apply_slippage(snapshot, side, raw_price) } + fn is_open_auction_matching(&self) -> bool { + self.execution_price_field == PriceField::DayOpen + } + fn apply_slippage( &self, snapshot: &crate::data::DailyMarketSnapshot, @@ -179,6 +183,10 @@ where return raw_price; } + if self.is_open_auction_matching() { + return self.clamp_execution_price(snapshot, side, raw_price); + } + let adjusted = match self.slippage_model { SlippageModel::None => raw_price, SlippageModel::PriceRatio(ratio) => { @@ -882,7 +890,12 @@ where date, cash_before, cash_after: portfolio.cash(), - total_equity: self.total_equity_at(date, portfolio, data, PriceField::Open)?, + total_equity: self.total_equity_at( + date, + portfolio, + data, + self.account_mark_price_field(), + )?, note: format!("sell {symbol} {reason}"), }); Ok(()) @@ -1251,7 +1264,12 @@ where date, cash_before, cash_after: portfolio.cash(), - total_equity: self.total_equity_at(date, portfolio, data, PriceField::Open)?, + total_equity: self.total_equity_at( + date, + portfolio, + data, + self.account_mark_price_field(), + )?, note: format!("buy {symbol} {reason}"), }); Ok(()) @@ -1305,6 +1323,14 @@ where .unwrap_or(self.board_lot_size.max(1)) } + fn account_mark_price_field(&self) -> PriceField { + if self.is_open_auction_matching() { + PriceField::DayOpen + } else { + PriceField::Open + } + } + fn round_buy_quantity( &self, quantity: u32, diff --git a/crates/fidc-core/src/strategy_ai.rs b/crates/fidc-core/src/strategy_ai.rs index a373040..3d06264 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 deb5f49..41fae54 100644 --- a/crates/fidc-core/tests/explicit_order_flow.rs +++ b/crates/fidc-core/tests/explicit_order_flow.rs @@ -179,7 +179,8 @@ fn broker_uses_day_open_price_for_open_auction_matching() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::DayOpen, - ); + ) + .with_slippage_model(SlippageModel::PriceRatio(0.05)); let report = broker .execute( @@ -203,6 +204,10 @@ fn broker_uses_day_open_price_for_open_auction_matching() { assert_eq!(report.fill_events.len(), 1); assert!((report.fill_events[0].price - 9.8).abs() < 1e-9); + let position = portfolio.position("000002.SZ").expect("position"); + let expected_total_equity = portfolio.cash() + position.quantity as f64 * 9.8; + assert_eq!(report.account_events.len(), 1); + assert!((report.account_events[0].total_equity - expected_total_equity).abs() < 1e-6); } #[test]