From 7f17fa1fb427fefe5c70c9170fe98e2a8761e3ac Mon Sep 17 00:00:00 2001 From: boris Date: Sat, 5 Sep 2026 02:02:01 +0800 Subject: [PATCH] perf: project stock snapshot fields by strategy --- .../fidc-core/src/platform_expr_strategy.rs | 107 ++++++++++++++++-- 1 file changed, 99 insertions(+), 8 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 0c2af72..be9cbfa 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -742,6 +742,13 @@ struct StockRollingRequirements { fields: BTreeSet<(StockRollingField, usize)>, } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +struct StockSnapshotFieldRequirements { + amount: bool, + touched_upper_limit: bool, + touched_lower_limit: bool, +} + impl StockRollingRequirements { fn require(&mut self, field: StockRollingField, lookback: usize) { if lookback > 0 { @@ -1046,6 +1053,7 @@ pub struct PlatformExprStrategy { selection_quote_usage: StockFilterQuoteUsage, stock_rolling_requirements: StockRollingRequirements, stock_current_rolling_mean_required: bool, + stock_snapshot_field_requirements: StockSnapshotFieldRequirements, stock_extra_factors_required: bool, stock_extra_factor_identifiers: BTreeSet, stock_extra_factor_map_required: bool, @@ -1325,6 +1333,8 @@ impl PlatformExprStrategy { let stock_rolling_requirements = Self::stock_rolling_requirements_for_config(&config); let stock_current_rolling_mean_required = Self::stock_current_rolling_mean_required_for_config(&config); + let stock_snapshot_field_requirements = + Self::stock_snapshot_field_requirements_for_config(&config); let stock_extra_factors_required = Self::stock_extra_factors_required_for_config(&config, &prelude_declared_identifiers); let stock_extra_factor_identifiers = @@ -1368,6 +1378,7 @@ impl PlatformExprStrategy { selection_quote_usage, stock_rolling_requirements, stock_current_rolling_mean_required, + stock_snapshot_field_requirements, stock_extra_factors_required, stock_extra_factor_identifiers, stock_extra_factor_map_required, @@ -4137,7 +4148,9 @@ impl PlatformExprStrategy { let stock_volume_ma20 = volume_rolling(2); let stock_volume_ma60 = volume_rolling(3); let stock_volume_ma100 = volume_rolling(4); - let touched_upper_limit = if intraday_same_day_factor { + let touched_upper_limit = if !self.stock_snapshot_field_requirements.touched_upper_limit { + false + } else if intraday_same_day_factor { !market.paused && (market.is_at_upper_limit_price(market.close) || market.is_at_upper_limit_price(market.open) @@ -4156,7 +4169,9 @@ impl PlatformExprStrategy { || feature_market.is_at_upper_limit_price(feature_market.open) || feature_market.is_at_upper_limit_price(feature_market.day_open)) }; - let touched_lower_limit = if intraday_same_day_factor { + let touched_lower_limit = if !self.stock_snapshot_field_requirements.touched_lower_limit { + false + } else if intraday_same_day_factor { !market.paused && (market.is_at_lower_limit_price(market.close) || market.is_at_lower_limit_price(market.open) @@ -4175,7 +4190,9 @@ impl PlatformExprStrategy { || feature_market.is_at_lower_limit_price(feature_market.open) || feature_market.is_at_lower_limit_price(feature_market.day_open)) }; - let amount = if intraday_same_day_factor { + let amount = if !self.stock_snapshot_field_requirements.amount { + 0.0 + } else if intraday_same_day_factor { f64::NAN } else { factor.extra_factors.get("amount").copied().unwrap_or(0.0) @@ -10052,6 +10069,43 @@ impl PlatformExprStrategy { false } + fn stock_snapshot_field_requirements_for_config( + config: &PlatformExprStrategyConfig, + ) -> StockSnapshotFieldRequirements { + if Self::has_stock_explicit_actions(config) + || Self::stock_extra_factor_map_required_for_config(config) + { + return StockSnapshotFieldRequirements { + amount: true, + touched_upper_limit: true, + touched_lower_limit: true, + }; + } + + let mut identifiers = BTreeSet::new(); + for expr in [ + config.prelude.as_str(), + config.stock_filter_expr.as_str(), + config.buy_scale_expr.as_str(), + config.stop_loss_expr.as_str(), + config.take_profit_expr.as_str(), + config.rank_expr.as_str(), + config.market_cap_field.as_str(), + config.rank_by.as_str(), + ] { + identifiers.extend(Self::extract_identifier_candidates(&Self::normalize_expr( + expr, + ))); + } + StockSnapshotFieldRequirements { + amount: identifiers.contains("amount"), + touched_upper_limit: identifiers.contains("touched_upper_limit") + || identifiers.contains("hit_upper_limit"), + touched_lower_limit: identifiers.contains("touched_lower_limit") + || identifiers.contains("hit_lower_limit"), + } + } + fn stock_extra_factors_required_for_config( config: &PlatformExprStrategyConfig, prelude_declared_identifiers: &BTreeSet, @@ -12622,7 +12676,8 @@ mod tests { PlatformPortfolioDrawdownController, PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformStopTakeReferencePriceMode, PlatformTradeAction, PlatformUniverseActionKind, RuntimeHelperResolution, SelectionRiskDeferral, StockFilterQuoteUsage, - framework_stock_rolling_factor_requirement, scheduled_position_exposure, + StockSnapshotFieldRequirements, framework_stock_rolling_factor_requirement, + scheduled_position_exposure, }; use crate::{ AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction, @@ -14460,6 +14515,38 @@ mod tests { ); } + #[test] + fn stock_snapshot_fields_are_loaded_only_when_the_strategy_uses_them() { + let base = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); + assert_eq!( + base.stock_snapshot_field_requirements, + StockSnapshotFieldRequirements::default() + ); + + let mut direct = PlatformExprStrategyConfig::microcap_rotation(); + direct.stock_filter_expr = + "amount > 0.0 && hit_upper_limit && !touched_lower_limit".to_string(); + assert_eq!( + PlatformExprStrategy::new(direct).stock_snapshot_field_requirements, + StockSnapshotFieldRequirements { + amount: true, + touched_upper_limit: true, + touched_lower_limit: true, + } + ); + + let mut dynamic_map = PlatformExprStrategyConfig::microcap_rotation(); + dynamic_map.stock_filter_expr = "factors[\"custom_alpha\"] > 0.0".to_string(); + assert_eq!( + PlatformExprStrategy::new(dynamic_map).stock_snapshot_field_requirements, + StockSnapshotFieldRequirements { + amount: true, + touched_upper_limit: true, + touched_lower_limit: true, + } + ); + } + #[test] fn platform_expr_missing_requested_factor_does_not_default_to_zero() { let date = d(2025, 5, 19); @@ -17918,7 +18005,9 @@ mod tests { order_events: &[], fills: &[], }; - let strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.stock_filter_expr = "touched_upper_limit || touched_lower_limit".to_string(); + let strategy = PlatformExprStrategy::new(cfg); let stock = strategy .stock_state_with_factor_date(&ctx, date, date, symbol) @@ -18054,7 +18143,9 @@ mod tests { order_events: &[], fills: &[], }; - let strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.stock_filter_expr = "touched_upper_limit".to_string(); + let strategy = PlatformExprStrategy::new(cfg); let stock = strategy .stock_state_with_factor_date(&ctx, date, factor_date, symbol) @@ -18159,7 +18250,7 @@ mod tests { }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.signal_symbol = symbol.to_string(); - cfg.stock_filter_expr = "true".to_string(); + cfg.stock_filter_expr = "touched_upper_limit".to_string(); cfg.benchmark_short_ma_days = 1; cfg.benchmark_long_ma_days = 1; let strategy = PlatformExprStrategy::new(cfg); @@ -21829,7 +21920,7 @@ mod tests { cfg.market_cap_upper_expr = "100".to_string(); cfg.selection_limit_expr = "1".to_string(); cfg.stock_filter_expr = - "last_price <= 0 || (!at_upper_limit && !at_lower_limit)".to_string(); + "amount >= 0 && (last_price <= 0 || (!at_upper_limit && !at_lower_limit))".to_string(); cfg.exposure_expr = "1.0".to_string(); let mut strategy = PlatformExprStrategy::new(cfg);