Expose richer factor maps to platform expressions

This commit is contained in:
boris
2026-04-21 23:43:49 -07:00
parent d739e141fd
commit 44bcdef920

View File

@@ -1,7 +1,7 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime}; use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime};
use rhai::{Dynamic, Engine, Scope}; use rhai::{Dynamic, Engine, Map, Scope};
use crate::data::{DailyMarketSnapshot, EligibleUniverseSnapshot, PriceField}; use crate::data::{DailyMarketSnapshot, EligibleUniverseSnapshot, PriceField};
use crate::engine::BacktestError; use crate::engine::BacktestError;
@@ -114,9 +114,11 @@ struct DayExpressionState {
benchmark_ma5: f64, benchmark_ma5: f64,
benchmark_ma10: f64, benchmark_ma10: f64,
benchmark_ma20: f64, benchmark_ma20: f64,
benchmark_ma30: f64,
signal_ma5: f64, signal_ma5: f64,
signal_ma10: f64, signal_ma10: f64,
signal_ma20: f64, signal_ma20: f64,
signal_ma30: f64,
cash: f64, cash: f64,
market_value: f64, market_value: f64,
total_equity: f64, total_equity: f64,
@@ -165,6 +167,7 @@ struct StockExpressionState {
stock_ma5: f64, stock_ma5: f64,
stock_ma10: f64, stock_ma10: f64,
stock_ma20: f64, stock_ma20: f64,
stock_ma30: f64,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -575,6 +578,10 @@ impl PlatformExprStrategy {
.data .data
.market_decision_close_moving_average(date, &self.config.signal_symbol, 20) .market_decision_close_moving_average(date, &self.config.signal_symbol, 20)
.unwrap_or(benchmark_ma10); .unwrap_or(benchmark_ma10);
let benchmark_ma30 = ctx
.data
.market_decision_close_moving_average(date, &self.config.signal_symbol, 30)
.unwrap_or(benchmark_ma20);
let cash = ctx.portfolio.cash(); let cash = ctx.portfolio.cash();
let market_value = ctx.portfolio.positions().values().map(|position| position.market_value()).sum::<f64>(); let market_value = ctx.portfolio.positions().values().map(|position| position.market_value()).sum::<f64>();
let total_equity = cash + market_value; let total_equity = cash + market_value;
@@ -590,9 +597,11 @@ impl PlatformExprStrategy {
benchmark_ma5, benchmark_ma5,
benchmark_ma10, benchmark_ma10,
benchmark_ma20, benchmark_ma20,
benchmark_ma30,
signal_ma5: benchmark_ma5, signal_ma5: benchmark_ma5,
signal_ma10: benchmark_ma10, signal_ma10: benchmark_ma10,
signal_ma20: benchmark_ma20, signal_ma20: benchmark_ma20,
signal_ma30: benchmark_ma30,
cash, cash,
market_value, market_value,
total_equity, total_equity,
@@ -646,6 +655,10 @@ impl PlatformExprStrategy {
.data .data
.market_decision_close_moving_average(date, symbol, 20) .market_decision_close_moving_average(date, symbol, 20)
.unwrap_or(stock_ma_long); .unwrap_or(stock_ma_long);
let stock_ma30 = ctx
.data
.market_decision_close_moving_average(date, symbol, 30)
.unwrap_or(stock_ma20);
Ok(StockExpressionState { Ok(StockExpressionState {
symbol: symbol.to_string(), symbol: symbol.to_string(),
@@ -676,6 +689,7 @@ impl PlatformExprStrategy {
stock_ma5, stock_ma5,
stock_ma10, stock_ma10,
stock_ma20, stock_ma20,
stock_ma30,
}) })
} }
@@ -694,6 +708,7 @@ impl PlatformExprStrategy {
scope.push("benchmark_ma5", day.benchmark_ma5); scope.push("benchmark_ma5", day.benchmark_ma5);
scope.push("benchmark_ma10", day.benchmark_ma10); scope.push("benchmark_ma10", day.benchmark_ma10);
scope.push("benchmark_ma20", day.benchmark_ma20); scope.push("benchmark_ma20", day.benchmark_ma20);
scope.push("benchmark_ma30", day.benchmark_ma30);
scope.push("benchmark_ma_short", day.benchmark_ma_short); scope.push("benchmark_ma_short", day.benchmark_ma_short);
scope.push("benchmark_ma_long", day.benchmark_ma_long); scope.push("benchmark_ma_long", day.benchmark_ma_long);
scope.push("cash", day.cash); scope.push("cash", day.cash);
@@ -713,6 +728,37 @@ impl PlatformExprStrategy {
scope.push("weekday", day.weekday); scope.push("weekday", day.weekday);
scope.push("is_month_start", day.is_month_start); scope.push("is_month_start", day.is_month_start);
scope.push("is_month_end", day.is_month_end); scope.push("is_month_end", day.is_month_end);
scope.push("signal_ma30", day.signal_ma30);
let mut day_factors = Map::new();
day_factors.insert("signal_close".into(), Dynamic::from(day.signal_close));
day_factors.insert("benchmark_close".into(), Dynamic::from(day.benchmark_close));
day_factors.insert("signal_ma5".into(), Dynamic::from(day.signal_ma5));
day_factors.insert("signal_ma10".into(), Dynamic::from(day.signal_ma10));
day_factors.insert("signal_ma20".into(), Dynamic::from(day.signal_ma20));
day_factors.insert("signal_ma30".into(), Dynamic::from(day.signal_ma30));
day_factors.insert("benchmark_ma5".into(), Dynamic::from(day.benchmark_ma5));
day_factors.insert("benchmark_ma10".into(), Dynamic::from(day.benchmark_ma10));
day_factors.insert("benchmark_ma20".into(), Dynamic::from(day.benchmark_ma20));
day_factors.insert("benchmark_ma30".into(), Dynamic::from(day.benchmark_ma30));
day_factors.insert("benchmark_ma_short".into(), Dynamic::from(day.benchmark_ma_short));
day_factors.insert("benchmark_ma_long".into(), Dynamic::from(day.benchmark_ma_long));
day_factors.insert("cash".into(), Dynamic::from(day.cash));
day_factors.insert("market_value".into(), Dynamic::from(day.market_value));
day_factors.insert("total_equity".into(), Dynamic::from(day.total_equity));
day_factors.insert("current_exposure".into(), Dynamic::from(day.current_exposure));
day_factors.insert("position_count".into(), Dynamic::from(day.position_count));
day_factors.insert("max_positions".into(), Dynamic::from(day.max_positions));
day_factors.insert("refresh_rate".into(), Dynamic::from(day.refresh_rate));
day_factors.insert("year".into(), Dynamic::from(day.year));
day_factors.insert("month".into(), Dynamic::from(day.month));
day_factors.insert("quarter".into(), Dynamic::from(day.quarter));
day_factors.insert("day_of_month".into(), Dynamic::from(day.day_of_month));
day_factors.insert("day_of_year".into(), Dynamic::from(day.day_of_year));
day_factors.insert("week_of_year".into(), Dynamic::from(day.week_of_year));
day_factors.insert("weekday".into(), Dynamic::from(day.weekday));
day_factors.insert("is_month_start".into(), Dynamic::from(day.is_month_start));
day_factors.insert("is_month_end".into(), Dynamic::from(day.is_month_end));
scope.push("day_factors", day_factors);
if let Some(stock) = stock { if let Some(stock) = stock {
scope.push("symbol", stock.symbol.clone()); scope.push("symbol", stock.symbol.clone());
scope.push("market_cap", stock.market_cap); scope.push("market_cap", stock.market_cap);
@@ -743,6 +789,46 @@ impl PlatformExprStrategy {
scope.push("stock_ma5", stock.stock_ma5); scope.push("stock_ma5", stock.stock_ma5);
scope.push("stock_ma10", stock.stock_ma10); scope.push("stock_ma10", stock.stock_ma10);
scope.push("stock_ma20", stock.stock_ma20); scope.push("stock_ma20", stock.stock_ma20);
scope.push("stock_ma30", stock.stock_ma30);
scope.push("ma5", stock.stock_ma5);
scope.push("ma10", stock.stock_ma10);
scope.push("ma20", stock.stock_ma20);
scope.push("ma30", stock.stock_ma30);
let mut factors = Map::new();
factors.insert("symbol".into(), Dynamic::from(stock.symbol.clone()));
factors.insert("market_cap".into(), Dynamic::from(stock.market_cap));
factors.insert("free_float_cap".into(), Dynamic::from(stock.free_float_cap));
factors.insert("pe_ttm".into(), Dynamic::from(stock.pe_ttm));
factors.insert("turnover_ratio".into(), Dynamic::from(stock.turnover_ratio));
factors.insert(
"effective_turnover_ratio".into(),
Dynamic::from(stock.effective_turnover_ratio),
);
factors.insert("open".into(), Dynamic::from(stock.open));
factors.insert("close".into(), Dynamic::from(stock.close));
factors.insert("last_price".into(), Dynamic::from(stock.last));
factors.insert("prev_close".into(), Dynamic::from(stock.prev_close));
factors.insert("upper_limit".into(), Dynamic::from(stock.upper_limit));
factors.insert("lower_limit".into(), Dynamic::from(stock.lower_limit));
factors.insert("price_tick".into(), Dynamic::from(stock.price_tick));
factors.insert("round_lot".into(), Dynamic::from(stock.round_lot));
factors.insert("paused".into(), Dynamic::from(stock.paused));
factors.insert("is_st".into(), Dynamic::from(stock.is_st));
factors.insert("is_kcb".into(), Dynamic::from(stock.is_kcb));
factors.insert("is_one_yuan".into(), Dynamic::from(stock.is_one_yuan));
factors.insert("is_new_listing".into(), Dynamic::from(stock.is_new_listing));
factors.insert("allow_buy".into(), Dynamic::from(stock.allow_buy));
factors.insert("allow_sell".into(), Dynamic::from(stock.allow_sell));
factors.insert("listed_days".into(), Dynamic::from(stock.listed_days));
factors.insert("stock_ma5".into(), Dynamic::from(stock.stock_ma5));
factors.insert("stock_ma10".into(), Dynamic::from(stock.stock_ma10));
factors.insert("stock_ma20".into(), Dynamic::from(stock.stock_ma20));
factors.insert("stock_ma30".into(), Dynamic::from(stock.stock_ma30));
factors.insert("ma5".into(), Dynamic::from(stock.stock_ma5));
factors.insert("ma10".into(), Dynamic::from(stock.stock_ma10));
factors.insert("ma20".into(), Dynamic::from(stock.stock_ma20));
factors.insert("ma30".into(), Dynamic::from(stock.stock_ma30));
scope.push("factors", factors);
} }
if let Some(position) = position { if let Some(position) = position {
scope.push("avg_cost", position.avg_cost); scope.push("avg_cost", position.avg_cost);
@@ -750,6 +836,7 @@ impl PlatformExprStrategy {
scope.push("holding_return", position.holding_return); scope.push("holding_return", position.holding_return);
scope.push("quantity", position.quantity); scope.push("quantity", position.quantity);
scope.push("sellable_qty", position.sellable_qty); scope.push("sellable_qty", position.sellable_qty);
scope.push("profit_pct", position.holding_return * 100.0);
} }
scope scope
} }
@@ -944,9 +1031,16 @@ impl PlatformExprStrategy {
"price_tick" => Some(stock.price_tick), "price_tick" => Some(stock.price_tick),
"round_lot" => Some(stock.round_lot as f64), "round_lot" => Some(stock.round_lot as f64),
"listed_days" => Some(stock.listed_days as f64), "listed_days" => Some(stock.listed_days as f64),
"stock_ma_short" | "stock_ma5" => Some(stock.stock_ma_short), "stock_ma_short" => Some(stock.stock_ma_short),
"stock_ma_mid" | "stock_ma10" => Some(stock.stock_ma_mid), "stock_ma5" => Some(stock.stock_ma5),
"stock_ma_long" | "stock_ma20" => Some(stock.stock_ma_long), "stock_ma_mid" => Some(stock.stock_ma_mid),
"stock_ma10" => Some(stock.stock_ma10),
"stock_ma_long" => Some(stock.stock_ma_long),
"stock_ma20" => Some(stock.stock_ma20),
"stock_ma30" | "ma30" => Some(stock.stock_ma30),
"ma5" => Some(stock.stock_ma5),
"ma10" => Some(stock.stock_ma10),
"ma20" => Some(stock.stock_ma20),
"allow_buy" => Some(if stock.allow_buy { 1.0 } else { 0.0 }), "allow_buy" => Some(if stock.allow_buy { 1.0 } else { 0.0 }),
"allow_sell" => Some(if stock.allow_sell { 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 }), "paused" => Some(if stock.paused { 1.0 } else { 0.0 }),