按需保留股票额外因子状态

This commit is contained in:
boris
2026-06-21 01:13:22 +08:00
parent 0714d1f77b
commit bb87d69224
+180 -1
View File
@@ -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<Option<NaiveDate>>,
stock_state_cache:
RefCell<HashMap<(NaiveDate, NaiveDate, String, Option<NaiveTime>), 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<String>,
) -> 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<String>,
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)<rolling_mean(\"volume\",100)*max_volume_ratio";
filter_body == base_microcap_filter
|| filter_body
.strip_prefix(base_microcap_filter)
.is_some_and(|tail| {
matches!(
tail,
"&&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
}