From 2135a5bd0391ab5b1b79100a4ddb9974cc5bb244 Mon Sep 17 00:00:00 2001 From: boris Date: Sun, 6 Sep 2026 05:16:48 +0800 Subject: [PATCH] perf: bind numeric VM identifiers at compile time --- .../fidc-core/src/platform_expr_strategy.rs | 974 ++++++++++++++---- 1 file changed, 768 insertions(+), 206 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 2fd9a4a..840e17e 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -957,6 +957,183 @@ struct ExpressionEvalPlan { struct NumericExpressionPlan { program: NumericVmProgram, helper_bindings: Vec>, + identifier_bindings: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum NumericVmIdentifierBinding { + Day(NumericVmDayField), + Context(NumericVmContextField), + Stock(NumericVmStockField), + Position(NumericVmPositionField), + DynamicFactor(Box), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum NumericVmDayField { + SignalOpen, + SignalClose, + BenchmarkOpen, + BenchmarkClose, + BenchmarkSignalClose, + SignalMa5, + SignalMa10, + SignalMa20, + SignalMa30, + SignalMaShort, + SignalMaLong, + BenchmarkMa5, + BenchmarkMa10, + BenchmarkMa20, + BenchmarkMa30, + BenchmarkMaShort, + BenchmarkMaLong, + Cash, + AvailableCash, + FrozenCash, + MarketValue, + TotalEquity, + TotalValue, + PortfolioValue, + StartingCash, + UnitNetValue, + StaticUnitNetValue, + DailyPnl, + DailyReturns, + TotalReturns, + TransactionCost, + TradingPnl, + PositionPnl, + CashLiabilities, + ManagementFeeRate, + ManagementFees, + CurrentExposure, + PositionCount, + MaxPositions, + RefreshRate, + Year, + Month, + Quarter, + DayOfMonth, + DayOfYear, + WeekOfYear, + Weekday, + IsMonthStart, + IsMonthEnd, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum NumericVmContextField { + HasOpenOrders, + OpenOrderCount, + OpenBuyOrderCount, + OpenSellOrderCount, + OpenBuyQty, + OpenSellQty, + LatestOpenOrderId, + LatestOpenOrderUnfilledQty, + HasDynamicUniverse, + DynamicUniverseCount, + HasSubscriptions, + SubscriptionCount, + SubscriptionGuardRequired, + HasProcessEvents, + ProcessEventCount, + CurrentProcessOrderId, + LatestProcessOrderId, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum NumericVmStockField { + MarketCap, + MarketCapBn, + FreeFloatCap, + FreeFloatCapBn, + FreeFloatCapOrMarketCap, + PeTtm, + Volume, + MinuteVolume, + Bid1Volume, + Ask1Volume, + TurnoverRatio, + EffectiveTurnoverRatio, + Open, + High, + Low, + Close, + Last, + PrevClose, + Amount, + UpperLimit, + LowerLimit, + PriceTick, + RoundLot, + MinimumOrderQuantity, + OrderStepSize, + Paused, + IsSt, + IsStarSt, + IsKcb, + IsBjse, + IsOneYuan, + IsNewListing, + AllowBuy, + AllowSell, + TouchedUpperLimit, + TouchedLowerLimit, + ListedDays, + AtUpperLimit, + AtLowerLimit, + SymbolOpenOrderCount, + SymbolOpenBuyQty, + SymbolOpenSellQty, + LatestSymbolOpenOrderId, + LatestSymbolOpenOrderUnfilledQty, + InDynamicUniverse, + IsSubscribed, + StockMaShort, + StockMaMid, + StockMaLong, + StockMa5, + StockMa10, + StockMa20, + StockMa30, + StockVolumeMa5, + StockVolumeMa10, + StockVolumeMa20, + StockVolumeMa60, + StockVolumeMa100, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum NumericVmPositionField { + AvgCost, + AvgPrice, + CurrentPrice, + PrevClose, + HoldingReturn, + Quantity, + SellableQty, + Sellable, + Closable, + OldQuantity, + BoughtQuantity, + SoldQuantity, + BuyAvgPrice, + SellAvgPrice, + BoughtValue, + SoldValue, + MarketValue, + Equity, + ValuePercent, + UnrealizedPnl, + RealizedPnl, + Pnl, + DayTradeQuantityDelta, + DividendReceivable, + AvailableSellableQty, + ReservedOpenSellQty, + ProfitPct, } struct PreludeDependencyPlan { @@ -5243,12 +5420,18 @@ impl PlatformExprStrategy { .numeric_vm_runtime_helper_value(ctx, day, stock, binding, expected_type) .map_err(|error| NumericVmEvalError::new(error.to_string())); } - self.numeric_vm_identifier_value(ctx, day, stock, position, identifier) - .ok_or_else(|| { - NumericVmEvalError::new(format!( - "missing numeric/boolean identifier {identifier}" - )) - }) + self.numeric_vm_identifier_value( + ctx, + day, + stock, + position, + &vm_plan.identifier_bindings[index], + ) + .ok_or_else(|| { + NumericVmEvalError::new(format!( + "missing numeric/boolean identifier {identifier}" + )) + }) }) .map_err(|error| { BacktestError::Execution(format!( @@ -5459,219 +5642,542 @@ impl PlatformExprStrategy { } } + fn numeric_vm_identifier_binding(identifier: &str) -> NumericVmIdentifierBinding { + match identifier { + "signal_open" => NumericVmIdentifierBinding::Day(NumericVmDayField::SignalOpen), + "signal_close" => NumericVmIdentifierBinding::Day(NumericVmDayField::SignalClose), + "benchmark_open" => NumericVmIdentifierBinding::Day(NumericVmDayField::BenchmarkOpen), + "benchmark_close" => NumericVmIdentifierBinding::Day(NumericVmDayField::BenchmarkClose), + "benchmark_signal_close" => { + NumericVmIdentifierBinding::Day(NumericVmDayField::BenchmarkSignalClose) + } + "signal_ma5" => NumericVmIdentifierBinding::Day(NumericVmDayField::SignalMa5), + "signal_ma10" => NumericVmIdentifierBinding::Day(NumericVmDayField::SignalMa10), + "signal_ma20" => NumericVmIdentifierBinding::Day(NumericVmDayField::SignalMa20), + "signal_ma30" => NumericVmIdentifierBinding::Day(NumericVmDayField::SignalMa30), + "signal_ma_short" => NumericVmIdentifierBinding::Day(NumericVmDayField::SignalMaShort), + "signal_ma_long" => NumericVmIdentifierBinding::Day(NumericVmDayField::SignalMaLong), + "benchmark_ma5" => NumericVmIdentifierBinding::Day(NumericVmDayField::BenchmarkMa5), + "benchmark_ma10" => NumericVmIdentifierBinding::Day(NumericVmDayField::BenchmarkMa10), + "benchmark_ma20" => NumericVmIdentifierBinding::Day(NumericVmDayField::BenchmarkMa20), + "benchmark_ma30" => NumericVmIdentifierBinding::Day(NumericVmDayField::BenchmarkMa30), + "benchmark_ma_short" => { + NumericVmIdentifierBinding::Day(NumericVmDayField::BenchmarkMaShort) + } + "benchmark_ma_long" => { + NumericVmIdentifierBinding::Day(NumericVmDayField::BenchmarkMaLong) + } + "cash" => NumericVmIdentifierBinding::Day(NumericVmDayField::Cash), + "available_cash" => NumericVmIdentifierBinding::Day(NumericVmDayField::AvailableCash), + "frozen_cash" => NumericVmIdentifierBinding::Day(NumericVmDayField::FrozenCash), + "market_value" => NumericVmIdentifierBinding::Day(NumericVmDayField::MarketValue), + "total_equity" => NumericVmIdentifierBinding::Day(NumericVmDayField::TotalEquity), + "total_value" => NumericVmIdentifierBinding::Day(NumericVmDayField::TotalValue), + "portfolio_value" => NumericVmIdentifierBinding::Day(NumericVmDayField::PortfolioValue), + "starting_cash" => NumericVmIdentifierBinding::Day(NumericVmDayField::StartingCash), + "unit_net_value" => NumericVmIdentifierBinding::Day(NumericVmDayField::UnitNetValue), + "static_unit_net_value" => { + NumericVmIdentifierBinding::Day(NumericVmDayField::StaticUnitNetValue) + } + "daily_pnl" => NumericVmIdentifierBinding::Day(NumericVmDayField::DailyPnl), + "daily_returns" => NumericVmIdentifierBinding::Day(NumericVmDayField::DailyReturns), + "total_returns" => NumericVmIdentifierBinding::Day(NumericVmDayField::TotalReturns), + "transaction_cost" => { + NumericVmIdentifierBinding::Day(NumericVmDayField::TransactionCost) + } + "trading_pnl" => NumericVmIdentifierBinding::Day(NumericVmDayField::TradingPnl), + "position_pnl" => NumericVmIdentifierBinding::Day(NumericVmDayField::PositionPnl), + "cash_liabilities" => { + NumericVmIdentifierBinding::Day(NumericVmDayField::CashLiabilities) + } + "management_fee_rate" => { + NumericVmIdentifierBinding::Day(NumericVmDayField::ManagementFeeRate) + } + "management_fees" => NumericVmIdentifierBinding::Day(NumericVmDayField::ManagementFees), + "current_exposure" => { + NumericVmIdentifierBinding::Day(NumericVmDayField::CurrentExposure) + } + "position_count" => NumericVmIdentifierBinding::Day(NumericVmDayField::PositionCount), + "max_positions" => NumericVmIdentifierBinding::Day(NumericVmDayField::MaxPositions), + "refresh_rate" => NumericVmIdentifierBinding::Day(NumericVmDayField::RefreshRate), + "year" => NumericVmIdentifierBinding::Day(NumericVmDayField::Year), + "month" => NumericVmIdentifierBinding::Day(NumericVmDayField::Month), + "quarter" => NumericVmIdentifierBinding::Day(NumericVmDayField::Quarter), + "day_of_month" => NumericVmIdentifierBinding::Day(NumericVmDayField::DayOfMonth), + "day_of_year" => NumericVmIdentifierBinding::Day(NumericVmDayField::DayOfYear), + "week_of_year" => NumericVmIdentifierBinding::Day(NumericVmDayField::WeekOfYear), + "weekday" => NumericVmIdentifierBinding::Day(NumericVmDayField::Weekday), + "is_month_start" => NumericVmIdentifierBinding::Day(NumericVmDayField::IsMonthStart), + "is_month_end" => NumericVmIdentifierBinding::Day(NumericVmDayField::IsMonthEnd), + "has_open_orders" => { + NumericVmIdentifierBinding::Context(NumericVmContextField::HasOpenOrders) + } + "open_order_count" => { + NumericVmIdentifierBinding::Context(NumericVmContextField::OpenOrderCount) + } + "open_buy_order_count" => { + NumericVmIdentifierBinding::Context(NumericVmContextField::OpenBuyOrderCount) + } + "open_sell_order_count" => { + NumericVmIdentifierBinding::Context(NumericVmContextField::OpenSellOrderCount) + } + "open_buy_qty" => { + NumericVmIdentifierBinding::Context(NumericVmContextField::OpenBuyQty) + } + "open_sell_qty" => { + NumericVmIdentifierBinding::Context(NumericVmContextField::OpenSellQty) + } + "latest_open_order_id" => { + NumericVmIdentifierBinding::Context(NumericVmContextField::LatestOpenOrderId) + } + "latest_open_order_unfilled_qty" => NumericVmIdentifierBinding::Context( + NumericVmContextField::LatestOpenOrderUnfilledQty, + ), + "has_dynamic_universe" => { + NumericVmIdentifierBinding::Context(NumericVmContextField::HasDynamicUniverse) + } + "dynamic_universe_count" => { + NumericVmIdentifierBinding::Context(NumericVmContextField::DynamicUniverseCount) + } + "has_subscriptions" => { + NumericVmIdentifierBinding::Context(NumericVmContextField::HasSubscriptions) + } + "subscription_count" => { + NumericVmIdentifierBinding::Context(NumericVmContextField::SubscriptionCount) + } + "subscription_guard_required" => NumericVmIdentifierBinding::Context( + NumericVmContextField::SubscriptionGuardRequired, + ), + "has_process_events" => { + NumericVmIdentifierBinding::Context(NumericVmContextField::HasProcessEvents) + } + "process_event_count" => { + NumericVmIdentifierBinding::Context(NumericVmContextField::ProcessEventCount) + } + "current_process_order_id" => { + NumericVmIdentifierBinding::Context(NumericVmContextField::CurrentProcessOrderId) + } + "latest_process_order_id" => { + NumericVmIdentifierBinding::Context(NumericVmContextField::LatestProcessOrderId) + } + "market_cap" => NumericVmIdentifierBinding::Stock(NumericVmStockField::MarketCap), + "market_cap_bn" => NumericVmIdentifierBinding::Stock(NumericVmStockField::MarketCapBn), + "free_float_cap" | "free_float_market_cap" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::FreeFloatCap) + } + "free_float_cap_bn" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::FreeFloatCapBn) + } + "free_float_cap_or_market_cap" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::FreeFloatCapOrMarketCap) + } + "pe_ttm" => NumericVmIdentifierBinding::Stock(NumericVmStockField::PeTtm), + "volume" => NumericVmIdentifierBinding::Stock(NumericVmStockField::Volume), + "minute_volume" | "intraday_volume" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::MinuteVolume) + } + "bid1_volume" => NumericVmIdentifierBinding::Stock(NumericVmStockField::Bid1Volume), + "ask1_volume" => NumericVmIdentifierBinding::Stock(NumericVmStockField::Ask1Volume), + "turnover" | "turnover_ratio" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::TurnoverRatio) + } + "effective_turnover_ratio" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::EffectiveTurnoverRatio) + } + "open" => NumericVmIdentifierBinding::Stock(NumericVmStockField::Open), + "high" => NumericVmIdentifierBinding::Stock(NumericVmStockField::High), + "low" => NumericVmIdentifierBinding::Stock(NumericVmStockField::Low), + "close" => NumericVmIdentifierBinding::Stock(NumericVmStockField::Close), + "last" | "last_price" => NumericVmIdentifierBinding::Stock(NumericVmStockField::Last), + "prev_close" => NumericVmIdentifierBinding::Stock(NumericVmStockField::PrevClose), + "amount" => NumericVmIdentifierBinding::Stock(NumericVmStockField::Amount), + "upper_limit" => NumericVmIdentifierBinding::Stock(NumericVmStockField::UpperLimit), + "lower_limit" => NumericVmIdentifierBinding::Stock(NumericVmStockField::LowerLimit), + "price_tick" => NumericVmIdentifierBinding::Stock(NumericVmStockField::PriceTick), + "round_lot" => NumericVmIdentifierBinding::Stock(NumericVmStockField::RoundLot), + "minimum_order_quantity" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::MinimumOrderQuantity) + } + "order_step_size" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::OrderStepSize) + } + "paused" => NumericVmIdentifierBinding::Stock(NumericVmStockField::Paused), + "is_st" => NumericVmIdentifierBinding::Stock(NumericVmStockField::IsSt), + "is_star_st" => NumericVmIdentifierBinding::Stock(NumericVmStockField::IsStarSt), + "is_kcb" => NumericVmIdentifierBinding::Stock(NumericVmStockField::IsKcb), + "is_bjse" => NumericVmIdentifierBinding::Stock(NumericVmStockField::IsBjse), + "is_one_yuan" => NumericVmIdentifierBinding::Stock(NumericVmStockField::IsOneYuan), + "is_new_listing" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::IsNewListing) + } + "allow_buy" => NumericVmIdentifierBinding::Stock(NumericVmStockField::AllowBuy), + "allow_sell" => NumericVmIdentifierBinding::Stock(NumericVmStockField::AllowSell), + "touched_upper_limit" | "hit_upper_limit" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::TouchedUpperLimit) + } + "touched_lower_limit" | "hit_lower_limit" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::TouchedLowerLimit) + } + "listed_days" => NumericVmIdentifierBinding::Stock(NumericVmStockField::ListedDays), + "at_upper_limit" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::AtUpperLimit) + } + "at_lower_limit" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::AtLowerLimit) + } + "symbol_open_order_count" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::SymbolOpenOrderCount) + } + "symbol_open_buy_qty" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::SymbolOpenBuyQty) + } + "symbol_open_sell_qty" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::SymbolOpenSellQty) + } + "latest_symbol_open_order_id" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::LatestSymbolOpenOrderId) + } + "latest_symbol_open_order_unfilled_qty" => NumericVmIdentifierBinding::Stock( + NumericVmStockField::LatestSymbolOpenOrderUnfilledQty, + ), + "in_dynamic_universe" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::InDynamicUniverse) + } + "is_subscribed" => NumericVmIdentifierBinding::Stock(NumericVmStockField::IsSubscribed), + "stock_ma_short" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::StockMaShort) + } + "stock_ma_mid" => NumericVmIdentifierBinding::Stock(NumericVmStockField::StockMaMid), + "stock_ma_long" => NumericVmIdentifierBinding::Stock(NumericVmStockField::StockMaLong), + "stock_ma5" | "ma5" => NumericVmIdentifierBinding::Stock(NumericVmStockField::StockMa5), + "stock_ma10" | "ma10" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::StockMa10) + } + "stock_ma20" | "ma20" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::StockMa20) + } + "stock_ma30" | "ma30" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::StockMa30) + } + "stock_volume_ma5" | "volume_ma5" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::StockVolumeMa5) + } + "stock_volume_ma10" | "volume_ma10" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::StockVolumeMa10) + } + "stock_volume_ma20" | "volume_ma20" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::StockVolumeMa20) + } + "stock_volume_ma60" | "volume_ma60" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::StockVolumeMa60) + } + "stock_volume_ma100" | "volume_ma100" => { + NumericVmIdentifierBinding::Stock(NumericVmStockField::StockVolumeMa100) + } + "avg_cost" => NumericVmIdentifierBinding::Position(NumericVmPositionField::AvgCost), + "avg_price" => NumericVmIdentifierBinding::Position(NumericVmPositionField::AvgPrice), + "current_price" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::CurrentPrice) + } + "position_prev_close" | "prev_position_close" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::PrevClose) + } + "holding_return" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::HoldingReturn) + } + "quantity" => NumericVmIdentifierBinding::Position(NumericVmPositionField::Quantity), + "sellable_qty" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::SellableQty) + } + "sellable" => NumericVmIdentifierBinding::Position(NumericVmPositionField::Sellable), + "closable" => NumericVmIdentifierBinding::Position(NumericVmPositionField::Closable), + "old_quantity" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::OldQuantity) + } + "buy_quantity" | "bought_quantity" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::BoughtQuantity) + } + "sell_quantity" | "sold_quantity" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::SoldQuantity) + } + "buy_avg_price" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::BuyAvgPrice) + } + "sell_avg_price" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::SellAvgPrice) + } + "bought_value" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::BoughtValue) + } + "sold_value" => NumericVmIdentifierBinding::Position(NumericVmPositionField::SoldValue), + "position_market_value" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::MarketValue) + } + "equity" => NumericVmIdentifierBinding::Position(NumericVmPositionField::Equity), + "value_percent" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::ValuePercent) + } + "unrealized_pnl" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::UnrealizedPnl) + } + "realized_pnl" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::RealizedPnl) + } + "pnl" => NumericVmIdentifierBinding::Position(NumericVmPositionField::Pnl), + "day_trade_quantity_delta" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::DayTradeQuantityDelta) + } + "dividend_receivable" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::DividendReceivable) + } + "available_sellable_qty" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::AvailableSellableQty) + } + "reserved_open_sell_qty" => { + NumericVmIdentifierBinding::Position(NumericVmPositionField::ReservedOpenSellQty) + } + "profit_pct" => NumericVmIdentifierBinding::Position(NumericVmPositionField::ProfitPct), + _ => NumericVmIdentifierBinding::DynamicFactor(identifier.into()), + } + } + fn numeric_vm_identifier_value( &self, ctx: &StrategyContext<'_>, day: &DayExpressionState, stock: Option<&StockExpressionState>, position: Option<&PositionExpressionState>, - identifier: &str, + binding: &NumericVmIdentifierBinding, ) -> Option { let number = |value: f64| Some(NumericVmValue::Number(value)); let integer = |value: i64| Some(NumericVmValue::Number(value as f64)); let boolean = |value: bool| Some(NumericVmValue::Boolean(value)); - match identifier { - "signal_open" => number(day.signal_open), - "signal_close" => number(day.signal_close), - "benchmark_open" => number(day.benchmark_open), - "benchmark_close" => number(day.benchmark_close), - "benchmark_signal_close" => number(day.benchmark_signal_close), - "signal_ma5" => number(day.signal_ma5), - "signal_ma10" => number(day.signal_ma10), - "signal_ma20" => number(day.signal_ma20), - "signal_ma30" => number(day.signal_ma30), - "signal_ma_short" => number(day.signal_ma_short), - "signal_ma_long" => number(day.signal_ma_long), - "benchmark_ma5" => number(day.benchmark_ma5), - "benchmark_ma10" => number(day.benchmark_ma10), - "benchmark_ma20" => number(day.benchmark_ma20), - "benchmark_ma30" => number(day.benchmark_ma30), - "benchmark_ma_short" => number(day.benchmark_ma_short), - "benchmark_ma_long" => number(day.benchmark_ma_long), - "cash" => number(day.cash), - "available_cash" => number(day.available_cash), - "frozen_cash" => number(day.frozen_cash), - "market_value" => number(day.market_value), - "total_equity" => number(day.total_equity), - "total_value" => number(day.total_value), - "portfolio_value" => number(day.portfolio_value), - "starting_cash" => number(day.starting_cash), - "unit_net_value" => number(day.unit_net_value), - "static_unit_net_value" => number(day.static_unit_net_value), - "daily_pnl" => number(day.daily_pnl), - "daily_returns" => number(day.daily_returns), - "total_returns" => number(day.total_returns), - "transaction_cost" => { - number(position.map_or(day.transaction_cost, |value| value.transaction_cost)) - } - "trading_pnl" => number(position.map_or(day.trading_pnl, |value| value.trading_pnl)), - "position_pnl" => number(position.map_or(day.position_pnl, |value| value.position_pnl)), - "cash_liabilities" => number(day.cash_liabilities), - "management_fee_rate" => number(day.management_fee_rate), - "management_fees" => number(day.management_fees), - "current_exposure" => number(day.current_exposure), - "position_count" => integer(day.position_count), - "max_positions" => integer(day.max_positions), - "refresh_rate" => integer(day.refresh_rate), - "year" => integer(day.year), - "month" => integer(day.month), - "quarter" => integer(day.quarter), - "day_of_month" => integer(day.day_of_month), - "day_of_year" => integer(day.day_of_year), - "week_of_year" => integer(day.week_of_year), - "weekday" => integer(day.weekday), - "is_month_start" => boolean(day.is_month_start), - "is_month_end" => boolean(day.is_month_end), - "has_open_orders" => boolean(ctx.has_open_orders()), - "open_order_count" => integer(ctx.open_order_count() as i64), - "open_buy_order_count" => integer(ctx.open_buy_order_count() as i64), - "open_sell_order_count" => integer(ctx.open_sell_order_count() as i64), - "open_buy_qty" => integer(ctx.open_buy_quantity() as i64), - "open_sell_qty" => integer(ctx.open_sell_quantity() as i64), - "latest_open_order_id" => integer(ctx.latest_open_order_id() as i64), - "latest_open_order_unfilled_qty" => { - integer(ctx.latest_open_order_unfilled_quantity() as i64) - } - "has_dynamic_universe" => boolean(ctx.has_dynamic_universe()), - "dynamic_universe_count" => integer(ctx.dynamic_universe_count() as i64), - "has_subscriptions" => boolean(ctx.has_subscriptions()), - "subscription_count" => integer(ctx.subscription_count() as i64), - "subscription_guard_required" => boolean(self.config.subscription_guard_required), - "has_process_events" => boolean(ctx.has_process_events()), - "process_event_count" => integer(ctx.process_event_count() as i64), - "current_process_order_id" => integer(ctx.current_process_event_order_id() as i64), - "latest_process_order_id" => integer(ctx.latest_process_event_order_id() as i64), - _ => { - if let Some(stock) = stock { - let at_upper_limit = Self::price_is_at_or_above_upper_limit( - stock.last, - stock.upper_limit, - stock.price_tick, - ); - let at_lower_limit = Self::price_is_at_or_below_lower_limit( - stock.last, - stock.lower_limit, - stock.price_tick, - ); - let stock_value = match identifier { - "market_cap" => number(stock.market_cap), - "market_cap_bn" => number(stock.market_cap_bn), - "free_float_cap" | "free_float_market_cap" => number(stock.free_float_cap), - "free_float_cap_bn" => number(stock.free_float_cap_bn), - "free_float_cap_or_market_cap" => number(if stock.free_float_cap > 0.0 { + match binding { + NumericVmIdentifierBinding::Day(field) => match field { + NumericVmDayField::SignalOpen => number(day.signal_open), + NumericVmDayField::SignalClose => number(day.signal_close), + NumericVmDayField::BenchmarkOpen => number(day.benchmark_open), + NumericVmDayField::BenchmarkClose => number(day.benchmark_close), + NumericVmDayField::BenchmarkSignalClose => number(day.benchmark_signal_close), + NumericVmDayField::SignalMa5 => number(day.signal_ma5), + NumericVmDayField::SignalMa10 => number(day.signal_ma10), + NumericVmDayField::SignalMa20 => number(day.signal_ma20), + NumericVmDayField::SignalMa30 => number(day.signal_ma30), + NumericVmDayField::SignalMaShort => number(day.signal_ma_short), + NumericVmDayField::SignalMaLong => number(day.signal_ma_long), + NumericVmDayField::BenchmarkMa5 => number(day.benchmark_ma5), + NumericVmDayField::BenchmarkMa10 => number(day.benchmark_ma10), + NumericVmDayField::BenchmarkMa20 => number(day.benchmark_ma20), + NumericVmDayField::BenchmarkMa30 => number(day.benchmark_ma30), + NumericVmDayField::BenchmarkMaShort => number(day.benchmark_ma_short), + NumericVmDayField::BenchmarkMaLong => number(day.benchmark_ma_long), + NumericVmDayField::Cash => number(day.cash), + NumericVmDayField::AvailableCash => number(day.available_cash), + NumericVmDayField::FrozenCash => number(day.frozen_cash), + NumericVmDayField::MarketValue => number(day.market_value), + NumericVmDayField::TotalEquity => number(day.total_equity), + NumericVmDayField::TotalValue => number(day.total_value), + NumericVmDayField::PortfolioValue => number(day.portfolio_value), + NumericVmDayField::StartingCash => number(day.starting_cash), + NumericVmDayField::UnitNetValue => number(day.unit_net_value), + NumericVmDayField::StaticUnitNetValue => number(day.static_unit_net_value), + NumericVmDayField::DailyPnl => number(day.daily_pnl), + NumericVmDayField::DailyReturns => number(day.daily_returns), + NumericVmDayField::TotalReturns => number(day.total_returns), + NumericVmDayField::TransactionCost => { + number(position.map_or(day.transaction_cost, |value| value.transaction_cost)) + } + NumericVmDayField::TradingPnl => { + number(position.map_or(day.trading_pnl, |value| value.trading_pnl)) + } + NumericVmDayField::PositionPnl => { + number(position.map_or(day.position_pnl, |value| value.position_pnl)) + } + NumericVmDayField::CashLiabilities => number(day.cash_liabilities), + NumericVmDayField::ManagementFeeRate => number(day.management_fee_rate), + NumericVmDayField::ManagementFees => number(day.management_fees), + NumericVmDayField::CurrentExposure => number(day.current_exposure), + NumericVmDayField::PositionCount => integer(day.position_count), + NumericVmDayField::MaxPositions => integer(day.max_positions), + NumericVmDayField::RefreshRate => integer(day.refresh_rate), + NumericVmDayField::Year => integer(day.year), + NumericVmDayField::Month => integer(day.month), + NumericVmDayField::Quarter => integer(day.quarter), + NumericVmDayField::DayOfMonth => integer(day.day_of_month), + NumericVmDayField::DayOfYear => integer(day.day_of_year), + NumericVmDayField::WeekOfYear => integer(day.week_of_year), + NumericVmDayField::Weekday => integer(day.weekday), + NumericVmDayField::IsMonthStart => boolean(day.is_month_start), + NumericVmDayField::IsMonthEnd => boolean(day.is_month_end), + }, + NumericVmIdentifierBinding::Context(field) => match field { + NumericVmContextField::HasOpenOrders => boolean(ctx.has_open_orders()), + NumericVmContextField::OpenOrderCount => integer(ctx.open_order_count() as i64), + NumericVmContextField::OpenBuyOrderCount => { + integer(ctx.open_buy_order_count() as i64) + } + NumericVmContextField::OpenSellOrderCount => { + integer(ctx.open_sell_order_count() as i64) + } + NumericVmContextField::OpenBuyQty => integer(ctx.open_buy_quantity() as i64), + NumericVmContextField::OpenSellQty => integer(ctx.open_sell_quantity() as i64), + NumericVmContextField::LatestOpenOrderId => { + integer(ctx.latest_open_order_id() as i64) + } + NumericVmContextField::LatestOpenOrderUnfilledQty => { + integer(ctx.latest_open_order_unfilled_quantity() as i64) + } + NumericVmContextField::HasDynamicUniverse => boolean(ctx.has_dynamic_universe()), + NumericVmContextField::DynamicUniverseCount => { + integer(ctx.dynamic_universe_count() as i64) + } + NumericVmContextField::HasSubscriptions => boolean(ctx.has_subscriptions()), + NumericVmContextField::SubscriptionCount => { + integer(ctx.subscription_count() as i64) + } + NumericVmContextField::SubscriptionGuardRequired => { + boolean(self.config.subscription_guard_required) + } + NumericVmContextField::HasProcessEvents => boolean(ctx.has_process_events()), + NumericVmContextField::ProcessEventCount => { + integer(ctx.process_event_count() as i64) + } + NumericVmContextField::CurrentProcessOrderId => { + integer(ctx.current_process_event_order_id() as i64) + } + NumericVmContextField::LatestProcessOrderId => { + integer(ctx.latest_process_event_order_id() as i64) + } + }, + NumericVmIdentifierBinding::Stock(field) => { + let stock = stock?; + match field { + NumericVmStockField::MarketCap => number(stock.market_cap), + NumericVmStockField::MarketCapBn => number(stock.market_cap_bn), + NumericVmStockField::FreeFloatCap => number(stock.free_float_cap), + NumericVmStockField::FreeFloatCapBn => number(stock.free_float_cap_bn), + NumericVmStockField::FreeFloatCapOrMarketCap => { + number(if stock.free_float_cap > 0.0 { stock.free_float_cap } else { stock.market_cap - }), - "pe_ttm" => number(stock.pe_ttm), - "volume" => number(stock.volume), - "minute_volume" | "intraday_volume" => integer(stock.minute_volume), - "bid1_volume" => integer(stock.bid1_volume), - "ask1_volume" => integer(stock.ask1_volume), - "turnover" | "turnover_ratio" => number(stock.turnover_ratio), - "effective_turnover_ratio" => number(stock.effective_turnover_ratio), - "open" => number(stock.open), - "high" => number(stock.high), - "low" => number(stock.low), - "close" => number(stock.close), - "last" | "last_price" => number(stock.last), - "prev_close" => number(stock.prev_close), - "amount" => number(stock.amount), - "upper_limit" => number(stock.upper_limit), - "lower_limit" => number(stock.lower_limit), - "price_tick" => number(stock.price_tick), - "round_lot" => integer(stock.round_lot), - "minimum_order_quantity" => integer(stock.minimum_order_quantity), - "order_step_size" => integer(stock.order_step_size), - "paused" => boolean(stock.paused), - "is_st" => boolean(stock.is_st), - "is_star_st" => boolean(stock.is_star_st), - "is_kcb" => boolean(stock.is_kcb), - "is_bjse" => boolean(stock.is_bjse), - "is_one_yuan" => boolean(stock.is_one_yuan), - "is_new_listing" => boolean(stock.is_new_listing), - "allow_buy" => boolean(stock.allow_buy), - "allow_sell" => boolean(stock.allow_sell), - "touched_upper_limit" | "hit_upper_limit" => { - boolean(stock.touched_upper_limit) - } - "touched_lower_limit" | "hit_lower_limit" => { - boolean(stock.touched_lower_limit) - } - "listed_days" => integer(stock.listed_days), - "at_upper_limit" => boolean(at_upper_limit), - "at_lower_limit" => boolean(at_lower_limit), - "symbol_open_order_count" => { - integer(ctx.symbol_open_order_count(&stock.symbol) as i64) - } - "symbol_open_buy_qty" => { - integer(ctx.symbol_open_buy_quantity(&stock.symbol) as i64) - } - "symbol_open_sell_qty" => { - integer(ctx.symbol_open_sell_quantity(&stock.symbol) as i64) - } - "latest_symbol_open_order_id" => { - integer(ctx.latest_symbol_open_order_id(&stock.symbol) as i64) - } - "latest_symbol_open_order_unfilled_qty" => integer( - ctx.latest_symbol_open_order_unfilled_quantity(&stock.symbol) as i64, - ), - "in_dynamic_universe" => { - boolean(ctx.dynamic_universe_contains(&stock.symbol)) - } - "is_subscribed" => boolean(ctx.is_subscribed(&stock.symbol)), - "stock_ma_short" => number(stock.stock_ma_short), - "stock_ma_mid" => number(stock.stock_ma_mid), - "stock_ma_long" => number(stock.stock_ma_long), - "stock_ma5" | "ma5" => number(stock.stock_ma5), - "stock_ma10" | "ma10" => number(stock.stock_ma10), - "stock_ma20" | "ma20" => number(stock.stock_ma20), - "stock_ma30" | "ma30" => number(stock.stock_ma30), - "stock_volume_ma5" | "volume_ma5" => number(stock.stock_volume_ma5), - "stock_volume_ma10" | "volume_ma10" => number(stock.stock_volume_ma10), - "stock_volume_ma20" | "volume_ma20" => number(stock.stock_volume_ma20), - "stock_volume_ma60" | "volume_ma60" => number(stock.stock_volume_ma60), - "stock_volume_ma100" | "volume_ma100" => number(stock.stock_volume_ma100), - _ => stock - .extra_factors - .get(identifier) - .copied() - .map(NumericVmValue::Number), - }; - if stock_value.is_some() { - return stock_value; + }) } - if day.available_factor_names.contains(identifier) { - return number(f64::NAN); + NumericVmStockField::PeTtm => number(stock.pe_ttm), + NumericVmStockField::Volume => number(stock.volume), + NumericVmStockField::MinuteVolume => integer(stock.minute_volume), + NumericVmStockField::Bid1Volume => integer(stock.bid1_volume), + NumericVmStockField::Ask1Volume => integer(stock.ask1_volume), + NumericVmStockField::TurnoverRatio => number(stock.turnover_ratio), + NumericVmStockField::EffectiveTurnoverRatio => { + number(stock.effective_turnover_ratio) } + NumericVmStockField::Open => number(stock.open), + NumericVmStockField::High => number(stock.high), + NumericVmStockField::Low => number(stock.low), + NumericVmStockField::Close => number(stock.close), + NumericVmStockField::Last => number(stock.last), + NumericVmStockField::PrevClose => number(stock.prev_close), + NumericVmStockField::Amount => number(stock.amount), + NumericVmStockField::UpperLimit => number(stock.upper_limit), + NumericVmStockField::LowerLimit => number(stock.lower_limit), + NumericVmStockField::PriceTick => number(stock.price_tick), + NumericVmStockField::RoundLot => integer(stock.round_lot), + NumericVmStockField::MinimumOrderQuantity => { + integer(stock.minimum_order_quantity) + } + NumericVmStockField::OrderStepSize => integer(stock.order_step_size), + NumericVmStockField::Paused => boolean(stock.paused), + NumericVmStockField::IsSt => boolean(stock.is_st), + NumericVmStockField::IsStarSt => boolean(stock.is_star_st), + NumericVmStockField::IsKcb => boolean(stock.is_kcb), + NumericVmStockField::IsBjse => boolean(stock.is_bjse), + NumericVmStockField::IsOneYuan => boolean(stock.is_one_yuan), + NumericVmStockField::IsNewListing => boolean(stock.is_new_listing), + NumericVmStockField::AllowBuy => boolean(stock.allow_buy), + NumericVmStockField::AllowSell => boolean(stock.allow_sell), + NumericVmStockField::TouchedUpperLimit => boolean(stock.touched_upper_limit), + NumericVmStockField::TouchedLowerLimit => boolean(stock.touched_lower_limit), + NumericVmStockField::ListedDays => integer(stock.listed_days), + NumericVmStockField::AtUpperLimit => { + boolean(Self::price_is_at_or_above_upper_limit( + stock.last, + stock.upper_limit, + stock.price_tick, + )) + } + NumericVmStockField::AtLowerLimit => { + boolean(Self::price_is_at_or_below_lower_limit( + stock.last, + stock.lower_limit, + stock.price_tick, + )) + } + NumericVmStockField::SymbolOpenOrderCount => { + integer(ctx.symbol_open_order_count(&stock.symbol) as i64) + } + NumericVmStockField::SymbolOpenBuyQty => { + integer(ctx.symbol_open_buy_quantity(&stock.symbol) as i64) + } + NumericVmStockField::SymbolOpenSellQty => { + integer(ctx.symbol_open_sell_quantity(&stock.symbol) as i64) + } + NumericVmStockField::LatestSymbolOpenOrderId => { + integer(ctx.latest_symbol_open_order_id(&stock.symbol) as i64) + } + NumericVmStockField::LatestSymbolOpenOrderUnfilledQty => integer( + ctx.latest_symbol_open_order_unfilled_quantity(&stock.symbol) as i64, + ), + NumericVmStockField::InDynamicUniverse => { + boolean(ctx.dynamic_universe_contains(&stock.symbol)) + } + NumericVmStockField::IsSubscribed => boolean(ctx.is_subscribed(&stock.symbol)), + NumericVmStockField::StockMaShort => number(stock.stock_ma_short), + NumericVmStockField::StockMaMid => number(stock.stock_ma_mid), + NumericVmStockField::StockMaLong => number(stock.stock_ma_long), + NumericVmStockField::StockMa5 => number(stock.stock_ma5), + NumericVmStockField::StockMa10 => number(stock.stock_ma10), + NumericVmStockField::StockMa20 => number(stock.stock_ma20), + NumericVmStockField::StockMa30 => number(stock.stock_ma30), + NumericVmStockField::StockVolumeMa5 => number(stock.stock_volume_ma5), + NumericVmStockField::StockVolumeMa10 => number(stock.stock_volume_ma10), + NumericVmStockField::StockVolumeMa20 => number(stock.stock_volume_ma20), + NumericVmStockField::StockVolumeMa60 => number(stock.stock_volume_ma60), + NumericVmStockField::StockVolumeMa100 => number(stock.stock_volume_ma100), } + } + NumericVmIdentifierBinding::Position(field) => { let position = position?; - match identifier { - "avg_cost" => number(position.avg_cost), - "avg_price" => number(position.avg_price), - "current_price" => number(position.current_price), - "position_prev_close" | "prev_position_close" => number(position.prev_close), - "holding_return" => number(position.holding_return), - "quantity" => integer(position.quantity), - "sellable_qty" => integer(position.sellable_qty), - "sellable" => integer(position.sellable), - "closable" => integer(position.closable), - "old_quantity" => integer(position.old_quantity), - "buy_quantity" | "bought_quantity" => integer(position.bought_quantity), - "sell_quantity" | "sold_quantity" => integer(position.sold_quantity), - "buy_avg_price" => number(position.buy_avg_price), - "sell_avg_price" => number(position.sell_avg_price), - "bought_value" => number(position.bought_value), - "sold_value" => number(position.sold_value), - "position_market_value" => number(position.market_value), - "equity" => number(position.equity), - "value_percent" => number(position.value_percent), - "unrealized_pnl" => number(position.unrealized_pnl), - "realized_pnl" => number(position.realized_pnl), - "pnl" => number(position.pnl), - "day_trade_quantity_delta" => integer(position.day_trade_quantity_delta), - "dividend_receivable" => number(position.dividend_receivable), - "available_sellable_qty" => integer( + match field { + NumericVmPositionField::AvgCost => number(position.avg_cost), + NumericVmPositionField::AvgPrice => number(position.avg_price), + NumericVmPositionField::CurrentPrice => number(position.current_price), + NumericVmPositionField::PrevClose => number(position.prev_close), + NumericVmPositionField::HoldingReturn => number(position.holding_return), + NumericVmPositionField::Quantity => integer(position.quantity), + NumericVmPositionField::SellableQty => integer(position.sellable_qty), + NumericVmPositionField::Sellable => integer(position.sellable), + NumericVmPositionField::Closable => integer(position.closable), + NumericVmPositionField::OldQuantity => integer(position.old_quantity), + NumericVmPositionField::BoughtQuantity => integer(position.bought_quantity), + NumericVmPositionField::SoldQuantity => integer(position.sold_quantity), + NumericVmPositionField::BuyAvgPrice => number(position.buy_avg_price), + NumericVmPositionField::SellAvgPrice => number(position.sell_avg_price), + NumericVmPositionField::BoughtValue => number(position.bought_value), + NumericVmPositionField::SoldValue => number(position.sold_value), + NumericVmPositionField::MarketValue => number(position.market_value), + NumericVmPositionField::Equity => number(position.equity), + NumericVmPositionField::ValuePercent => number(position.value_percent), + NumericVmPositionField::UnrealizedPnl => number(position.unrealized_pnl), + NumericVmPositionField::RealizedPnl => number(position.realized_pnl), + NumericVmPositionField::Pnl => number(position.pnl), + NumericVmPositionField::DayTradeQuantityDelta => { + integer(position.day_trade_quantity_delta) + } + NumericVmPositionField::DividendReceivable => { + number(position.dividend_receivable) + } + NumericVmPositionField::AvailableSellableQty => integer( stock .map(|stock| { ctx.available_sellable_qty( @@ -5681,15 +6187,27 @@ impl PlatformExprStrategy { }) .unwrap_or(position.sellable_qty.max(0)), ), - "reserved_open_sell_qty" => integer( + NumericVmPositionField::ReservedOpenSellQty => integer( stock .map(|stock| ctx.symbol_open_sell_quantity(&stock.symbol) as i64) .unwrap_or(0), ), - "profit_pct" => number(position.holding_return * 100.0), - _ => None, + NumericVmPositionField::ProfitPct => number(position.holding_return * 100.0), } } + NumericVmIdentifierBinding::DynamicFactor(identifier) => { + let stock = stock?; + stock + .extra_factors + .get(identifier.as_ref()) + .copied() + .map(NumericVmValue::Number) + .or_else(|| { + day.available_factor_names + .contains(identifier.as_ref()) + .then_some(NumericVmValue::Number(f64::NAN)) + }) + } } } @@ -6389,10 +6907,16 @@ impl PlatformExprStrategy { .variables() .iter() .map(|identifier| bindings.get(identifier).cloned()) + .collect::>(); + let identifier_bindings = program + .variables() + .iter() + .map(|identifier| Self::numeric_vm_identifier_binding(identifier)) .collect(); Some(NumericExpressionPlan { program, helper_bindings, + identifier_bindings, }) } @@ -13246,7 +13770,8 @@ mod tests { use chrono::{NaiveDate, NaiveTime}; use super::{ - CompiledRuntimeHelperArgs, PlatformAccountActionKind, PlatformExplicitActionStage, + CompiledRuntimeHelperArgs, NumericVmIdentifierBinding, NumericVmPositionField, + NumericVmStockField, PlatformAccountActionKind, PlatformExplicitActionStage, PlatformExplicitCancelKind, PlatformExplicitOrderKind, PlatformExprStrategy, PlatformExprStrategyConfig, PlatformPortfolioDrawdownControlConfig, PlatformPortfolioDrawdownController, PlatformPositionTargetRule, PlatformRebalanceSchedule, @@ -34213,6 +34738,43 @@ let target_exposure = csi_ready ? dynamic_exposure : 0.0; ); } + #[test] + fn numeric_vm_freezes_identifier_bindings_at_plan_compilation() { + let strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); + let plan = strategy.expression_eval_plan( + "market_cap > 1.0 && !is_st && profit_pct > 0.0 && custom_score > 0.0", + ); + let vm = plan.numeric_vm.as_ref().expect("numeric VM plan"); + let bindings = vm + .program + .variables() + .iter() + .zip(&vm.identifier_bindings) + .collect::>(); + + assert!(matches!( + bindings.get(&"market_cap".to_string()), + Some(NumericVmIdentifierBinding::Stock( + NumericVmStockField::MarketCap + )) + )); + assert!(matches!( + bindings.get(&"is_st".to_string()), + Some(NumericVmIdentifierBinding::Stock(NumericVmStockField::IsSt)) + )); + assert!(matches!( + bindings.get(&"profit_pct".to_string()), + Some(NumericVmIdentifierBinding::Position( + NumericVmPositionField::ProfitPct + )) + )); + assert!(matches!( + bindings.get(&"custom_score".to_string()), + Some(NumericVmIdentifierBinding::DynamicFactor(identifier)) + if identifier.as_ref() == "custom_score" + )); + } + #[test] fn numeric_vm_compiles_static_runtime_helper_arguments() { let strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation());