From ca9732ecb20f357a2134d684e46dcbfc2c2fda48 Mon Sep 17 00:00:00 2001 From: boris Date: Wed, 22 Jul 2026 09:13:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=A8=A1=E5=9E=8B=E6=8E=92?= =?UTF-8?q?=E5=90=8D=E7=BC=BA=E5=A4=B1=E8=AF=8A=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 77 ++++++++++++++++++- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index a914b23..613fd34 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -8040,6 +8040,38 @@ impl PlatformExprStrategy { } let rank_value = self.rank_value(ctx, day, &candidate, &stock)?; if !rank_value.is_finite() { + // Model-score artifacts intentionally contain only the PIT-eligible + // ranked universe. Do not report a missing score for a symbol that + // the same signal-day business filter would reject before ranking. + if !ctx.is_lagged_execution() + && let Some(reason) = self.stock_selection_limit_rejection_reason(&stock) + { + if diagnostics.len() < 12 { + diagnostics.push(format!("{} rejected by {}", candidate.symbol, reason)); + } + continue; + } + if !self.stock_passes_expr(ctx, day, &stock)? { + if diagnostics.len() < 12 { + diagnostics.push(format!("{} rejected by stock_expr", candidate.symbol)); + } + continue; + } + if self.config.stop_take_reference_price_mode + == PlatformStopTakeReferencePriceMode::SignalDayPostAdjustedClose + && ctx + .data + .market_latest_back_adjusted_close(date, &candidate.symbol) + .is_none() + { + if diagnostics.len() < 12 { + diagnostics.push(format!( + "{} rejected by missing signal-day post-adjusted close", + candidate.symbol + )); + } + continue; + } missing_rank_count += 1; if missing_rank_examples.len() < 5 { missing_rank_examples.push(candidate.symbol.clone()); @@ -8754,6 +8786,18 @@ impl PlatformExprStrategy { } let rank_value = self.rank_value(ctx, day, &candidate, &stock)?; if !rank_value.is_finite() { + // The score universe is already filtered by the signal-day + // candidate contract. Missing rank is actionable only when the + // daily portion of that same contract still admits the symbol. + if !self.stock_passes_quote_plan_filter(ctx, day, &stock, quote_usage)? { + if diagnostics.len() < 12 { + diagnostics.push(format!( + "{} quote_plan rejected by stock_expr", + candidate.symbol + )); + } + continue; + } missing_rank_count += 1; if missing_rank_examples.len() < 5 { missing_rank_examples.push(candidate.symbol.clone()); @@ -31687,7 +31731,11 @@ mod tests { pe_ttm: 8.0, turnover_ratio: Some(1.0), effective_turnover_ratio: Some(1.0), - extra_factors: BTreeMap::new(), + extra_factors: if *symbol == "000002.SZ" { + BTreeMap::new() + } else { + BTreeMap::from([("model_score".to_string(), 4.0 - index as f64)]) + }, }) .collect(), symbols @@ -31736,14 +31784,16 @@ mod tests { }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.signal_symbol = "000001.SZ".to_string(); - cfg.max_positions = 2; + cfg.max_positions = 3; cfg.benchmark_short_ma_days = 1; cfg.benchmark_long_ma_days = 1; cfg.market_cap_lower_expr = "0".to_string(); cfg.market_cap_upper_expr = "100".to_string(); - cfg.selection_limit_expr = "2".to_string(); + cfg.selection_limit_expr = "3".to_string(); cfg.stock_filter_expr = "prev_close > 5.0 && !at_upper_limit && !at_lower_limit".to_string(); + cfg.rank_by = "model_score".to_string(); + cfg.rank_desc = true; cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); let strategy = PlatformExprStrategy::new(cfg); @@ -31770,6 +31820,27 @@ mod tests { ], "quote plan may prefetch a superset; final selection/order risk is checked in the decision path" ); + assert!( + plan.diagnostics + .iter() + .all(|item| !item.contains("rank_field_unavailable")), + "a symbol rejected by the daily candidate filter must not be reported as missing model score" + ); + + let day = strategy.day_state(&ctx, date).expect("day state"); + let (selected, diagnostics, _) = strategy + .select_symbols(&ctx, date, date, date, &day, 0.0, 100.0, 3) + .expect("selection"); + assert_eq!( + selected, + vec!["000003.SZ".to_string(), "000004.SZ".to_string()] + ); + assert!( + diagnostics + .iter() + .all(|item| !item.contains("rank_field_unavailable")), + "the final selector must apply the same missing-score classification" + ); } #[test]