diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 00c72eb..59731f8 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; 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::engine::BacktestError; @@ -114,9 +114,11 @@ struct DayExpressionState { benchmark_ma5: f64, benchmark_ma10: f64, benchmark_ma20: f64, + benchmark_ma30: f64, signal_ma5: f64, signal_ma10: f64, signal_ma20: f64, + signal_ma30: f64, cash: f64, market_value: f64, total_equity: f64, @@ -165,6 +167,7 @@ struct StockExpressionState { stock_ma5: f64, stock_ma10: f64, stock_ma20: f64, + stock_ma30: f64, } #[derive(Debug, Clone)] @@ -575,6 +578,10 @@ impl PlatformExprStrategy { .data .market_decision_close_moving_average(date, &self.config.signal_symbol, 20) .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 market_value = ctx.portfolio.positions().values().map(|position| position.market_value()).sum::(); let total_equity = cash + market_value; @@ -590,9 +597,11 @@ impl PlatformExprStrategy { benchmark_ma5, benchmark_ma10, benchmark_ma20, + benchmark_ma30, signal_ma5: benchmark_ma5, signal_ma10: benchmark_ma10, signal_ma20: benchmark_ma20, + signal_ma30: benchmark_ma30, cash, market_value, total_equity, @@ -646,6 +655,10 @@ impl PlatformExprStrategy { .data .market_decision_close_moving_average(date, symbol, 20) .unwrap_or(stock_ma_long); + let stock_ma30 = ctx + .data + .market_decision_close_moving_average(date, symbol, 30) + .unwrap_or(stock_ma20); Ok(StockExpressionState { symbol: symbol.to_string(), @@ -676,6 +689,7 @@ impl PlatformExprStrategy { stock_ma5, stock_ma10, stock_ma20, + stock_ma30, }) } @@ -694,6 +708,7 @@ impl PlatformExprStrategy { scope.push("benchmark_ma5", day.benchmark_ma5); scope.push("benchmark_ma10", day.benchmark_ma10); 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_long", day.benchmark_ma_long); scope.push("cash", day.cash); @@ -713,6 +728,37 @@ impl PlatformExprStrategy { scope.push("weekday", day.weekday); scope.push("is_month_start", day.is_month_start); 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 { scope.push("symbol", stock.symbol.clone()); scope.push("market_cap", stock.market_cap); @@ -743,6 +789,46 @@ impl PlatformExprStrategy { scope.push("stock_ma5", stock.stock_ma5); scope.push("stock_ma10", stock.stock_ma10); 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 { scope.push("avg_cost", position.avg_cost); @@ -750,6 +836,7 @@ impl PlatformExprStrategy { scope.push("holding_return", position.holding_return); scope.push("quantity", position.quantity); scope.push("sellable_qty", position.sellable_qty); + scope.push("profit_pct", position.holding_return * 100.0); } scope } @@ -944,9 +1031,16 @@ impl PlatformExprStrategy { "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), + "stock_ma_short" => Some(stock.stock_ma_short), + "stock_ma5" => Some(stock.stock_ma5), + "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_sell" => Some(if stock.allow_sell { 1.0 } else { 0.0 }), "paused" => Some(if stock.paused { 1.0 } else { 0.0 }),