Expand platform DSL expression selection

This commit is contained in:
boris
2026-04-21 22:19:33 -07:00
parent 1ba3264990
commit 2100cd1995

View File

@@ -28,6 +28,7 @@ pub struct PlatformExprStrategyConfig {
pub stop_loss_expr: String, pub stop_loss_expr: String,
pub take_profit_expr: String, pub take_profit_expr: String,
pub rank_by: String, pub rank_by: String,
pub rank_expr: String,
pub rank_desc: bool, pub rank_desc: bool,
pub benchmark_short_ma_days: usize, pub benchmark_short_ma_days: usize,
pub benchmark_long_ma_days: usize, pub benchmark_long_ma_days: usize,
@@ -71,6 +72,7 @@ fn band_low(index_close) {
stop_loss_expr: "0.93".to_string(), stop_loss_expr: "0.93".to_string(),
take_profit_expr: "1.07".to_string(), take_profit_expr: "1.07".to_string(),
rank_by: "market_cap".to_string(), rank_by: "market_cap".to_string(),
rank_expr: String::new(),
rank_desc: false, rank_desc: false,
benchmark_short_ma_days: 5, benchmark_short_ma_days: 5,
benchmark_long_ma_days: 10, benchmark_long_ma_days: 10,
@@ -130,6 +132,7 @@ struct DayExpressionState {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct StockExpressionState { struct StockExpressionState {
symbol: String,
market_cap: f64, market_cap: f64,
free_float_cap: f64, free_float_cap: f64,
pe_ttm: f64, pe_ttm: f64,
@@ -626,6 +629,7 @@ impl PlatformExprStrategy {
.unwrap_or(stock_ma_long); .unwrap_or(stock_ma_long);
Ok(StockExpressionState { Ok(StockExpressionState {
symbol: symbol.to_string(),
market_cap: factor.market_cap_bn, market_cap: factor.market_cap_bn,
free_float_cap: factor.free_float_cap_bn, free_float_cap: factor.free_float_cap_bn,
pe_ttm: factor.pe_ttm, pe_ttm: factor.pe_ttm,
@@ -686,6 +690,7 @@ impl PlatformExprStrategy {
scope.push("day_of_month", day.day_of_month); scope.push("day_of_month", day.day_of_month);
scope.push("weekday", day.weekday); scope.push("weekday", day.weekday);
if let Some(stock) = stock { if let Some(stock) = stock {
scope.push("symbol", stock.symbol.clone());
scope.push("market_cap", stock.market_cap); scope.push("market_cap", stock.market_cap);
scope.push("free_float_cap", stock.free_float_cap); scope.push("free_float_cap", stock.free_float_cap);
scope.push("pe_ttm", stock.pe_ttm); scope.push("pe_ttm", stock.pe_ttm);
@@ -894,13 +899,66 @@ impl PlatformExprStrategy {
} }
} }
fn rank_value(&self, row: &EligibleUniverseSnapshot) -> f64 { fn stock_numeric_field_value(
match self.config.rank_by.as_str() { &self,
"free_float_cap" | "free_float_market_cap" => row.free_float_cap_bn, candidate: &EligibleUniverseSnapshot,
_ => row.market_cap_bn, stock: &StockExpressionState,
field: &str,
) -> Option<f64> {
match field {
"market_cap" => Some(stock.market_cap),
"free_float_cap" | "free_float_market_cap" => Some(stock.free_float_cap),
"pe_ttm" => Some(stock.pe_ttm),
"turnover_ratio" => Some(stock.turnover_ratio),
"effective_turnover_ratio" => Some(stock.effective_turnover_ratio),
"open" => Some(stock.open),
"close" => Some(stock.close),
"last" | "last_price" => Some(stock.last),
"prev_close" => Some(stock.prev_close),
"upper_limit" => Some(stock.upper_limit),
"lower_limit" => Some(stock.lower_limit),
"price_tick" => Some(stock.price_tick),
"round_lot" => Some(stock.round_lot as f64),
"listed_days" => Some(stock.listed_days as f64),
"stock_ma_short" | "stock_ma5" => Some(stock.stock_ma_short),
"stock_ma_mid" | "stock_ma10" => Some(stock.stock_ma_mid),
"stock_ma_long" | "stock_ma20" => Some(stock.stock_ma_long),
"allow_buy" => Some(if stock.allow_buy { 1.0 } else { 0.0 }),
"allow_sell" => Some(if stock.allow_sell { 1.0 } else { 0.0 }),
"paused" => Some(if stock.paused { 1.0 } else { 0.0 }),
"is_st" => Some(if stock.is_st { 1.0 } else { 0.0 }),
"is_kcb" => Some(if stock.is_kcb { 1.0 } else { 0.0 }),
"is_one_yuan" => Some(if stock.is_one_yuan { 1.0 } else { 0.0 }),
"is_new_listing" => Some(if stock.is_new_listing { 1.0 } else { 0.0 }),
"candidate_market_cap" => Some(candidate.market_cap_bn),
"candidate_free_float_cap" => Some(candidate.free_float_cap_bn),
_ => None,
} }
} }
fn selection_field_value(
&self,
candidate: &EligibleUniverseSnapshot,
stock: &StockExpressionState,
) -> f64 {
self.stock_numeric_field_value(candidate, stock, self.config.market_cap_field.as_str())
.unwrap_or_else(|| self.field_value(candidate))
}
fn rank_value(
&self,
day: &DayExpressionState,
candidate: &EligibleUniverseSnapshot,
stock: &StockExpressionState,
) -> Result<f64, BacktestError> {
if !self.config.rank_expr.trim().is_empty() {
return self.eval_float(&self.config.rank_expr, day, Some(stock), None);
}
Ok(self
.stock_numeric_field_value(candidate, stock, self.config.rank_by.as_str())
.unwrap_or_else(|| self.field_value(candidate)))
}
fn can_sell_position(&self, ctx: &StrategyContext<'_>, date: NaiveDate, symbol: &str) -> bool { fn can_sell_position(&self, ctx: &StrategyContext<'_>, date: NaiveDate, symbol: &str) -> bool {
let Some(position) = ctx.portfolio.position(symbol) else { let Some(position) = ctx.portfolio.position(symbol) else {
return false; return false;
@@ -974,18 +1032,20 @@ impl PlatformExprStrategy {
) -> Result<(Vec<String>, Vec<String>), BacktestError> { ) -> Result<(Vec<String>, Vec<String>), BacktestError> {
let universe = ctx.data.eligible_universe_on(date); let universe = ctx.data.eligible_universe_on(date);
let mut diagnostics = Vec::new(); let mut diagnostics = Vec::new();
let mut candidates = universe let mut candidates = Vec::new();
.iter() for candidate in universe.iter().cloned() {
.filter(|candidate| { let stock = self.stock_state(ctx, date, &candidate.symbol)?;
let field_value = self.field_value(candidate); let field_value = self.selection_field_value(&candidate, &stock);
field_value >= band_low && field_value <= band_high if field_value < band_low || field_value > band_high {
}) continue;
.cloned() }
.collect::<Vec<_>>(); let rank_value = self.rank_value(day, &candidate, &stock)?;
candidates.push((candidate, stock, rank_value));
}
candidates.sort_by(|lhs, rhs| { candidates.sort_by(|lhs, rhs| {
let lhs_value = self.rank_value(lhs); let lhs_value = lhs.2;
let rhs_value = self.rank_value(rhs); let rhs_value = rhs.2;
if self.config.rank_desc { let ordering = if self.config.rank_desc {
rhs_value rhs_value
.partial_cmp(&lhs_value) .partial_cmp(&lhs_value)
.unwrap_or(std::cmp::Ordering::Equal) .unwrap_or(std::cmp::Ordering::Equal)
@@ -993,12 +1053,16 @@ impl PlatformExprStrategy {
lhs_value lhs_value
.partial_cmp(&rhs_value) .partial_cmp(&rhs_value)
.unwrap_or(std::cmp::Ordering::Equal) .unwrap_or(std::cmp::Ordering::Equal)
};
if ordering == std::cmp::Ordering::Equal {
lhs.0.symbol.cmp(&rhs.0.symbol)
} else {
ordering
} }
}); });
let mut selected = Vec::new(); let mut selected = Vec::new();
for candidate in candidates { for (candidate, stock, _) in candidates {
let stock = self.stock_state(ctx, date, &candidate.symbol)?;
if let Some(reason) = self.buy_rejection_reason(ctx, date, &candidate.symbol, &stock)? { if let Some(reason) = self.buy_rejection_reason(ctx, date, &candidate.symbol, &stock)? {
if diagnostics.len() < 12 { if diagnostics.len() < 12 {
diagnostics.push(format!("{} rejected by {}", candidate.symbol, reason)); diagnostics.push(format!("{} rejected by {}", candidate.symbol, reason));