统一策略表达式执行与默认配置
This commit is contained in:
@@ -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)];
|
||||||
|
|||||||
@@ -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!({
|
||||||
|
|||||||
Reference in New Issue
Block a user