支持动态因子缺值安全表达式

This commit is contained in:
boris
2026-06-27 07:53:51 +08:00
parent 275dde61ae
commit 6067adc120
+87 -2
View File
@@ -556,6 +556,7 @@ pub struct PlatformExprStrategy {
selection_quote_usage: StockFilterQuoteUsage, selection_quote_usage: StockFilterQuoteUsage,
stock_rolling_requirements: StockRollingRequirements, stock_rolling_requirements: StockRollingRequirements,
stock_extra_factors_required: bool, stock_extra_factors_required: bool,
stock_extra_factor_identifiers: BTreeSet<String>,
stock_text_factors_required: bool, stock_text_factors_required: bool,
stock_state_cache_date: RefCell<Option<NaiveDate>>, stock_state_cache_date: RefCell<Option<NaiveDate>>,
stock_state_cache: RefCell< stock_state_cache: RefCell<
@@ -657,6 +658,11 @@ impl PlatformExprStrategy {
&normalized_stock_filter_expr, &normalized_stock_filter_expr,
&prelude_declared_identifiers, &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( let stock_text_factors_required = Self::stock_text_factors_required_for_config(
&config, &config,
&normalized_stock_filter_expr, &normalized_stock_filter_expr,
@@ -681,6 +687,7 @@ impl PlatformExprStrategy {
selection_quote_usage, selection_quote_usage,
stock_rolling_requirements, stock_rolling_requirements,
stock_extra_factors_required, stock_extra_factors_required,
stock_extra_factor_identifiers,
stock_text_factors_required, stock_text_factors_required,
stock_state_cache_date: RefCell::new(None), stock_state_cache_date: RefCell::new(None),
stock_state_cache: RefCell::new(HashMap::new()), stock_state_cache: RefCell::new(HashMap::new()),
@@ -3196,7 +3203,9 @@ impl PlatformExprStrategy {
for identifier in Self::extract_identifier_candidates(&expanded_expr) { for identifier in Self::extract_identifier_candidates(&expanded_expr) {
if Self::is_reserved_scope_name(identifier.as_str()) if Self::is_reserved_scope_name(identifier.as_str())
|| self.prelude_declared_identifiers.contains(&identifier) || 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)) && !day.available_text_factor_names.contains(&identifier))
{ {
continue; continue;
@@ -3204,7 +3213,7 @@ impl PlatformExprStrategy {
if let Some(value) = item.extra_text_factors.get(&identifier) { if let Some(value) = item.extra_text_factors.get(&identifier) {
scope.push_dynamic(identifier, Dynamic::from(value.clone())); scope.push_dynamic(identifier, Dynamic::from(value.clone()));
} else { } 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)); 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<String>,
) -> BTreeSet<String> {
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<String>,
expr: &str,
prelude_declared_identifiers: &BTreeSet<String>,
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( fn stock_text_factors_required_for_config(
config: &PlatformExprStrategyConfig, config: &PlatformExprStrategyConfig,
normalized_stock_filter_expr: &str, normalized_stock_filter_expr: &str,
@@ -8213,6 +8285,19 @@ mod tests {
assert!((strategy.buy_commission(1_000.0) - 5.0).abs() < 1e-9); 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] #[test]
fn platform_expr_rewrites_prelude_assignment_ternary() { fn platform_expr_rewrites_prelude_assignment_ternary() {
let prelude = "let cap_low = sig_close <= 0 ? 7 : max(5, min(cap_low_raw, 70));"; let prelude = "let cap_low = sig_close <= 0 ? 7 : max(5, min(cap_low_raw, 70));";