From fe39a75e6e33c1175dc57d04dfe9081d70992e65 Mon Sep 17 00:00:00 2001 From: boris Date: Mon, 6 Jul 2026 09:54:14 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=88=86=E9=92=9F=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E4=BB=B7=E7=BC=BA=E5=A4=B1=E6=A1=A3=E4=BD=8D=E9=87=8F?= =?UTF-8?q?=E6=88=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/broker.rs | 45 ++++++++++++++++++- .../fidc-core/src/platform_expr_strategy.rs | 28 ++++++++++-- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index 0db5dd7..ca2b5a6 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -5596,7 +5596,8 @@ where if remaining_qty == 0 { break; } - let mut available_qty = if quote_quantity_limited { + let missing_level1_depth = Self::quote_lacks_level1_depth(quote); + let mut available_qty = if quote_quantity_limited && !missing_level1_depth { let top_level_liquidity = match side { OrderSide::Buy => quote.ask1_volume, OrderSide::Sell => quote.bid1_volume, @@ -5785,6 +5786,10 @@ where } } + fn quote_lacks_level1_depth(quote: &IntradayExecutionQuote) -> bool { + quote.volume_delta > 0 && quote.bid1_volume == 0 && quote.ask1_volume == 0 + } + fn matching_type_uses_intraday_quotes(&self) -> bool { matches!( self.matching_type, @@ -6128,6 +6133,44 @@ mod tests { assert_eq!(fill.quantity, 1_200); } + #[test] + fn minute_last_uses_volume_delta_when_level1_depth_missing() { + 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 = limit_test_quote(10.0, 0.0, 0.0); + quote.timestamp = date.and_hms_opt(10, 15, 0).expect("valid timestamp"); + quote.volume_delta = 600; + quote.ask1_volume = 0; + quote.bid1_volume = 0; + + let fill = broker + .select_execution_fill( + &limit_test_snapshot(), + &[quote], + OrderSide::Buy, + MatchingType::MinuteLast, + Some(date.and_hms_opt(10, 15, 0).expect("valid timestamp")), + None, + 400, + 100, + 100, + 100, + false, + None, + None, + None, + ) + .expect("bar-derived minute quote fill"); + + assert_eq!(fill.quantity, 100); + assert_eq!(fill.legs.len(), 1); + assert_eq!(fill.legs[0].price, 10.0); + } + #[test] fn minute_last_volume_limit_rejects_sub_lot_quote_volume() { let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date"); diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 1c7076f..de54a60 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -1583,10 +1583,11 @@ impl PlatformExprStrategy { let constraints = self.config.risk_config.trading_constraints; let mut max_fill = requested_qty; + let missing_level1_depth = quote.is_some_and(Self::quote_lacks_level1_depth); let enforce_top_level_liquidity = quote.is_some() && (self.config.quote_quantity_limit || constraints.liquidity_limit_enabled) || quote.is_none() && constraints.liquidity_limit_enabled; - if enforce_top_level_liquidity { + if enforce_top_level_liquidity && !missing_level1_depth { let top_level_liquidity = match (quote, side) { (Some(quote), OrderSide::Buy) => quote .ask1_volume @@ -1654,6 +1655,10 @@ impl PlatformExprStrategy { Some(max_fill) } + fn quote_lacks_level1_depth(quote: &crate::data::IntradayExecutionQuote) -> bool { + quote.volume_delta > 0 && quote.bid1_volume == 0 && quote.ask1_volume == 0 + } + fn projected_select_execution_fill( &self, ctx: &StrategyContext<'_>, @@ -14086,7 +14091,7 @@ mod tests { fn platform_aiquant_weak_market_emits_daily_position_target_adjustment() { let date = d(2023, 5, 5); let symbol = "300621.SZ"; - let data = DataSet::from_components( + let data = DataSet::from_components_with_actions_and_quotes( vec![Instrument { symbol: symbol.to_string(), name: symbol.to_string(), @@ -14150,6 +14155,20 @@ mod tests { prev_close: 999.0, volume: 1_000_000, }], + Vec::new(), + vec![IntradayExecutionQuote { + date, + symbol: symbol.to_string(), + timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"), + last_price: 10.8, + bid1: 0.0, + ask1: 0.0, + bid1_volume: 0, + ask1_volume: 0, + volume_delta: 600, + amount_delta: 6_480.0, + trading_phase: Some("continuous".to_string()), + }], ) .expect("dataset"); let mut portfolio = PortfolioState::new(1_000_000.0); @@ -14181,6 +14200,9 @@ mod tests { cfg.stock_filter_expr = "false".to_string(); cfg.stop_loss_expr.clear(); cfg.take_profit_expr.clear(); + cfg.risk_config.trading_constraints.volume_limit_enabled = true; + cfg.risk_config.trading_constraints.liquidity_limit_enabled = true; + cfg.risk_config.trading_constraints.volume_percent = 0.25; let mut strategy = PlatformExprStrategy::new(cfg); let decision = strategy.on_day(&ctx).expect("platform decision"); @@ -14193,7 +14215,7 @@ mod tests { reason } if intent_symbol == symbol && reason == "daily_position_target_adjust" - && *quantity > 0 + && *quantity == 100 ))); }