diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index cebd83b..9485d78 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -917,6 +917,17 @@ impl<'a> SelectiveExpressionScope<'a> { self } + fn push_lazy(&mut self, name: &str, build: F) -> &mut Self + where + T: Into, + F: FnOnce() -> T, + { + if self.requires(name) { + self.inner.push_dynamic(name.to_string(), build().into()); + } + self + } + fn push_required>(&mut self, name: &str, value: T) -> &mut Self { self.inner.push_dynamic(name.to_string(), value.into()); self @@ -4448,14 +4459,15 @@ impl PlatformExprStrategy { include_process_event_counts: bool, ) -> Scope<'static> { let mut scope = SelectiveExpressionScope::new(scope_identifiers); - let trade_date = day.date.format("%Y-%m-%d").to_string(); - let decision_date = ctx.decision_date.format("%Y-%m-%d").to_string(); - let execution_date = ctx.execution_date.format("%Y-%m-%d").to_string(); - scope.push("trade_date", trade_date.clone()); - scope.push("current_date", trade_date.clone()); - scope.push("date", trade_date); - scope.push("decision_date", decision_date); - scope.push("execution_date", execution_date); + scope.push_lazy("trade_date", || day.date.format("%Y-%m-%d").to_string()); + scope.push_lazy("current_date", || day.date.format("%Y-%m-%d").to_string()); + scope.push_lazy("date", || day.date.format("%Y-%m-%d").to_string()); + scope.push_lazy("decision_date", || { + ctx.decision_date.format("%Y-%m-%d").to_string() + }); + scope.push_lazy("execution_date", || { + ctx.execution_date.format("%Y-%m-%d").to_string() + }); scope.push("signal_open", day.signal_open); scope.push("signal_close", day.signal_close); scope.push("benchmark_open", day.benchmark_open); @@ -4542,74 +4554,63 @@ impl PlatformExprStrategy { scope.push("is_month_start", day.is_month_start); scope.push("is_month_end", day.is_month_end); scope.push("signal_ma30", day.signal_ma30); - scope.push("has_open_orders", ctx.has_open_orders()); - scope.push("open_order_count", ctx.open_order_count() as i64); - scope.push("open_buy_order_count", ctx.open_buy_order_count() as i64); - scope.push("open_sell_order_count", ctx.open_sell_order_count() as i64); - scope.push("open_buy_qty", ctx.open_buy_quantity() as i64); - scope.push("open_sell_qty", ctx.open_sell_quantity() as i64); - scope.push("latest_open_order_id", ctx.latest_open_order_id() as i64); - scope.push( - "latest_open_order_status", - ctx.latest_open_order_status().to_string(), - ); - scope.push( - "latest_open_order_unfilled_qty", - ctx.latest_open_order_unfilled_quantity() as i64, - ); - scope.push("has_dynamic_universe", ctx.has_dynamic_universe()); - scope.push( - "dynamic_universe_count", - ctx.dynamic_universe_count() as i64, - ); - scope.push("has_subscriptions", ctx.has_subscriptions()); - scope.push("subscription_count", ctx.subscription_count() as i64); + scope.push_lazy("has_open_orders", || ctx.has_open_orders()); + scope.push_lazy("open_order_count", || ctx.open_order_count() as i64); + scope.push_lazy("open_buy_order_count", || ctx.open_buy_order_count() as i64); + scope.push_lazy("open_sell_order_count", || { + ctx.open_sell_order_count() as i64 + }); + scope.push_lazy("open_buy_qty", || ctx.open_buy_quantity() as i64); + scope.push_lazy("open_sell_qty", || ctx.open_sell_quantity() as i64); + scope.push_lazy("latest_open_order_id", || ctx.latest_open_order_id() as i64); + scope.push_lazy("latest_open_order_status", || { + ctx.latest_open_order_status().to_string() + }); + scope.push_lazy("latest_open_order_unfilled_qty", || { + ctx.latest_open_order_unfilled_quantity() as i64 + }); + scope.push_lazy("has_dynamic_universe", || ctx.has_dynamic_universe()); + scope.push_lazy("dynamic_universe_count", || { + ctx.dynamic_universe_count() as i64 + }); + scope.push_lazy("has_subscriptions", || ctx.has_subscriptions()); + scope.push_lazy("subscription_count", || ctx.subscription_count() as i64); scope.push( "subscription_guard_required", self.config.subscription_guard_required, ); - scope.push("has_process_events", ctx.has_process_events()); - scope.push("process_event_count", ctx.process_event_count() as i64); - scope.push( - "current_process_kind", - ctx.current_process_event_kind().to_string(), - ); - scope.push( - "current_process_order_id", - ctx.current_process_event_order_id() as i64, - ); - scope.push( - "current_process_symbol", - ctx.current_process_event_symbol().to_string(), - ); - scope.push( - "current_process_side", - ctx.current_process_event_side().to_string(), - ); - scope.push( - "current_process_detail", - ctx.current_process_event_detail().to_string(), - ); - scope.push( - "latest_process_kind", - ctx.latest_process_event_kind().to_string(), - ); - scope.push( - "latest_process_order_id", - ctx.latest_process_event_order_id() as i64, - ); - scope.push( - "latest_process_symbol", - ctx.latest_process_event_symbol().to_string(), - ); - scope.push( - "latest_process_side", - ctx.latest_process_event_side().to_string(), - ); - scope.push( - "latest_process_detail", - ctx.latest_process_event_detail().to_string(), - ); + scope.push_lazy("has_process_events", || ctx.has_process_events()); + scope.push_lazy("process_event_count", || ctx.process_event_count() as i64); + scope.push_lazy("current_process_kind", || { + ctx.current_process_event_kind().to_string() + }); + scope.push_lazy("current_process_order_id", || { + ctx.current_process_event_order_id() as i64 + }); + scope.push_lazy("current_process_symbol", || { + ctx.current_process_event_symbol().to_string() + }); + scope.push_lazy("current_process_side", || { + ctx.current_process_event_side().to_string() + }); + scope.push_lazy("current_process_detail", || { + ctx.current_process_event_detail().to_string() + }); + scope.push_lazy("latest_process_kind", || { + ctx.latest_process_event_kind().to_string() + }); + scope.push_lazy("latest_process_order_id", || { + ctx.latest_process_event_order_id() as i64 + }); + scope.push_lazy("latest_process_symbol", || { + ctx.latest_process_event_symbol().to_string() + }); + scope.push_lazy("latest_process_side", || { + ctx.latest_process_event_side().to_string() + }); + scope.push_lazy("latest_process_detail", || { + ctx.latest_process_event_detail().to_string() + }); let process_event_counts = if include_day_factors || include_process_event_counts { let mut counts = Map::new(); for (key, value) in ctx.process_event_counts() { @@ -4820,7 +4821,7 @@ impl PlatformExprStrategy { stock.lower_limit, stock.price_tick, ); - scope.push("symbol", stock.symbol.to_string()); + scope.push_lazy("symbol", || stock.symbol.to_string()); scope.push("market_cap", stock.market_cap); scope.push("market_cap_bn", stock.market_cap_bn); scope.push("free_float_cap", stock.free_float_cap); @@ -4866,36 +4867,29 @@ impl PlatformExprStrategy { scope.push("listed_days", stock.listed_days); scope.push("at_upper_limit", at_upper_limit); scope.push("at_lower_limit", at_lower_limit); - scope.push( - "symbol_open_order_count", - ctx.symbol_open_order_count(&stock.symbol) as i64, - ); - scope.push( - "symbol_open_buy_qty", - ctx.symbol_open_buy_quantity(&stock.symbol) as i64, - ); - scope.push( - "symbol_open_sell_qty", - ctx.symbol_open_sell_quantity(&stock.symbol) as i64, - ); - scope.push( - "latest_symbol_open_order_id", - ctx.latest_symbol_open_order_id(&stock.symbol) as i64, - ); - scope.push( - "latest_symbol_open_order_status", + scope.push_lazy("symbol_open_order_count", || { + ctx.symbol_open_order_count(&stock.symbol) as i64 + }); + scope.push_lazy("symbol_open_buy_qty", || { + ctx.symbol_open_buy_quantity(&stock.symbol) as i64 + }); + scope.push_lazy("symbol_open_sell_qty", || { + ctx.symbol_open_sell_quantity(&stock.symbol) as i64 + }); + scope.push_lazy("latest_symbol_open_order_id", || { + ctx.latest_symbol_open_order_id(&stock.symbol) as i64 + }); + scope.push_lazy("latest_symbol_open_order_status", || { ctx.latest_symbol_open_order_status(&stock.symbol) - .to_string(), - ); - scope.push( - "latest_symbol_open_order_unfilled_qty", - ctx.latest_symbol_open_order_unfilled_quantity(&stock.symbol) as i64, - ); - scope.push( - "in_dynamic_universe", - ctx.dynamic_universe_contains(&stock.symbol), - ); - scope.push("is_subscribed", ctx.is_subscribed(&stock.symbol)); + .to_string() + }); + scope.push_lazy("latest_symbol_open_order_unfilled_qty", || { + ctx.latest_symbol_open_order_unfilled_quantity(&stock.symbol) as i64 + }); + scope.push_lazy("in_dynamic_universe", || { + ctx.dynamic_universe_contains(&stock.symbol) + }); + scope.push_lazy("is_subscribed", || ctx.is_subscribed(&stock.symbol)); scope.push("stock_ma_short", stock.stock_ma_short); scope.push("stock_ma_mid", stock.stock_ma_mid); scope.push("stock_ma_long", stock.stock_ma_long); @@ -5077,7 +5071,7 @@ impl PlatformExprStrategy { } } if let Some(position) = position { - scope.push("order_book_id", position.order_book_id.clone()); + scope.push_lazy("order_book_id", || position.order_book_id.clone()); scope.push("avg_cost", position.avg_cost); scope.push("avg_price", position.avg_price); scope.push("current_price", position.current_price); @@ -5111,18 +5105,19 @@ impl PlatformExprStrategy { scope.push("trading_pnl", position.trading_pnl); scope.push("position_pnl", position.position_pnl); scope.push("dividend_receivable", position.dividend_receivable); - let available_sellable_qty = stock - .map(|stock| { - ctx.available_sellable_qty(&stock.symbol, position.sellable_qty as u32) - }) - .unwrap_or(position.sellable_qty.max(0) as u32); - scope.push("available_sellable_qty", available_sellable_qty as i64); - scope.push( - "reserved_open_sell_qty", + scope.push_lazy("available_sellable_qty", || { + stock + .map(|stock| { + ctx.available_sellable_qty(&stock.symbol, position.sellable_qty as u32) + as i64 + }) + .unwrap_or(position.sellable_qty.max(0)) + }); + scope.push_lazy("reserved_open_sell_qty", || { stock .map(|stock| ctx.symbol_open_sell_quantity(&stock.symbol) as i64) - .unwrap_or(0), - ); + .unwrap_or(0) + }); scope.push("profit_pct", position.holding_return * 100.0); } scope.into_inner() @@ -12833,9 +12828,11 @@ fn code_number_value(value: &str) -> i64 { #[cfg(test)] mod tests { + use std::cell::Cell; use std::collections::{BTreeMap, BTreeSet}; use std::sync::Arc; + use ahash::AHashSet; use chrono::{NaiveDate, NaiveTime}; use super::{ @@ -12844,9 +12841,9 @@ mod tests { PlatformExprStrategyConfig, PlatformPortfolioDrawdownControlConfig, PlatformPortfolioDrawdownController, PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformStopTakeReferencePriceMode, PlatformTradeAction, PlatformUniverseActionKind, - RuntimeHelperResolution, SelectionRiskDeferral, StockFilterQuoteUsage, StockRollingField, - StockSnapshotFieldRequirements, framework_stock_rolling_factor_requirement, - scheduled_position_exposure, + RuntimeHelperResolution, SelectionRiskDeferral, SelectiveExpressionScope, + StockFilterQuoteUsage, StockRollingField, StockSnapshotFieldRequirements, + framework_stock_rolling_factor_requirement, scheduled_position_exposure, }; use crate::{ AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction, @@ -12862,6 +12859,25 @@ mod tests { NaiveDate::from_ymd_opt(year, month, day).expect("valid date") } + #[test] + fn selective_expression_scope_does_not_build_unreferenced_values() { + let required = AHashSet::from_iter(["needed".to_string()]); + let builds = Cell::new(0); + let mut scope = SelectiveExpressionScope::new(&required); + + scope.push_lazy("unused", || { + builds.set(builds.get() + 1); + "unused".to_string() + }); + scope.push_lazy("needed", || { + builds.set(builds.get() + 1); + "value".to_string() + }); + + assert_eq!(builds.get(), 1); + assert!(scope.into_inner().contains("needed")); + } + #[test] fn stock_state_cache_resets_before_reusing_compact_keys_on_another_date() { let dates = [d(2025, 1, 2), d(2025, 1, 3)];