From 3e3bebf3e0f5dd39cfab89be8a28f5cbf6c263ba Mon Sep 17 00:00:00 2001 From: boris Date: Mon, 6 Jul 2026 07:22:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=88=86=E9=92=9F=E6=88=90?= =?UTF-8?q?=E4=BA=A4=E9=87=8F=E9=99=90=E5=88=B6=E8=AF=AF=E7=94=A8=E7=9B=98?= =?UTF-8?q?=E5=8F=A3=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/broker.rs | 6 +- .../fidc-core/src/platform_expr_strategy.rs | 131 ++++++++++++++++++ .../fidc-core/src/platform_strategy_spec.rs | 28 +++- 3 files changed, 159 insertions(+), 6 deletions(-) diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index d0edc75..ef4e34b 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -5785,7 +5785,7 @@ where MatchingType::OpenAuction | MatchingType::CurrentBarClose | MatchingType::NextBarOpen => false, - MatchingType::MinuteLast => self.volume_limit || self.liquidity_limit, + MatchingType::MinuteLast => self.liquidity_limit, MatchingType::MinuteBestOwn | MatchingType::MinuteBestCounterparty | MatchingType::Vwap @@ -6062,7 +6062,7 @@ mod tests { } #[test] - fn minute_last_keeps_quote_quantity_cap_when_limits_enabled() { + fn minute_last_quote_quantity_cap_depends_on_liquidity_limit() { let volume_limited = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks) .with_volume_limit(true) @@ -6072,7 +6072,7 @@ mod tests { .with_volume_limit(false) .with_liquidity_limit(true); - assert!(volume_limited.quote_quantity_limited(MatchingType::MinuteLast)); + assert!(!volume_limited.quote_quantity_limited(MatchingType::MinuteLast)); assert!(liquidity_limited.quote_quantity_limited(MatchingType::MinuteLast)); } diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 0f138ee..c5bbbee 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -10502,6 +10502,137 @@ mod tests { assert!((position.average_cost - 5.12022).abs() < 1e-9); } + #[test] + fn platform_minute_last_volume_limit_does_not_require_l1_quantity() { + let date = d(2023, 5, 4); + let symbol = "000782.SZ"; + let data = DataSet::from_components_with_actions_and_quotes( + vec![Instrument { + symbol: symbol.to_string(), + name: symbol.to_string(), + board: "SZ".to_string(), + round_lot: 100, + listed_at: Some(d(2010, 1, 1)), + delisted_at: None, + status: "active".to_string(), + }], + vec![DailyMarketSnapshot { + date, + symbol: symbol.to_string(), + timestamp: Some("2023-05-04 10:40:00".to_string()), + day_open: 5.10, + open: 5.10, + high: 5.20, + low: 5.00, + close: 5.20, + last_price: 5.11, + bid1: 0.0, + ask1: 0.0, + prev_close: 5.00, + volume: 1_000_000, + minute_volume: 100_000, + bid1_volume: 0, + ask1_volume: 0, + trading_phase: Some("continuous".to_string()), + paused: false, + upper_limit: 5.50, + lower_limit: 4.50, + price_tick: 0.01, + }], + vec![DailyFactorSnapshot { + date, + symbol: symbol.to_string(), + market_cap_bn: 10.0, + free_float_cap_bn: 10.0, + pe_ttm: 8.0, + turnover_ratio: Some(1.0), + effective_turnover_ratio: Some(1.0), + extra_factors: BTreeMap::new(), + }], + vec![CandidateEligibility { + date, + symbol: symbol.to_string(), + is_st: false, + is_star_st: false, + is_new_listing: false, + is_paused: false, + allow_buy: true, + allow_sell: true, + is_kcb: false, + is_one_yuan: false, + risk_level_code: None, + }], + vec![BenchmarkSnapshot { + date, + benchmark: "000852.SH".to_string(), + open: 1000.0, + close: 1000.0, + prev_close: 998.0, + volume: 1_000_000, + }], + Vec::new(), + vec![IntradayExecutionQuote { + date, + symbol: symbol.to_string(), + timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"), + last_price: 5.11, + bid1: 0.0, + ask1: 0.0, + bid1_volume: 0, + ask1_volume: 0, + volume_delta: 100_000, + amount_delta: 511_000.0, + trading_phase: Some("continuous".to_string()), + }], + ) + .expect("dataset"); + + let subscriptions = BTreeSet::new(); + let portfolio = PortfolioState::new(125_000.0); + let ctx = StrategyContext { + execution_date: date, + decision_date: date, + decision_index: 20, + data: &data, + portfolio: &portfolio, + futures_account: None, + open_orders: &[], + dynamic_universe: None, + subscriptions: &subscriptions, + process_events: &[], + active_process_event: None, + active_datetime: None, + order_events: &[], + fills: &[], + }; + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.aiquant_transaction_cost = true; + cfg.commission_rate = Some(0.0003); + cfg.minimum_commission = Some(5.0); + cfg.strict_value_budget = true; + cfg.slippage_model = SlippageModel::PriceRatio(0.002); + cfg.matching_type = MatchingType::MinuteLast; + cfg.quote_quantity_limit = false; + cfg.risk_config.trading_constraints.volume_limit_enabled = true; + cfg.risk_config.trading_constraints.liquidity_limit_enabled = false; + cfg.risk_config.trading_constraints.volume_percent = 0.25; + cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).expect("time")); + let strategy = PlatformExprStrategy::new(cfg); + + let mut projected = portfolio.clone(); + let mut execution_state = super::ProjectedExecutionState::default(); + let filled = strategy.project_order_value( + &ctx, + &mut projected, + date, + symbol, + 125_000.0, + &mut execution_state, + ); + + assert_eq!(filled, 24_400); + } + #[test] fn platform_projected_market_fill_caps_volume_limit() { let date = d(2023, 5, 12); diff --git a/crates/fidc-core/src/platform_strategy_spec.rs b/crates/fidc-core/src/platform_strategy_spec.rs index dbb3f49..44eb4e9 100644 --- a/crates/fidc-core/src/platform_strategy_spec.rs +++ b/crates/fidc-core/src/platform_strategy_spec.rs @@ -868,12 +868,15 @@ fn apply_flat_risk_overrides( let value = normalize_percent_ratio(raw_value, "volume_percent")?; cfg.risk_config.trading_constraints.volume_percent = value; } - cfg.quote_quantity_limit = cfg.risk_config.trading_constraints.volume_limit_enabled - || cfg.risk_config.trading_constraints.liquidity_limit_enabled - || cfg.matching_type != MatchingType::MinuteLast; + sync_quote_quantity_limit(cfg); Ok(()) } +fn sync_quote_quantity_limit(cfg: &mut PlatformExprStrategyConfig) { + cfg.quote_quantity_limit = cfg.risk_config.trading_constraints.liquidity_limit_enabled + || cfg.matching_type != MatchingType::MinuteLast; +} + fn apply_risk_policy_overrides( cfg: &mut PlatformExprStrategyConfig, policy: Option<&StrategyRiskPolicySpec>, @@ -1757,6 +1760,7 @@ pub fn platform_expr_config_from_spec( execution.slippage_max_value, execution.strict_value_budget, )?; + sync_quote_quantity_limit(&mut cfg); } if cfg.aiquant_transaction_cost && cfg @@ -2412,6 +2416,24 @@ mod tests { assert!(cfg.quote_quantity_limit); } + #[test] + fn volume_limit_does_not_enable_quote_quantity_limit_for_minute_last() { + let spec = serde_json::json!({ + "execution": { + "matchingType": "minute_last", + "volumeLimit": true, + "liquidityLimit": false, + "volumePercent": 0.25 + } + }); + + let cfg = platform_expr_config_from_value("", "", &spec).expect("config"); + + assert!(cfg.risk_config.trading_constraints.volume_limit_enabled); + assert!(!cfg.risk_config.trading_constraints.liquidity_limit_enabled); + assert!(!cfg.quote_quantity_limit); + } + #[test] fn parses_st_and_star_st_risk_policy_switches_into_platform_config() { let spec = serde_json::json!({