diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index ec41c4f..591b8ce 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -11509,6 +11509,88 @@ impl PlatformExprStrategy { let universe_factor_day = ctx.data.daily_snapshot_view(universe_factor_date); let factor_day = ctx.data.daily_snapshot_view(stock_factor_date); let quote_candidate_limit = self.quote_plan_candidate_limit(selection_limit); + if self.rank_reuses_market_cap_order() { + let candidate_range = self.market_cap_ordered_selection_range( + &universe_factor_day, + &universe_symbol_ids, + band_low, + band_high, + ); + let mut quote_candidate_symbols = Vec::new(); + let mut selected_symbols = Vec::new(); + let mut processed_scope = 0usize; + for &symbol_id in &universe_symbol_ids[candidate_range] { + let factor = universe_factor_day + .factor(symbol_id) + .expect("market-cap order references missing factor row"); + let symbol = factor.symbol.as_str(); + let market_cap_bn = decision_market_cap_bn(factor); + let free_float_cap_bn = decision_free_float_cap_bn(factor); + let stock = self.uncached_selection_stock_state_from_views_by_symbol_id( + ctx, + date, + stock_factor_date, + symbol_id, + symbol, + &execution_day, + &factor_day, + )?; + let field_value = self.selection_field_value_from_caps( + market_cap_bn, + free_float_cap_bn, + &stock, + ); + if !field_value.is_finite() { + if diagnostics.len() < 12 { + diagnostics.push(format!( + "{symbol} quote_plan rejected by missing selection field" + )); + } + continue; + } + if field_value < band_low || field_value > band_high { + continue; + } + processed_scope += 1; + if !self.stock_passes_quote_plan_filter(ctx, day, &stock, quote_usage)? { + if diagnostics.len() < 12 { + diagnostics.push(format!("{symbol} quote_plan rejected by stock_expr")); + } + continue; + } + if quote_candidate_symbols.len() < quote_candidate_limit { + quote_candidate_symbols.push(factor.symbol.clone()); + } + if !ctx.is_lagged_execution() + && let Some(reason) = self.stock_selection_limit_rejection_reason(&stock) + { + if diagnostics.len() < 12 { + diagnostics.push(format!("{symbol} quote_plan rejected by {reason}")); + } + continue; + } + if quote_usage == StockFilterQuoteUsage::IntradayQuote + && !self.stock_passes_expr(ctx, day, &stock)? + { + if diagnostics.len() < 12 { + diagnostics.push(format!("{symbol} quote_plan rejected by stock_expr")); + } + continue; + } + selected_symbols.push(factor.symbol.clone()); + if selected_symbols.len() >= selection_limit + && quote_candidate_symbols.len() >= quote_candidate_limit + { + break; + } + } + return Ok(( + quote_candidate_symbols, + selected_symbols, + processed_scope, + diagnostics, + )); + } for symbol_id in universe_symbol_ids { let factor = universe_factor_day .factor(symbol_id) @@ -36132,7 +36214,7 @@ let target_exposure = csi_ready ? dynamic_exposure : 0.0; cfg.stock_filter_expr = "last_price > 0".to_string(); cfg.rank_by = "market_cap".to_string(); cfg.risk_config.static_rules.reject_paused_selection = true; - let strategy = PlatformExprStrategy::new(cfg); + let strategy = PlatformExprStrategy::new(cfg.clone()); let plan = strategy.selection_quote_plan(&ctx, 0).expect("quote plan"); @@ -36143,6 +36225,15 @@ let target_exposure = csi_ready ? dynamic_exposure : 0.0; plan.order_symbols, vec!["000001.SZ".to_string(), "000003.SZ".to_string()] ); + + cfg.rank_expr = "market_cap".to_string(); + let generic_plan = PlatformExprStrategy::new(cfg) + .selection_quote_plan(&ctx, 0) + .expect("generic quote plan"); + assert_eq!(plan.candidate_symbols, generic_plan.candidate_symbols); + assert_eq!(plan.order_symbols, generic_plan.order_symbols); + assert_eq!(plan.processed_scope, generic_plan.processed_scope); + assert_eq!(plan.diagnostics, generic_plan.diagnostics); } #[test]