diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index c29257f..ce1e3e8 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -5515,6 +5515,66 @@ mod tests { assert_eq!(report.order_events[0].filled_quantity, 14_300); } + #[test] + fn value_buy_rejects_when_slippage_clamps_execution_price_to_upper_limit() { + let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date"); + let broker = BrokerSimulator::new_with_execution_price( + ChinaAShareCostModel::default(), + ChinaEquityRuleHooks, + PriceField::Open, + ) + .with_slippage_model(SlippageModel::PriceRatio(0.002)) + .with_strict_value_budget(true) + .with_volume_limit(false) + .with_liquidity_limit(false) + .with_inactive_limit(false); + let mut snapshot = limit_test_snapshot(); + snapshot.day_open = 10.98; + snapshot.open = 10.98; + snapshot.last_price = 10.98; + snapshot.close = 10.98; + snapshot.bid1 = 10.98; + snapshot.ask1 = 10.98; + snapshot.upper_limit = 11.0; + let data = DataSet::from_components_with_actions_and_quotes( + vec![limit_test_instrument()], + vec![snapshot], + Vec::new(), + vec![limit_test_candidate(true, true)], + vec![limit_test_benchmark()], + Vec::new(), + Vec::new(), + ) + .expect("valid dataset"); + let mut portfolio = PortfolioState::new(10_000_000.0); + let mut report = BrokerExecutionReport::default(); + + broker + .process_value( + date, + &mut portfolio, + &data, + "000001.SZ", + 125_000.0, + "periodic_rebalance_buy", + &mut BTreeMap::new(), + &mut BTreeMap::new(), + &mut None, + &mut BTreeMap::new(), + &mut report, + ) + .expect("value buy processed"); + + assert!(portfolio.position("000001.SZ").is_none()); + assert_eq!(report.order_events.len(), 1); + assert_eq!(report.order_events[0].filled_quantity, 0); + assert!( + report.order_events[0] + .reason + .contains("open at or above upper limit") + ); + } + #[test] fn strict_value_buy_budget_includes_commission_at_execution_price() { let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");