diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index ca2b5a6..9348d50 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -1285,17 +1285,21 @@ where || self.quote_has_executable_liquidity(quote, side, matching_type)) }); }; - quotes + let latest = quotes .iter() .filter(|quote| { quote.timestamp <= cursor && self .select_quote_reference_price(snapshot, quote, side, matching_type) .is_some() - && (!require_executable_liquidity - || self.quote_has_executable_liquidity(quote, side, matching_type)) }) - .max_by_key(|quote| quote.timestamp) + .max_by_key(|quote| quote.timestamp)?; + if require_executable_liquidity + && !self.quote_has_executable_liquidity(latest, side, matching_type) + { + return None; + } + Some(latest) } fn process_limit_shares( @@ -6171,6 +6175,45 @@ mod tests { assert_eq!(fill.legs[0].price, 10.0); } + #[test] + fn minute_last_exact_time_does_not_fallback_to_older_liquid_quote() { + let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date"); + let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks) + .with_matching_type(MatchingType::MinuteLast) + .with_volume_limit(true) + .with_volume_percent(0.25) + .with_liquidity_limit(true); + let mut quote_0931 = limit_test_quote(10.0, 9.99, 10.01); + quote_0931.timestamp = date.and_hms_opt(9, 31, 0).expect("valid timestamp"); + quote_0931.volume_delta = 10_000; + quote_0931.ask1_volume = 10_000; + quote_0931.bid1_volume = 10_000; + let mut quote_1015 = limit_test_quote(10.2, 10.19, 10.21); + quote_1015.timestamp = date.and_hms_opt(10, 15, 0).expect("valid timestamp"); + quote_1015.volume_delta = 0; + quote_1015.ask1_volume = 10_000; + quote_1015.bid1_volume = 10_000; + + let fill = broker.select_execution_fill( + &limit_test_snapshot(), + &[quote_0931, quote_1015], + OrderSide::Sell, + MatchingType::MinuteLast, + Some(date.and_hms_opt(10, 15, 0).expect("valid timestamp")), + Some(date.and_hms_opt(10, 15, 0).expect("valid timestamp")), + 1_000, + 100, + 100, + 100, + false, + None, + None, + None, + ); + + assert!(fill.is_none(), "{fill:?}"); + } + #[test] fn minute_last_volume_limit_rejects_sub_lot_quote_volume() { let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");