diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 98837eb..fde2659 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -556,6 +556,7 @@ pub struct PlatformExprStrategy { selection_quote_usage: StockFilterQuoteUsage, stock_rolling_requirements: StockRollingRequirements, stock_extra_factors_required: bool, + stock_extra_factor_identifiers: BTreeSet, stock_text_factors_required: bool, stock_state_cache_date: RefCell>, stock_state_cache: RefCell< @@ -657,6 +658,11 @@ impl PlatformExprStrategy { &normalized_stock_filter_expr, &prelude_declared_identifiers, ); + let stock_extra_factor_identifiers = Self::stock_extra_factor_identifiers_for_config( + &config, + &normalized_stock_filter_expr, + &prelude_declared_identifiers, + ); let stock_text_factors_required = Self::stock_text_factors_required_for_config( &config, &normalized_stock_filter_expr, @@ -681,6 +687,7 @@ impl PlatformExprStrategy { selection_quote_usage, stock_rolling_requirements, stock_extra_factors_required, + stock_extra_factor_identifiers, stock_text_factors_required, stock_state_cache_date: RefCell::new(None), stock_state_cache: RefCell::new(HashMap::new()), @@ -3196,7 +3203,9 @@ impl PlatformExprStrategy { for identifier in Self::extract_identifier_candidates(&expanded_expr) { if Self::is_reserved_scope_name(identifier.as_str()) || self.prelude_declared_identifiers.contains(&identifier) - || (!day.available_factor_names.contains(&identifier) + || (!self.stock_extra_factor_identifiers.contains(&identifier) + && !item.extra_factors.contains_key(&identifier) + && !day.available_factor_names.contains(&identifier) && !day.available_text_factor_names.contains(&identifier)) { continue; @@ -3204,7 +3213,7 @@ impl PlatformExprStrategy { if let Some(value) = item.extra_text_factors.get(&identifier) { scope.push_dynamic(identifier, Dynamic::from(value.clone())); } else { - let value = item.extra_factors.get(&identifier).copied().unwrap_or(0.0); + let value = item.extra_factors.get(&identifier).copied().unwrap_or(f64::NAN); scope.push_dynamic(identifier, Dynamic::from(value)); } } @@ -6423,6 +6432,69 @@ impl PlatformExprStrategy { }) } + fn stock_extra_factor_identifiers_for_config( + config: &PlatformExprStrategyConfig, + normalized_stock_filter_expr: &str, + prelude_declared_identifiers: &BTreeSet, + ) -> BTreeSet { + let mut identifiers = BTreeSet::new(); + if Self::stock_field_may_use_extra_factors(&config.market_cap_field) { + identifiers.insert(config.market_cap_field.trim().to_string()); + } + if Self::stock_field_may_use_extra_factors(&config.rank_by) { + identifiers.insert(config.rank_by.trim().to_string()); + } + let stock_filter_has_fast_path = + Self::stock_filter_fast_path_supported(normalized_stock_filter_expr); + if !stock_filter_has_fast_path { + Self::collect_stock_extra_factor_identifiers( + &mut identifiers, + &config.stock_filter_expr, + prelude_declared_identifiers, + true, + ); + } + for expr in [ + config.buy_scale_expr.as_str(), + config.stop_loss_expr.as_str(), + config.take_profit_expr.as_str(), + config.rank_expr.as_str(), + ] { + Self::collect_stock_extra_factor_identifiers( + &mut identifiers, + expr, + prelude_declared_identifiers, + true, + ); + } + identifiers + } + + fn collect_stock_extra_factor_identifiers( + out: &mut BTreeSet, + expr: &str, + prelude_declared_identifiers: &BTreeSet, + stock_rolling_helpers_require_extra: bool, + ) { + let normalized = Self::normalize_expr(expr); + for name in Self::extract_identifier_candidates(&normalized) { + if Self::is_expression_keyword(&name) + || Self::is_runtime_helper(&name) + || Self::is_reserved_scope_name(&name) + || Self::stock_field_is_builtin(&name) + || prelude_declared_identifiers.contains(&name) + { + continue; + } + if stock_rolling_helpers_require_extra + && matches!(name.as_str(), "rolling_mean" | "sma" | "ma" | "vma") + { + continue; + } + out.insert(name); + } + } + fn stock_text_factors_required_for_config( config: &PlatformExprStrategyConfig, normalized_stock_filter_expr: &str, @@ -8213,6 +8285,19 @@ mod tests { assert!((strategy.buy_commission(1_000.0) - 5.0).abs() < 1e-9); } + #[test] + fn platform_expr_tracks_requested_dynamic_factor_identifiers() { + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.rank_expr = "model_score + close / prev_close".to_string(); + + let strategy = PlatformExprStrategy::new(cfg); + + assert!(strategy.stock_extra_factors_required); + assert!(strategy.stock_extra_factor_identifiers.contains("model_score")); + assert!(!strategy.stock_extra_factor_identifiers.contains("close")); + assert!(!strategy.stock_extra_factor_identifiers.contains("prev_close")); + } + #[test] fn platform_expr_rewrites_prelude_assignment_ternary() { let prelude = "let cap_low = sig_close <= 0 ? 7 : max(5, min(cap_low_raw, 70));";