From bb87d69224fd3b8d2f831a538682bddd638edd6c Mon Sep 17 00:00:00 2001 From: boris Date: Sun, 21 Jun 2026 01:13:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=89=E9=9C=80=E4=BF=9D=E7=95=99=E8=82=A1?= =?UTF-8?q?=E7=A5=A8=E9=A2=9D=E5=A4=96=E5=9B=A0=E5=AD=90=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 181 +++++++++++++++++- 1 file changed, 180 insertions(+), 1 deletion(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 8033e8b..3152e98 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -554,6 +554,7 @@ pub struct PlatformExprStrategy { compact_stock_filter_expr: String, stock_filter_quote_usage: StockFilterQuoteUsage, stock_rolling_requirements: StockRollingRequirements, + stock_extra_factors_required: bool, stock_state_cache_date: RefCell>, stock_state_cache: RefCell), StockExpressionState>>, @@ -646,6 +647,11 @@ impl PlatformExprStrategy { Self::stock_filter_quote_usage_for_expr(&normalized_stock_filter_expr); let stock_rolling_requirements = Self::stock_rolling_requirements_for_config(&config, &normalized_stock_filter_expr); + let stock_extra_factors_required = Self::stock_extra_factors_required_for_config( + &config, + &normalized_stock_filter_expr, + &prelude_declared_identifiers, + ); Self { config, engine, @@ -663,6 +669,7 @@ impl PlatformExprStrategy { compact_stock_filter_expr, stock_filter_quote_usage, stock_rolling_requirements, + stock_extra_factors_required, stock_state_cache_date: RefCell::new(None), stock_state_cache: RefCell::new(HashMap::new()), } @@ -2461,7 +2468,11 @@ impl PlatformExprStrategy { stock_volume_ma20, stock_volume_ma60, stock_volume_ma100, - extra_factors: factor.extra_factors.clone(), + extra_factors: if self.stock_extra_factors_required { + factor.extra_factors.clone() + } else { + BTreeMap::new() + }, extra_text_factors: ctx .data .factor_text_snapshots_on(date) @@ -6291,6 +6302,152 @@ impl PlatformExprStrategy { requirements } + fn stock_extra_factors_required_for_config( + config: &PlatformExprStrategyConfig, + normalized_stock_filter_expr: &str, + prelude_declared_identifiers: &BTreeSet, + ) -> bool { + if !config.explicit_actions.is_empty() { + return true; + } + if Self::stock_field_may_use_extra_factors(&config.market_cap_field) + || Self::stock_field_may_use_extra_factors(&config.rank_by) + { + return true; + } + let stock_filter_has_fast_path = + Self::stock_filter_fast_path_supported(normalized_stock_filter_expr); + if !stock_filter_has_fast_path + && Self::expr_requires_stock_extra_factors( + &config.stock_filter_expr, + prelude_declared_identifiers, + true, + ) + { + return true; + } + [ + config.buy_scale_expr.as_str(), + config.stop_loss_expr.as_str(), + config.take_profit_expr.as_str(), + config.rank_expr.as_str(), + ] + .into_iter() + .any(|expr| { + Self::expr_requires_stock_extra_factors(expr, prelude_declared_identifiers, true) + }) + } + + fn expr_requires_stock_extra_factors( + expr: &str, + prelude_declared_identifiers: &BTreeSet, + stock_rolling_helpers_require_extra: bool, + ) -> bool { + let normalized = Self::normalize_expr(expr); + let identifiers = Self::extract_identifier_candidates(&normalized); + if identifiers.contains("factors") || identifiers.contains("factor") { + return true; + } + if stock_rolling_helpers_require_extra + && identifiers + .iter() + .any(|name| matches!(name.as_str(), "rolling_mean" | "sma" | "ma" | "vma")) + { + return true; + } + identifiers.into_iter().any(|name| { + !Self::is_expression_keyword(&name) + && !Self::is_runtime_helper(&name) + && !Self::is_reserved_scope_name(&name) + && !prelude_declared_identifiers.contains(&name) + }) + } + + fn is_expression_keyword(name: &str) -> bool { + matches!( + name, + "true" | "false" | "let" | "if" | "else" | "return" | "and" | "or" | "not" + ) + } + + fn stock_field_may_use_extra_factors(field: &str) -> bool { + let field = field.trim(); + if field.is_empty() { + return false; + } + !Self::stock_field_is_builtin(field) + } + + fn stock_field_is_builtin(field: &str) -> bool { + matches!( + field.trim(), + "market_cap" + | "market_cap_bn" + | "free_float_cap" + | "free_float_market_cap" + | "free_float_cap_bn" + | "pe_ttm" + | "volume" + | "tick_volume" + | "bid1_volume" + | "ask1_volume" + | "turnover" + | "turnover_ratio" + | "effective_turnover_ratio" + | "open" + | "high" + | "low" + | "close" + | "last" + | "last_price" + | "prev_close" + | "amount" + | "upper_limit" + | "lower_limit" + | "price_tick" + | "round_lot" + | "minimum_order_quantity" + | "order_step_size" + | "listed_days" + | "stock_ma_short" + | "stock_ma_mid" + | "stock_ma_long" + | "stock_ma5" + | "stock_ma10" + | "stock_ma20" + | "stock_ma30" + | "ma5" + | "ma10" + | "ma20" + | "ma30" + | "stock_volume_ma5" + | "stock_volume_ma10" + | "stock_volume_ma20" + | "stock_volume_ma60" + | "stock_volume_ma100" + | "volume_ma5" + | "volume_ma10" + | "volume_ma20" + | "volume_ma60" + | "volume_ma100" + | "allow_buy" + | "allow_sell" + | "touched_upper_limit" + | "hit_upper_limit" + | "touched_lower_limit" + | "hit_lower_limit" + | "paused" + | "is_st" + | "is_kcb" + | "is_one_yuan" + | "is_new_listing" + | "candidate_market_cap" + | "candidate_market_cap_bn" + | "candidate_free_float_cap" + | "candidate_free_float_cap_bn" + ) + } + fn require_stock_rollings_for_identifiers( requirements: &mut StockRollingRequirements, config: &PlatformExprStrategyConfig, @@ -6368,6 +6525,28 @@ impl PlatformExprStrategy { } } + fn stock_filter_fast_path_supported(normalized_stock_filter_expr: &str) -> bool { + let compact = Self::compact_expr(normalized_stock_filter_expr); + if compact == "stock_ma_short>stock_ma_mid*ma_ratio&&stock_ma_mid>stock_ma_long" { + return true; + } + let mut filter_body = compact.as_str(); + if let Some(rest) = filter_body.strip_prefix("listed_days>=min_listed_days&&") { + filter_body = rest; + } + let base_microcap_filter = "rolling_mean(\"close\",5)>rolling_mean(\"close\",10)*ma_ratio&&rolling_mean(\"close\",10)>rolling_mean(\"close\",30)*ma_ratio&&rolling_mean(\"volume\",5)0&&rolling_mean(\"volume\",100)>0" + | "&&rolling_mean(\"volume\",5)>0.0&&rolling_mean(\"volume\",100)>0.0" + ) + }) + } + fn stock_filter_uses_intraday_quote_fields(&self) -> bool { self.stock_filter_quote_usage() != StockFilterQuoteUsage::DailyOnly }