diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 840e17e..2fd9a4a 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -957,183 +957,6 @@ 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 { @@ -5420,18 +5243,12 @@ 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, - &vm_plan.identifier_bindings[index], - ) - .ok_or_else(|| { - NumericVmEvalError::new(format!( - "missing numeric/boolean identifier {identifier}" - )) - }) + self.numeric_vm_identifier_value(ctx, day, stock, position, identifier) + .ok_or_else(|| { + NumericVmEvalError::new(format!( + "missing numeric/boolean identifier {identifier}" + )) + }) }) .map_err(|error| { BacktestError::Execution(format!( @@ -5642,542 +5459,219 @@ 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>, - binding: &NumericVmIdentifierBinding, + identifier: &str, ) -> 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 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 { + 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 { 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; } - 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) + if day.available_factor_names.contains(identifier) { + return number(f64::NAN); } - 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 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( + 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( stock .map(|stock| { ctx.available_sellable_qty( @@ -6187,27 +5681,15 @@ impl PlatformExprStrategy { }) .unwrap_or(position.sellable_qty.max(0)), ), - NumericVmPositionField::ReservedOpenSellQty => integer( + "reserved_open_sell_qty" => integer( stock .map(|stock| ctx.symbol_open_sell_quantity(&stock.symbol) as i64) .unwrap_or(0), ), - NumericVmPositionField::ProfitPct => number(position.holding_return * 100.0), + "profit_pct" => number(position.holding_return * 100.0), + _ => None, } } - 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)) - }) - } } } @@ -6907,16 +6389,10 @@ 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, }) } @@ -13770,8 +13246,7 @@ mod tests { use chrono::{NaiveDate, NaiveTime}; use super::{ - CompiledRuntimeHelperArgs, NumericVmIdentifierBinding, NumericVmPositionField, - NumericVmStockField, PlatformAccountActionKind, PlatformExplicitActionStage, + CompiledRuntimeHelperArgs, PlatformAccountActionKind, PlatformExplicitActionStage, PlatformExplicitCancelKind, PlatformExplicitOrderKind, PlatformExprStrategy, PlatformExprStrategyConfig, PlatformPortfolioDrawdownControlConfig, PlatformPortfolioDrawdownController, PlatformPositionTargetRule, PlatformRebalanceSchedule, @@ -34738,43 +34213,6 @@ 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());