From 1ba3264990ace0a77cfcb9cbdd2a41c0e979a9ea Mon Sep 17 00:00:00 2001 From: boris Date: Tue, 21 Apr 2026 21:45:18 -0700 Subject: [PATCH] Expand platform strategy runtime helpers --- .../fidc-core/src/platform_expr_strategy.rs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 59361ee..d6693bd 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -115,18 +115,34 @@ struct DayExpressionState { signal_ma5: f64, signal_ma10: f64, signal_ma20: f64, + cash: f64, + market_value: f64, + total_equity: f64, + current_exposure: f64, + position_count: i64, + max_positions: i64, + refresh_rate: i64, + year: i64, + month: i64, + day_of_month: i64, + weekday: i64, } #[derive(Debug, Clone)] struct StockExpressionState { market_cap: f64, free_float_cap: f64, + pe_ttm: f64, + turnover_ratio: f64, + effective_turnover_ratio: f64, open: f64, close: f64, last: f64, prev_close: f64, upper_limit: f64, lower_limit: f64, + price_tick: f64, + round_lot: i64, paused: bool, is_st: bool, is_kcb: bool, @@ -166,6 +182,23 @@ impl PlatformExprStrategy { engine.register_fn("abs", |value: f64| value.abs()); engine.register_fn("min", |lhs: f64, rhs: f64| lhs.min(rhs)); engine.register_fn("max", |lhs: f64, rhs: f64| lhs.max(rhs)); + engine.register_fn("sqrt", |value: f64| value.sqrt()); + engine.register_fn("pow", |lhs: f64, rhs: f64| lhs.powf(rhs)); + engine.register_fn("log", |value: f64| value.ln()); + engine.register_fn("exp", |value: f64| value.exp()); + engine.register_fn("clamp", |value: f64, low: f64, high: f64| value.clamp(low, high)); + engine.register_fn("between", |value: f64, low: f64, high: f64| value >= low && value <= high); + engine.register_fn("nz", |value: f64, fallback: f64| if value.is_finite() { value } else { fallback }); + engine.register_fn("safe_div", |lhs: f64, rhs: f64, fallback: f64| { + if rhs.abs() <= f64::EPSILON { + fallback + } else { + lhs / rhs + } + }); + engine.register_fn("iff", |condition: bool, when_true: Dynamic, when_false: Dynamic| { + if condition { when_true } else { when_false } + }); Self { config, engine } } @@ -527,6 +560,10 @@ impl PlatformExprStrategy { .data .market_decision_close_moving_average(date, &self.config.signal_symbol, 20) .unwrap_or(benchmark_ma10); + let cash = ctx.portfolio.cash(); + let market_value = ctx.portfolio.positions().values().map(|position| position.market_value()).sum::(); + let total_equity = cash + market_value; + let current_exposure = if total_equity > 0.0 { market_value / total_equity } else { 0.0 }; Ok(DayExpressionState { signal_close, @@ -539,6 +576,17 @@ impl PlatformExprStrategy { signal_ma5: benchmark_ma5, signal_ma10: benchmark_ma10, signal_ma20: benchmark_ma20, + cash, + market_value, + total_equity, + current_exposure, + position_count: ctx.portfolio.positions().len() as i64, + max_positions: self.config.max_positions as i64, + refresh_rate: self.config.refresh_rate as i64, + year: date.year() as i64, + month: date.month() as i64, + day_of_month: date.day() as i64, + weekday: date.weekday().number_from_monday() as i64, }) } @@ -551,6 +599,7 @@ impl PlatformExprStrategy { let market = ctx.data.require_market(date, symbol)?; let factor = ctx.data.require_factor(date, symbol)?; let candidate = ctx.data.require_candidate(date, symbol)?; + let instrument = ctx.data.instrument(symbol); let stock_ma_short = ctx .data .market_decision_close_moving_average(date, symbol, self.config.stock_short_ma_days) @@ -579,12 +628,17 @@ impl PlatformExprStrategy { Ok(StockExpressionState { market_cap: factor.market_cap_bn, free_float_cap: factor.free_float_cap_bn, + pe_ttm: factor.pe_ttm, + turnover_ratio: factor.turnover_ratio.unwrap_or(0.0), + effective_turnover_ratio: factor.effective_turnover_ratio.unwrap_or(0.0), open: market.day_open, close: market.close, last: market.last_price, prev_close: market.prev_close, upper_limit: market.upper_limit, lower_limit: market.lower_limit, + price_tick: market.price_tick, + round_lot: instrument.map(|item| item.effective_round_lot()).unwrap_or(100) as i64, paused: market.paused || candidate.is_paused, is_st: candidate.is_st || self.special_name(ctx, symbol), is_kcb: candidate.is_kcb, @@ -619,9 +673,24 @@ impl PlatformExprStrategy { scope.push("benchmark_ma20", day.benchmark_ma20); scope.push("benchmark_ma_short", day.benchmark_ma_short); scope.push("benchmark_ma_long", day.benchmark_ma_long); + scope.push("cash", day.cash); + scope.push("available_cash", day.cash); + scope.push("market_value", day.market_value); + scope.push("total_equity", day.total_equity); + scope.push("current_exposure", day.current_exposure); + scope.push("position_count", day.position_count); + scope.push("max_positions", day.max_positions); + scope.push("refresh_rate", day.refresh_rate); + scope.push("year", day.year); + scope.push("month", day.month); + scope.push("day_of_month", day.day_of_month); + scope.push("weekday", day.weekday); if let Some(stock) = stock { scope.push("market_cap", stock.market_cap); scope.push("free_float_cap", stock.free_float_cap); + scope.push("pe_ttm", stock.pe_ttm); + scope.push("turnover_ratio", stock.turnover_ratio); + scope.push("effective_turnover_ratio", stock.effective_turnover_ratio); scope.push("open", stock.open); scope.push("close", stock.close); scope.push("last", stock.last); @@ -629,6 +698,8 @@ impl PlatformExprStrategy { scope.push("prev_close", stock.prev_close); scope.push("upper_limit", stock.upper_limit); scope.push("lower_limit", stock.lower_limit); + scope.push("price_tick", stock.price_tick); + scope.push("round_lot", stock.round_lot); scope.push("paused", stock.paused); scope.push("is_st", stock.is_st); scope.push("is_kcb", stock.is_kcb);