perf: build expression scope values lazily

This commit is contained in:
boris
2026-09-05 03:56:51 +08:00
parent 05085b961b
commit 1b449287fd
+132 -116
View File
@@ -917,6 +917,17 @@ impl<'a> SelectiveExpressionScope<'a> {
self
}
fn push_lazy<T, F>(&mut self, name: &str, build: F) -> &mut Self
where
T: Into<Dynamic>,
F: FnOnce() -> T,
{
if self.requires(name) {
self.inner.push_dynamic(name.to_string(), build().into());
}
self
}
fn push_required<T: Into<Dynamic>>(&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)];