统一策略表达式执行与默认配置

This commit is contained in:
boris
2026-08-24 09:28:33 +08:00
parent 589f94e5b2
commit 61a4172bd4
2 changed files with 163 additions and 243 deletions
+126 -242
View File
@@ -401,30 +401,23 @@ pub struct PlatformExprStrategyConfig {
} }
impl PlatformExprStrategyConfig { impl PlatformExprStrategyConfig {
pub fn microcap_rotation() -> Self { pub fn generic() -> Self {
Self { Self {
strategy_name: "microcap_rotation".to_string(), strategy_name: "platform-expression".to_string(),
market: "CN_A".to_string(), market: "CN_A".to_string(),
benchmark_symbol: "000852.SH".to_string(), benchmark_symbol: String::new(),
signal_symbol: "000001.SH".to_string(), signal_symbol: String::new(),
refresh_rate: 15, refresh_rate: 1,
refresh_rate_expr: String::new(), refresh_rate_expr: String::new(),
max_positions: 40, max_positions: 1,
prelude: r#"let stocknum = 40; prelude: String::new(),
let ma_ratio = 1.0001;
fn band_low(index_close) {
round((index_close - 2000) * 4 / 500 + 7)
}"#
.to_string(),
universe_exclude: Vec::new(), universe_exclude: Vec::new(),
market_cap_field: "market_cap".to_string(), market_cap_field: "market_cap".to_string(),
market_cap_lower_expr: "band_low(signal_close)".to_string(), market_cap_lower_expr: "0.0".to_string(),
market_cap_upper_expr: "band_low(signal_close) + 10".to_string(), market_cap_upper_expr: "1.0e30".to_string(),
selection_limit_expr: "stocknum".to_string(), selection_limit_expr: "1".to_string(),
selection_candidate_limit_expr: String::new(), selection_candidate_limit_expr: String::new(),
stock_filter_expr: stock_filter_expr: String::new(),
"stock_ma_short > stock_ma_mid * ma_ratio && stock_ma_mid > stock_ma_long"
.to_string(),
buy_scale_expr: "1.0".to_string(), buy_scale_expr: "1.0".to_string(),
exposure_expr: "1.0".to_string(), exposure_expr: "1.0".to_string(),
portfolio_drawdown_control: None, portfolio_drawdown_control: None,
@@ -434,17 +427,17 @@ fn band_low(index_close) {
rank_by: "market_cap".to_string(), rank_by: "market_cap".to_string(),
rank_expr: String::new(), rank_expr: String::new(),
rank_desc: false, rank_desc: false,
benchmark_short_ma_days: 5, benchmark_short_ma_days: 1,
benchmark_long_ma_days: 10, benchmark_long_ma_days: 1,
stock_short_ma_days: 5, stock_short_ma_days: 1,
stock_mid_ma_days: 10, stock_mid_ma_days: 1,
stock_long_ma_days: 20, stock_long_ma_days: 1,
skip_month_day_ranges: Vec::new(), skip_month_day_ranges: Vec::new(),
rebalance_schedule: None, rebalance_schedule: None,
signal_rebalance_dates: BTreeSet::new(), signal_rebalance_dates: BTreeSet::new(),
rotation_enabled: true, rotation_enabled: true,
daily_top_up_enabled: false, daily_top_up_enabled: false,
daily_position_target_adjust_enabled: true, daily_position_target_adjust_enabled: false,
target_portfolio_daily_enabled: false, target_portfolio_daily_enabled: false,
rebalance_existing_positions: false, rebalance_existing_positions: false,
hold_until_exit_enabled: false, hold_until_exit_enabled: false,
@@ -478,6 +471,33 @@ fn band_low(index_close) {
} }
} }
pub fn microcap_rotation() -> Self {
let mut config = Self::generic();
config.strategy_name = "microcap_rotation".to_string();
config.benchmark_symbol = "000852.SH".to_string();
config.signal_symbol = "000001.SH".to_string();
config.refresh_rate = 15;
config.max_positions = 40;
config.prelude = r#"let stocknum = 40;
let ma_ratio = 1.0001;
fn band_low(index_close) {
round((index_close - 2000) * 4 / 500 + 7)
}"#
.to_string();
config.market_cap_lower_expr = "band_low(signal_close)".to_string();
config.market_cap_upper_expr = "band_low(signal_close) + 10".to_string();
config.selection_limit_expr = "stocknum".to_string();
config.stock_filter_expr =
"stock_ma_short > stock_ma_mid * ma_ratio && stock_ma_mid > stock_ma_long".to_string();
config.benchmark_short_ma_days = 5;
config.benchmark_long_ma_days = 10;
config.stock_short_ma_days = 5;
config.stock_mid_ma_days = 10;
config.stock_long_ma_days = 20;
config.daily_position_target_adjust_enabled = true;
config
}
fn in_skip_window(&self, date: NaiveDate) -> bool { fn in_skip_window(&self, date: NaiveDate) -> bool {
let year = date.year() as u32; let year = date.year() as u32;
let month = date.month(); let month = date.month();
@@ -791,6 +811,7 @@ impl<'a> SelectiveExpressionScope<'a> {
struct ExpressionEvalPlan { struct ExpressionEvalPlan {
identifiers: BTreeSet<String>, identifiers: BTreeSet<String>,
runtime_template: Result<RuntimeExpressionTemplate, String>, runtime_template: Result<RuntimeExpressionTemplate, String>,
requires_prelude: bool,
} }
struct RuntimeExpressionTemplate { struct RuntimeExpressionTemplate {
@@ -832,8 +853,6 @@ pub struct PlatformExprStrategy {
prelude_runtime_template: Result<RuntimeExpressionTemplate, String>, prelude_runtime_template: Result<RuntimeExpressionTemplate, String>,
prelude_identifier_candidates: BTreeSet<String>, prelude_identifier_candidates: BTreeSet<String>,
prelude_declared_identifiers: BTreeSet<String>, prelude_declared_identifiers: BTreeSet<String>,
prelude_numeric_constants: HashMap<String, f64>,
compact_stock_filter_expr: String,
stock_filter_quote_usage: StockFilterQuoteUsage, stock_filter_quote_usage: StockFilterQuoteUsage,
selection_quote_usage: StockFilterQuoteUsage, selection_quote_usage: StockFilterQuoteUsage,
stock_rolling_requirements: StockRollingRequirements, stock_rolling_requirements: StockRollingRequirements,
@@ -1106,25 +1125,16 @@ impl PlatformExprStrategy {
let prelude_identifier_candidates = let prelude_identifier_candidates =
Self::extract_identifier_candidates(&normalized_prelude); Self::extract_identifier_candidates(&normalized_prelude);
let prelude_declared_identifiers = Self::declared_prelude_identifiers(&config.prelude); let prelude_declared_identifiers = Self::declared_prelude_identifiers(&config.prelude);
let prelude_numeric_constants = Self::parse_prelude_numeric_constants(&normalized_prelude);
let normalized_stock_filter_expr = Self::normalize_expr(&config.stock_filter_expr); let normalized_stock_filter_expr = Self::normalize_expr(&config.stock_filter_expr);
let compact_stock_filter_expr = Self::compact_expr(&normalized_stock_filter_expr);
let stock_filter_quote_usage = let stock_filter_quote_usage =
Self::stock_filter_quote_usage_for_expr(&normalized_stock_filter_expr); Self::stock_filter_quote_usage_for_expr(&normalized_stock_filter_expr);
let selection_quote_usage = let selection_quote_usage =
Self::selection_quote_usage_for_config(&config, &normalized_stock_filter_expr); Self::selection_quote_usage_for_config(&config, &normalized_stock_filter_expr);
let stock_rolling_requirements = let stock_rolling_requirements = Self::stock_rolling_requirements_for_config(&config);
Self::stock_rolling_requirements_for_config(&config, &normalized_stock_filter_expr); let stock_extra_factors_required =
let stock_extra_factors_required = Self::stock_extra_factors_required_for_config( Self::stock_extra_factors_required_for_config(&config, &prelude_declared_identifiers);
&config, let stock_extra_factor_identifiers =
&normalized_stock_filter_expr, Self::stock_extra_factor_identifiers_for_config(&config, &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,
@@ -1154,8 +1164,6 @@ impl PlatformExprStrategy {
prelude_runtime_template, prelude_runtime_template,
prelude_identifier_candidates, prelude_identifier_candidates,
prelude_declared_identifiers, prelude_declared_identifiers,
prelude_numeric_constants,
compact_stock_filter_expr,
stock_filter_quote_usage, stock_filter_quote_usage,
selection_quote_usage, selection_quote_usage,
stock_rolling_requirements, stock_rolling_requirements,
@@ -3879,12 +3887,12 @@ impl PlatformExprStrategy {
stock: Option<&StockExpressionState>, stock: Option<&StockExpressionState>,
position: Option<&PositionExpressionState>, position: Option<&PositionExpressionState>,
identifiers: &BTreeSet<String>, identifiers: &BTreeSet<String>,
prelude_identifiers: &BTreeSet<String>,
include_day_factors: bool, include_day_factors: bool,
include_factors_map: bool, include_factors_map: bool,
include_process_event_counts: bool, include_process_event_counts: bool,
) -> Scope<'static> { ) -> Scope<'static> {
let mut scope = let mut scope = SelectiveExpressionScope::new(identifiers, prelude_identifiers);
SelectiveExpressionScope::new(identifiers, &self.prelude_identifier_candidates);
let trade_date = day.date.format("%Y-%m-%d").to_string(); let trade_date = day.date.format("%Y-%m-%d").to_string();
let decision_date = ctx.decision_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(); let execution_date = ctx.execution_date.format("%Y-%m-%d").to_string();
@@ -4573,7 +4581,12 @@ impl PlatformExprStrategy {
) -> Result<Dynamic, BacktestError> { ) -> Result<Dynamic, BacktestError> {
let expression_plan = self.expression_eval_plan(expr); let expression_plan = self.expression_eval_plan(expr);
let normalized_identifiers = &expression_plan.identifiers; let normalized_identifiers = &expression_plan.identifiers;
let prelude_identifiers = &self.prelude_identifier_candidates; let empty_prelude_identifiers = BTreeSet::new();
let prelude_identifiers = if expression_plan.requires_prelude {
&self.prelude_identifier_candidates
} else {
&empty_prelude_identifiers
};
let include_day_factors = normalized_identifiers.contains("day_factors") let include_day_factors = normalized_identifiers.contains("day_factors")
|| normalized_identifiers.contains("day_factor") || normalized_identifiers.contains("day_factor")
|| prelude_identifiers.contains("day_factors"); || prelude_identifiers.contains("day_factors");
@@ -4590,17 +4603,22 @@ impl PlatformExprStrategy {
stock, stock,
position, position,
&normalized_identifiers, &normalized_identifiers,
prelude_identifiers,
include_day_factors, include_day_factors,
include_factors_map, include_factors_map,
include_process_event_counts, include_process_event_counts,
); );
let expanded_prelude = self.expand_runtime_helper_template( let expanded_prelude = if expression_plan.requires_prelude {
ctx, self.expand_runtime_helper_template(
day, ctx,
stock, day,
&self.prelude_runtime_template, stock,
&mut scope, &self.prelude_runtime_template,
)?; &mut scope,
)?
} else {
String::new()
};
let expanded_expr = self.expand_runtime_helper_template( let expanded_expr = self.expand_runtime_helper_template(
ctx, ctx,
day, day,
@@ -4650,8 +4668,12 @@ impl PlatformExprStrategy {
return plan; return plan;
} }
let normalized = Self::normalize_expr(expr); let normalized = Self::normalize_expr(expr);
let identifiers = Self::extract_identifier_candidates(&normalized);
let plan = Arc::new(ExpressionEvalPlan { let plan = Arc::new(ExpressionEvalPlan {
identifiers: Self::extract_identifier_candidates(&normalized), requires_prelude: identifiers
.iter()
.any(|identifier| self.prelude_declared_identifiers.contains(identifier)),
identifiers,
runtime_template: Self::compile_runtime_helper_template(&normalized), runtime_template: Self::compile_runtime_helper_template(&normalized),
}); });
self.expression_plan_cache self.expression_plan_cache
@@ -4700,28 +4722,6 @@ impl PlatformExprStrategy {
output output
} }
fn prelude_numeric_constant(&self, name: &str) -> Option<f64> {
self.prelude_numeric_constants.get(name).copied()
}
fn parse_prelude_numeric_constants(normalized_prelude: &str) -> HashMap<String, f64> {
let mut constants = HashMap::new();
for line in normalized_prelude.lines() {
let trimmed = line.trim();
let Some(body) = trimmed.strip_prefix("let ") else {
continue;
};
let Some((lhs, rhs)) = body.split_once('=') else {
continue;
};
let rhs = rhs.trim().trim_end_matches(';').trim();
if let Ok(value) = rhs.parse::<f64>() {
constants.insert(lhs.trim().to_string(), value);
}
}
constants
}
fn normalize_prelude_for_eval(prelude: &str) -> String { fn normalize_prelude_for_eval(prelude: &str) -> String {
prelude prelude
.lines() .lines()
@@ -6101,13 +6101,10 @@ impl PlatformExprStrategy {
.lines() .lines()
.filter_map(|line| { .filter_map(|line| {
let trimmed = line.trim_start(); let trimmed = line.trim_start();
let body = if let Some(rest) = trimmed.strip_prefix("let ") { let body = trimmed
rest .strip_prefix("let ")
} else if let Some(rest) = trimmed.strip_prefix("fn ") { .or_else(|| trimmed.strip_prefix("const "))
rest .or_else(|| trimmed.strip_prefix("fn "))?;
} else {
return None;
};
let identifier: String = body let identifier: String = body
.chars() .chars()
.take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_') .take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_')
@@ -7611,9 +7608,6 @@ impl PlatformExprStrategy {
if self.config.stock_filter_expr.trim().is_empty() { if self.config.stock_filter_expr.trim().is_empty() {
return Ok(true); return Ok(true);
} }
if let Some(value) = self.fast_stock_passes_expr(ctx, day, stock) {
return Ok(value);
}
match self.eval_bool(ctx, &self.config.stock_filter_expr, day, Some(stock), None) { match self.eval_bool(ctx, &self.config.stock_filter_expr, day, Some(stock), None) {
Ok(value) => Ok(value), Ok(value) => Ok(value),
Err(error) if Self::is_missing_rolling_mean_error(&error) => Ok(false), Err(error) if Self::is_missing_rolling_mean_error(&error) => Ok(false),
@@ -7621,79 +7615,6 @@ impl PlatformExprStrategy {
} }
} }
fn fast_stock_passes_expr(
&self,
_ctx: &StrategyContext<'_>,
_day: &DayExpressionState,
stock: &StockExpressionState,
) -> Option<bool> {
let compact = self.compact_stock_filter_expr.as_str();
let ma_ratio = self.prelude_numeric_constant("ma_ratio").unwrap_or(1.0);
if compact == "stock_ma_short>stock_ma_mid*ma_ratio&&stock_ma_mid>stock_ma_long" {
return Some(
stock.stock_ma_short.is_finite()
&& stock.stock_ma_mid.is_finite()
&& stock.stock_ma_long.is_finite()
&& stock.stock_ma_short > stock.stock_ma_mid * ma_ratio
&& stock.stock_ma_mid > stock.stock_ma_long,
);
}
let mut filter_body = compact;
let requires_min_listed_days =
if let Some(rest) = filter_body.strip_prefix("listed_days>=min_listed_days&&") {
filter_body = rest;
true
} else {
false
};
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";
let requires_positive_volume = if filter_body == base_microcap_filter {
false
} else if 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"
)
})
{
true
} else {
return None;
};
let listed_days_pass = if requires_min_listed_days {
let min_listed_days = self
.prelude_numeric_constant("min_listed_days")
.unwrap_or(0.0);
(stock.listed_days as f64) >= min_listed_days
} else {
true
};
let max_volume_ratio = self
.prelude_numeric_constant("max_volume_ratio")
.unwrap_or(1.0);
let volume_ma100 = stock.stock_volume_ma100;
let positive_volume_pass =
!requires_positive_volume || (stock.stock_volume_ma5 > 0.0 && volume_ma100 > 0.0);
Some(
listed_days_pass
&& stock.stock_ma5.is_finite()
&& stock.stock_ma10.is_finite()
&& stock.stock_ma30.is_finite()
&& stock.stock_volume_ma5.is_finite()
&& volume_ma100.is_finite()
&& positive_volume_pass
&& stock.stock_ma5 > stock.stock_ma10 * ma_ratio
&& stock.stock_ma10 > stock.stock_ma30 * ma_ratio
&& stock.stock_volume_ma5 < volume_ma100 * max_volume_ratio,
)
}
fn field_value(&self, row: &EligibleUniverseSnapshot) -> f64 { fn field_value(&self, row: &EligibleUniverseSnapshot) -> f64 {
match self.config.market_cap_field.as_str() { match self.config.market_cap_field.as_str() {
"market_cap_bn" => row.market_cap_bn, "market_cap_bn" => row.market_cap_bn,
@@ -8397,7 +8318,6 @@ impl PlatformExprStrategy {
fn stock_rolling_requirements_for_config( fn stock_rolling_requirements_for_config(
config: &PlatformExprStrategyConfig, config: &PlatformExprStrategyConfig,
normalized_stock_filter_expr: &str,
) -> StockRollingRequirements { ) -> StockRollingRequirements {
let mut requirements = StockRollingRequirements::default(); let mut requirements = StockRollingRequirements::default();
let expressions = [ let expressions = [
@@ -8418,17 +8338,11 @@ impl PlatformExprStrategy {
Self::require_stock_rollings_for_identifiers(&mut requirements, config, &normalized); Self::require_stock_rollings_for_identifiers(&mut requirements, config, &normalized);
Self::require_stock_rollings_for_helper_calls(&mut requirements, &normalized); Self::require_stock_rollings_for_helper_calls(&mut requirements, &normalized);
} }
Self::require_stock_rollings_for_fast_filter(
&mut requirements,
config,
normalized_stock_filter_expr,
);
requirements requirements
} }
fn stock_extra_factors_required_for_config( fn stock_extra_factors_required_for_config(
config: &PlatformExprStrategyConfig, config: &PlatformExprStrategyConfig,
normalized_stock_filter_expr: &str,
prelude_declared_identifiers: &BTreeSet<String>, prelude_declared_identifiers: &BTreeSet<String>,
) -> bool { ) -> bool {
if !config.explicit_actions.is_empty() { if !config.explicit_actions.is_empty() {
@@ -8439,14 +8353,10 @@ impl PlatformExprStrategy {
{ {
return true; return true;
} }
let stock_filter_has_fast_path = if Self::expr_requires_stock_extra_factors(
Self::stock_filter_fast_path_supported(normalized_stock_filter_expr); &config.stock_filter_expr,
if !stock_filter_has_fast_path prelude_declared_identifiers,
&& Self::expr_requires_stock_extra_factors( ) {
&config.stock_filter_expr,
prelude_declared_identifiers,
)
{
return true; return true;
} }
[ [
@@ -8461,7 +8371,6 @@ impl PlatformExprStrategy {
fn stock_extra_factor_identifiers_for_config( fn stock_extra_factor_identifiers_for_config(
config: &PlatformExprStrategyConfig, config: &PlatformExprStrategyConfig,
normalized_stock_filter_expr: &str,
prelude_declared_identifiers: &BTreeSet<String>, prelude_declared_identifiers: &BTreeSet<String>,
) -> BTreeSet<String> { ) -> BTreeSet<String> {
let mut identifiers = BTreeSet::new(); let mut identifiers = BTreeSet::new();
@@ -8471,15 +8380,11 @@ impl PlatformExprStrategy {
if Self::stock_field_may_use_extra_factors(&config.rank_by) { if Self::stock_field_may_use_extra_factors(&config.rank_by) {
identifiers.insert(config.rank_by.trim().to_string()); identifiers.insert(config.rank_by.trim().to_string());
} }
let stock_filter_has_fast_path = Self::collect_stock_extra_factor_identifiers(
Self::stock_filter_fast_path_supported(normalized_stock_filter_expr); &mut identifiers,
if !stock_filter_has_fast_path { &config.stock_filter_expr,
Self::collect_stock_extra_factor_identifiers( prelude_declared_identifiers,
&mut identifiers, );
&config.stock_filter_expr,
prelude_declared_identifiers,
);
}
for expr in [ for expr in [
config.buy_scale_expr.as_str(), config.buy_scale_expr.as_str(),
config.stop_loss_expr.as_str(), config.stop_loss_expr.as_str(),
@@ -8793,65 +8698,6 @@ impl PlatformExprStrategy {
.filter(|value| *value > 0) .filter(|value| *value > 0)
} }
fn require_stock_rollings_for_fast_filter(
requirements: &mut StockRollingRequirements,
config: &PlatformExprStrategyConfig,
normalized_stock_filter_expr: &str,
) {
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" {
requirements.require(StockRollingField::Close, config.stock_short_ma_days);
requirements.require(StockRollingField::Close, config.stock_mid_ma_days);
requirements.require(StockRollingField::Close, config.stock_long_ma_days);
return;
}
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";
let matches_microcap_fast_filter = 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"
)
});
if matches_microcap_fast_filter {
requirements.require(StockRollingField::Close, 5);
requirements.require(StockRollingField::Close, 10);
requirements.require(StockRollingField::Close, 30);
requirements.require(StockRollingField::Volume, 5);
requirements.require(StockRollingField::Volume, 100);
}
}
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"
)
})
}
#[cfg(test)] #[cfg(test)]
fn stock_filter_uses_intraday_quote_fields(&self) -> bool { fn stock_filter_uses_intraday_quote_fields(&self) -> bool {
self.stock_filter_quote_usage() != StockFilterQuoteUsage::DailyOnly self.stock_filter_quote_usage() != StockFilterQuoteUsage::DailyOnly
@@ -31333,6 +31179,44 @@ mod tests {
); );
} }
#[test]
fn expression_plan_only_executes_prelude_when_expression_depends_on_it() {
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.prelude = r#"
let stock_threshold = 10.0;
const constant_threshold = 11.0;
let unrelated_market_signal = rolling_mean_current("signal_close", 60);
fn passes_threshold(value) { value > stock_threshold }
"#
.to_string();
let strategy = PlatformExprStrategy::new(cfg);
assert!(
!strategy
.expression_eval_plan("close > 0 && !is_st")
.requires_prelude,
"stock-only expressions must not execute unrelated day-level prelude declarations"
);
assert!(
strategy
.expression_eval_plan("market_cap > stock_threshold")
.requires_prelude,
"direct prelude variable dependencies must retain the prelude"
);
assert!(
strategy
.expression_eval_plan("market_cap > constant_threshold")
.requires_prelude,
"const prelude dependencies must retain the prelude"
);
assert!(
strategy
.expression_eval_plan("passes_threshold(market_cap)")
.requires_prelude,
"user-defined prelude function dependencies must retain the prelude"
);
}
#[test] #[test]
fn ast_cache_reuses_rolling_helper_scripts_across_dates() { fn ast_cache_reuses_rolling_helper_scripts_across_dates() {
let dates = [d(2025, 2, 3), d(2025, 2, 4)]; let dates = [d(2025, 2, 3), d(2025, 2, 4)];
+37 -1
View File
@@ -1389,7 +1389,7 @@ pub fn platform_expr_config_from_spec(
signal_symbol: &str, signal_symbol: &str,
strategy_spec: Option<&StrategyRuntimeSpec>, strategy_spec: Option<&StrategyRuntimeSpec>,
) -> Result<PlatformExprStrategyConfig, String> { ) -> Result<PlatformExprStrategyConfig, String> {
let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); let mut cfg = PlatformExprStrategyConfig::generic();
cfg.strategy_name = strategy_id.to_string(); cfg.strategy_name = strategy_id.to_string();
if !signal_symbol.trim().is_empty() { if !signal_symbol.trim().is_empty() {
cfg.signal_symbol = signal_symbol.trim().to_string(); cfg.signal_symbol = signal_symbol.trim().to_string();
@@ -1620,6 +1620,17 @@ pub fn platform_expr_config_from_spec(
.filter(|value| !value.trim().is_empty()) .filter(|value| !value.trim().is_empty())
{ {
cfg.selection_limit_expr = expr.clone(); cfg.selection_limit_expr = expr.clone();
if let Ok(limit) = expr.trim().parse::<usize>()
&& limit > 0
&& spec
.engine_config
.as_ref()
.and_then(|engine| engine.rank_limit)
.filter(|value| *value > 0)
.is_none()
{
cfg.max_positions = limit;
}
} }
if let Some(expr) = selection if let Some(expr) = selection
.candidate_limit_expr .candidate_limit_expr
@@ -2491,6 +2502,31 @@ mod tests {
); );
} }
#[test]
fn runtime_expression_parser_does_not_inherit_microcap_template_defaults() {
let spec = serde_json::json!({
"strategyId": "generic_runtime_strategy",
"signalSymbol": "000300.SH",
"benchmark": { "instrumentId": "000300.SH" },
"runtimeExpressions": {
"selection": { "limitExpr": "7" },
"trading": { "rotationEnabled": true }
}
});
let cfg = platform_expr_config_from_value("", "", &spec).expect("generic config");
assert_eq!(cfg.strategy_name, "generic_runtime_strategy");
assert_eq!(cfg.max_positions, 7);
assert_eq!(cfg.selection_limit_expr, "7");
assert_eq!(cfg.market_cap_lower_expr, "0.0");
assert_eq!(cfg.market_cap_upper_expr, "1.0e30");
assert!(cfg.stock_filter_expr.is_empty());
assert!(cfg.prelude.is_empty());
assert_eq!(cfg.refresh_rate, 1);
assert!(!cfg.daily_position_target_adjust_enabled);
}
#[test] #[test]
fn engine_config_parses_weak_market_shrink_overweight_threshold() { fn engine_config_parses_weak_market_shrink_overweight_threshold() {
let spec = serde_json::json!({ let spec = serde_json::json!({