From 24cb4805a728b7e27d545bd324eaba669a218460 Mon Sep 17 00:00:00 2001 From: boris Date: Sat, 12 Sep 2026 06:46:49 +0800 Subject: [PATCH] fix: keep decision estimates independent and use timed capacity in fill fixtures --- crates/fidc-core/src/broker.rs | 5 +- crates/fidc-core/src/engine.rs | 9 +- .../fidc-core/src/platform_expr_strategy.rs | 13 +++ .../tests/automatic_trade_protection.rs | 2 + crates/fidc-core/tests/engine_hooks.rs | 1 + crates/fidc-core/tests/explicit_order_flow.rs | 102 +++++++++--------- .../tests/stock_pool_execution_contract.rs | 10 +- 7 files changed, 89 insertions(+), 53 deletions(-) diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index 1fab28a..fe3a1eb 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -7725,7 +7725,7 @@ where } else { remaining_qty.min(available_qty) }; - if !(side == OrderSide::Sell && allow_odd_lot_sell && take_qty == remaining_qty) { + if !(side == OrderSide::Sell && allow_odd_lot_sell) { take_qty = self.round_buy_quantity(take_qty, minimum_order_quantity, order_step_size); } @@ -8397,6 +8397,7 @@ mod tests { vec![dated_limit_test_benchmark(first), dated_limit_test_benchmark(second)], ).unwrap(); let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks) + .with_volume_capacity_mode(crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit) .with_matching_type(MatchingType::CurrentBarClose); let mut portfolio = PortfolioState::new(100_000.0); broker.execute(first, &mut portfolio, &data, &next_open_buy_decision()).unwrap(); @@ -8423,6 +8424,7 @@ mod tests { let data = DataSet::from_components(vec![limit_test_instrument()], vec![limit_test_snapshot()], Vec::new(), vec![limit_test_candidate(true, true)], vec![limit_test_benchmark()]).unwrap(); let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks) + .with_volume_capacity_mode(crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit) .with_matching_type(MatchingType::CurrentBarClose); broker.upsert_open_order(test_open_order(99)); let mut decision = StrategyDecision::default(); @@ -8450,6 +8452,7 @@ mod tests { dated_limit_test_candidate(second, false, false, true, true)], vec![dated_limit_test_benchmark(first), dated_limit_test_benchmark(second)]).unwrap(); let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks) + .with_volume_capacity_mode(crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit) .with_matching_type(MatchingType::NextBarOpen); let mut portfolio = PortfolioState::new(1_000_000.0); let mut initial = StrategyDecision::default(); diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index 4f60b97..333f666 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -7028,6 +7028,7 @@ mod tests { let third = d(2025, 1, 6); let fourth = d(2025, 1, 7); let broker = scheduled_next_open_broker(FidcRiskControlConfig::default()) + .with_volume_capacity_mode(crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit) .with_volume_limit(true) .with_volume_percent(0.25); let result = run_scheduled_round_trip_next_open_with_dataset_and_broker( @@ -7055,12 +7056,13 @@ mod tests { } #[test] - fn next_bar_open_sell_volume_limit_rejects_execution_day_zero_volume() { + fn next_bar_open_session_audit_flags_zero_volume_without_rewriting_fills() { let first = d(2025, 1, 2); let second = d(2025, 1, 3); let third = d(2025, 1, 6); let fourth = d(2025, 1, 7); let broker = scheduled_next_open_broker(FidcRiskControlConfig::default()) + .with_volume_capacity_mode(crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit) .with_volume_limit(true) .with_volume_percent(0.25); let result = run_scheduled_round_trip_next_open_with_dataset_and_broker( @@ -7081,7 +7083,10 @@ mod tests { broker, ); - assert_round_trip_sell_canceled_with_reason(&result, "daily volume limit"); + assert!(result.fills.iter().any(|fill| fill.side == OrderSide::Sell && fill.date == fourth)); + assert_eq!(result.capacity_audit.audit_passed, Some(false)); + assert_eq!(result.capacity_audit.failed_symbol_sessions, 1); + assert!(result.process_events.iter().any(|event| event.kind == ProcessEventKind::SessionCapacityAudit)); } #[test] diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 06c520f..6a31080 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -3157,10 +3157,16 @@ impl PlatformExprStrategy { allow_odd_lot_sell: bool, current_fill_quantity: u32, execution_state: &ProjectedExecutionState, + future_execution: bool, ) -> Result, BacktestError> { if requested_qty == 0 { return Ok(Some(0)); } + if future_execution { + // A decision-day estimate cannot use tomorrow's liquidity to + // change the orders created today. + return Ok(Some(requested_qty)); + } let constraints = self.config.risk_config.trading_constraints; let mut max_fill = requested_qty; @@ -3333,6 +3339,7 @@ impl PlatformExprStrategy { allow_odd_lot_sell, filled_qty, execution_state, + Self::defer_projection_execution_risk(ctx, date), )? .unwrap_or(0); if available_qty == 0 { @@ -3521,6 +3528,7 @@ impl PlatformExprStrategy { sellable_qty >= current_qty, 0, execution_state, + Self::defer_projection_execution_risk(ctx, date), )?.filter(|quantity| *quantity > 0) { fill = Some(ProjectedExecutionFill { @@ -4153,6 +4161,7 @@ impl PlatformExprStrategy { false, 0, execution_state, + Self::defer_projection_execution_risk(ctx, date), )?.filter(|quantity| *quantity > 0) { fill = Some(ProjectedExecutionFill { @@ -15072,6 +15081,7 @@ mod tests { order_events:&[],fills:&[], }; let mut cfg=PlatformExprStrategyConfig::generic(); + cfg.risk_config.trading_constraints.volume_limit_enabled=false; cfg.signal_symbol=symbol.into(); cfg.rotation_enabled=false; cfg.signal_book=Some(book); @@ -15190,6 +15200,7 @@ mod tests { let rows = Arc::new(Mutex::new(Vec::new())); let strategy = Capture { inner: PlatformExprStrategy::new(config), first, rows: Arc::clone(&rows) }; let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks) + .with_volume_capacity_mode(crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit) .with_matching_type(MatchingType::CurrentBarClose); let mut engine = BacktestEngine::new(data, strategy, broker, BacktestConfig { initial_cash: 10_000.0, benchmark_code: "000852.SH".to_owned(), start_date: Some(first), @@ -18046,6 +18057,7 @@ mod tests { false, 0, &execution_state, + false, ).expect("valid volume capacity"), Some(2_500) ); @@ -18066,6 +18078,7 @@ mod tests { false, 0, &execution_state, + false, ).expect("valid remaining volume capacity"), Some(100) ); diff --git a/crates/fidc-core/tests/automatic_trade_protection.rs b/crates/fidc-core/tests/automatic_trade_protection.rs index eb85ee8..6ed5012 100644 --- a/crates/fidc-core/tests/automatic_trade_protection.rs +++ b/crates/fidc-core/tests/automatic_trade_protection.rs @@ -107,6 +107,7 @@ fn action(quantity: &str, when: &str) -> PlatformTradeAction { } fn run(policy: AutomaticTradeProtection) -> fidc_core::BacktestResult { let mut config = PlatformExprStrategyConfig::generic(); + config.volume_capacity_mode = fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit; config.signal_symbol = "000001.SZ".into(); config.benchmark_symbol = "000300.SH".into(); config.rotation_enabled = false; @@ -276,6 +277,7 @@ fn locked_holding_keeps_its_slot_even_when_cash_can_buy_the_next_candidate() { ) .unwrap(); let mut config = PlatformExprStrategyConfig::generic(); + config.volume_capacity_mode = fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit; config.signal_symbol = "000001.SZ".into(); config.benchmark_symbol = "000300.SH".into(); config.strategy_name = "protection_test".into(); diff --git a/crates/fidc-core/tests/engine_hooks.rs b/crates/fidc-core/tests/engine_hooks.rs index 07bb37c..5baf98e 100644 --- a/crates/fidc-core/tests/engine_hooks.rs +++ b/crates/fidc-core/tests/engine_hooks.rs @@ -4290,6 +4290,7 @@ fn platform_strategy_cannot_hide_missing_valuation_by_skipping_stop_take() { ) .expect("dataset"); let mut config = PlatformExprStrategyConfig::microcap_rotation(); + config.volume_capacity_mode = fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit; config.strategy_name = "missing-row-platform-risk".to_string(); config.benchmark_symbol = "000300.SH".to_string(); config.signal_symbol = "000001.SZ".to_string(); diff --git a/crates/fidc-core/tests/explicit_order_flow.rs b/crates/fidc-core/tests/explicit_order_flow.rs index 3f65dd6..2f48b3b 100644 --- a/crates/fidc-core/tests/explicit_order_flow.rs +++ b/crates/fidc-core/tests/explicit_order_flow.rs @@ -202,6 +202,7 @@ fn execute_single_value_order( ChinaEquityRuleHooks::default(), PriceField::Open, ) + .with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit) .with_strict_value_budget(true); let report = broker .execute( @@ -392,7 +393,7 @@ fn broker_executes_explicit_order_value_buy() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ); + ).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit); let report = broker .execute( @@ -682,7 +683,7 @@ fn broker_executes_order_shares_and_order_lots() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ); + ).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit); let report = broker .execute( @@ -802,7 +803,7 @@ fn broker_executes_target_shares_like_order_to() { let broker = BrokerSimulator::new( ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), - ); + ).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit); let report = broker .execute( @@ -978,7 +979,7 @@ fn broker_executes_target_portfolio_smart_with_custom_prices() { let broker = BrokerSimulator::new( ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), - ); + ).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit); let report = broker .execute( @@ -1252,7 +1253,7 @@ fn broker_executes_order_percent_and_target_percent() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ); + ).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit); let mut percent_portfolio = PortfolioState::new(1_000_000.0); let percent_report = broker @@ -1380,7 +1381,7 @@ fn broker_uses_day_open_price_for_open_auction_matching() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::DayOpen, - ) + ).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit) .with_slippage_model(SlippageModel::PriceRatio(0.05)); let report = broker @@ -1414,7 +1415,7 @@ fn broker_uses_day_open_price_for_open_auction_matching() { } #[test] -fn broker_open_auction_uses_auction_volume_without_quote_liquidity() { +fn broker_rejects_unproven_auction_capacity_in_a_daily_snapshot() { let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap(); let data = DataSet::from_components( vec![Instrument { @@ -1511,11 +1512,10 @@ fn broker_open_auction_uses_auction_volume_without_quote_liquidity() { risk_decisions: Vec::new(), }, ) - .expect("broker execution"); + .expect_err("a timestamped daily total is not proof of auction volume"); - assert_eq!(report.fill_events.len(), 1); - assert_eq!(report.fill_events[0].quantity, 200); - assert_eq!(report.fill_events[0].price, 9.8); + assert!(report.to_string().contains("execution-time capacity is missing")); + assert_eq!(portfolio.cash(), 1_000_000.0); } #[test] @@ -1710,7 +1710,7 @@ fn broker_applies_price_ratio_slippage_on_snapshot_fills() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ) + ).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit) .with_slippage_model(SlippageModel::PriceRatio(0.01)); let report = broker @@ -2337,7 +2337,7 @@ fn broker_cancels_market_order_remainder_when_intraday_quote_liquidity_exhausted } #[test] -fn broker_cancels_market_buy_when_minute_has_no_volume() { +fn broker_rejects_missing_execution_capacity_instead_of_declaring_suspension() { let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap(); let data = DataSet::from_components( vec![Instrument { @@ -2433,15 +2433,10 @@ fn broker_cancels_market_buy_when_minute_has_no_volume() { risk_decisions: Vec::new(), }, ) - .expect("broker execution"); + .expect_err("missing capacity is a contract error, not a normal no-volume cancellation"); - assert_eq!(report.fill_events.len(), 0); - assert_eq!(report.order_events.len(), 1); - assert_eq!( - report.order_events[0].status, - fidc_core::OrderStatus::Canceled - ); - assert!(report.order_events[0].reason.contains("daily no volume")); + assert!(report.to_string().contains("execution-time capacity is missing")); + assert_eq!(portfolio.cash(), 1_000_000.0); } #[test] @@ -3679,7 +3674,7 @@ fn rebalance_uses_day_open_for_open_auction_valuation() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::DayOpen, - ); + ).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit); let report = broker .execute( @@ -3864,7 +3859,7 @@ fn rebalance_optimizer_prioritizes_higher_target_weight_when_cash_is_tight() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ); + ).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit); let report = broker .execute( @@ -4049,7 +4044,7 @@ fn rebalance_optimizer_does_not_scale_targets_above_requested_weight() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ); + ).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit); broker .execute( @@ -4163,7 +4158,7 @@ fn broker_uses_board_specific_min_quantity_and_step_size_for_buy_sizing() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ) + ).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit) .with_risk_config(risk_config); let report = broker @@ -4269,7 +4264,7 @@ fn broker_allows_bjse_quantities_above_minimum_without_round_lot_step() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ) + ).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit) .with_risk_config(risk_config); let report = broker @@ -4377,7 +4372,7 @@ fn broker_allows_full_odd_lot_sell_when_liquidating_position() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ); + ).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit); let report = broker .execute( @@ -4511,7 +4506,7 @@ fn same_day_sell_then_rebuy_is_rejected_by_default() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ); + ).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit); broker .execute( @@ -4655,7 +4650,7 @@ fn same_day_sell_then_rebuy_can_be_allowed_by_policy() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ) + ).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit) .with_risk_config(risk_config); broker @@ -4708,7 +4703,7 @@ fn broker_configured_policy_can_allow_upper_limit_buy() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ) + ).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit) .with_risk_config(risk_config); let mut portfolio = PortfolioState::new(1_000_000.0); @@ -4752,7 +4747,7 @@ fn broker_configured_policy_can_allow_lower_limit_sell() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ) + ).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap()) .with_risk_config(risk_config); let mut portfolio = PortfolioState::new(1_000_000.0); portfolio @@ -4791,7 +4786,7 @@ fn broker_configured_policy_can_allow_lower_limit_sell() { fn two_day_limit_order_data(day1_open: f64, day2_open: f64) -> DataSet { let day1 = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap(); let day2 = NaiveDate::from_ymd_opt(2024, 1, 11).unwrap(); - DataSet::from_components( + DataSet::from_components_with_actions_and_quotes( vec![Instrument { symbol: "000002.SZ".to_string(), name: "Test".to_string(), @@ -4919,6 +4914,13 @@ fn two_day_limit_order_data(day1_open: f64, day2_open: f64) -> DataSet { volume: 1_000_000, }, ], + Vec::new(), + [(day1, day1_open), (day2, day2_open)].into_iter().map(|(date, price)| IntradayExecutionQuote { + date, symbol: "000002.SZ".into(), timestamp: date.and_hms_opt(9, 30, 0).unwrap(), + last_price: price, bid1: price, ask1: price, bid1_volume: 0, ask1_volume: 0, + volume_delta: 100_000, amount_delta: 100_000.0 * price, + trading_phase: Some("synthetic_observation_fixture".into()), + }).collect(), ) .expect("dataset") } @@ -4932,7 +4934,7 @@ fn broker_expires_day_limit_buy_at_market_close() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ); + ).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap()); let mut portfolio = PortfolioState::new(1_000_000.0); let day1_report = broker @@ -5006,7 +5008,7 @@ fn broker_ioc_limit_order_fills_available_quantity_and_cancels_remainder() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ) + ).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap()) .with_volume_limit(true) .with_volume_percent(0.001) .with_liquidity_limit(false); @@ -5042,7 +5044,7 @@ fn broker_ioc_limit_order_fills_available_quantity_and_cancels_remainder() { } #[test] -fn broker_persists_daily_volume_consumption_across_execute_calls() { +fn broker_persists_observed_volume_consumption_across_execute_calls() { let day1 = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap(); let day2 = NaiveDate::from_ymd_opt(2024, 1, 11).unwrap(); let data = two_day_limit_order_data(10.0, 10.0); @@ -5051,6 +5053,8 @@ fn broker_persists_daily_volume_consumption_across_execute_calls() { ChinaEquityRuleHooks::default(), PriceField::Open, ) + .with_matching_type(MatchingType::MinuteLast) + .with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap()) .with_volume_limit(true) .with_volume_percent(0.001) .with_liquidity_limit(false); @@ -5077,7 +5081,7 @@ fn broker_persists_daily_volume_consumption_across_execute_calls() { assert_eq!(second.order_events.len(), 1); assert_eq!(second.order_events[0].status, OrderStatus::Canceled); assert_eq!(second.order_events[0].filled_quantity, 0); - assert!(second.order_events[0].reason.contains("daily volume limit")); + assert!(second.order_events[0].reason.contains("intraday quote liquidity exhausted")); assert_eq!(portfolio.position("000002.SZ").unwrap().quantity, 100); let next_day = broker @@ -5294,7 +5298,7 @@ fn broker_day_market_order_cancels_remainder_without_creating_invalid_open_order ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ) + ).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap()) .with_volume_limit(true) .with_volume_percent(0.001) .with_liquidity_limit(false); @@ -5334,7 +5338,7 @@ fn broker_fok_order_is_atomic_when_liquidity_is_insufficient() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ) + ).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap()) .with_volume_limit(true) .with_volume_percent(0.001) .with_liquidity_limit(false); @@ -5421,7 +5425,7 @@ fn broker_gtc_limit_order_survives_close_and_fills_next_day() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ); + ).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap()); let mut portfolio = PortfolioState::new(1_000_000.0); let day1_report = broker @@ -5469,7 +5473,7 @@ fn broker_gtc_partial_fills_preserve_cumulative_order_and_commission_state() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ) + ).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap()) .with_volume_limit(true) .with_volume_percent(0.001) .with_liquidity_limit(false); @@ -5536,7 +5540,7 @@ fn broker_modifies_gtc_limit_order_without_changing_order_identity() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ); + ).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap()); let mut portfolio = PortfolioState::new(1_000_000.0); let created = broker @@ -5621,7 +5625,7 @@ fn broker_modifies_partially_filled_gtc_total_and_preserves_commission_state() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ) + ).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap()) .with_volume_limit(true) .with_volume_percent(0.001) .with_liquidity_limit(false); @@ -5699,7 +5703,7 @@ fn broker_rejected_modify_has_zero_side_effects() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ) + ).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap()) .with_volume_limit(true) .with_volume_percent(0.001) .with_liquidity_limit(false); @@ -5767,7 +5771,7 @@ fn broker_accepted_modify_resets_queue_priority_but_reduction_preserves_it() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ); + ).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap()); let mut portfolio = PortfolioState::new(1_000_000.0); let create = |reason: &str| StrategyDecision { order_intents: vec![ @@ -5899,7 +5903,7 @@ fn broker_uses_limit_price_slippage_for_limit_orders() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ) + ).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap()) .with_slippage_model(SlippageModel::LimitPrice); let mut portfolio = PortfolioState::new(1_000_000.0); @@ -5938,7 +5942,7 @@ fn broker_rejects_limit_buy_when_final_execution_price_reaches_upper_limit() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ) + ).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap()) .with_slippage_model(SlippageModel::LimitPrice); let mut portfolio = PortfolioState::new(1_000_000.0); @@ -5984,7 +5988,7 @@ fn broker_executes_limit_value_and_limit_percent_intents() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ); + ).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap()); let mut value_portfolio = PortfolioState::new(1_000_000.0); let value_report = broker @@ -6047,7 +6051,7 @@ fn broker_cancels_open_order_by_order_id() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ); + ).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap()); let mut portfolio = PortfolioState::new(1_000_000.0); let day1_report = broker @@ -6225,7 +6229,7 @@ fn broker_reserves_sellable_quantity_for_open_limit_sells() { ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ); + ).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit); let mut portfolio = PortfolioState::new(1_000_000.0); portfolio .position_mut("000002.SZ") diff --git a/crates/fidc-core/tests/stock_pool_execution_contract.rs b/crates/fidc-core/tests/stock_pool_execution_contract.rs index 28b5155..4d6719a 100644 --- a/crates/fidc-core/tests/stock_pool_execution_contract.rs +++ b/crates/fidc-core/tests/stock_pool_execution_contract.rs @@ -142,7 +142,13 @@ fn data_with_fund_rules( }) }) .collect(); - DataSet::from_components(instruments, market, factors, candidates, benchmarks).unwrap() + let quotes = market.iter().filter(|row| row.symbol != "000300.SH").map(|row| fidc_core::IntradayExecutionQuote { + date: row.date, symbol: row.symbol.clone(), timestamp: row.date.and_hms_opt(9, 30, 0).unwrap(), + last_price: row.open, bid1: row.open, ask1: row.open, bid1_volume: 0, ask1_volume: 0, + volume_delta: row.volume, amount_delta: row.open * row.volume as f64, + trading_phase: Some("synthetic_observation_fixture".into()), + }).collect(); + DataSet::from_components_with_actions_and_quotes(instruments, market, factors, candidates, benchmarks, vec![], quotes).unwrap() } fn broker(volume: bool) -> BrokerSimulator { let mut risk = FidcRiskControlConfig::default(); @@ -159,6 +165,7 @@ fn broker(volume: bool) -> BrokerSimulator FrozenStockPoolIntent { @@ -268,6 +275,7 @@ fn mixed_fund_and_stock_round_trip_uses_declared_ticks_and_asset_specific_fees() let mut costs = ChinaAShareCostModel::default(); costs.set_transfer_fee_rate(0.00001); let broker = BrokerSimulator::new(costs, ChinaEquityRuleHooks) + .with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit) .with_matching_type(MatchingType::NextBarOpen); let mut account = PortfolioState::new(30_000.); let mut entry = contract(day(2), 1, false);