use std::collections::{BTreeMap, BTreeSet};

use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime};
use rhai::{Dynamic, Engine, Map, Scope};

use crate::cost::ChinaAShareCostModel;
use crate::data::{DailyMarketSnapshot, EligibleUniverseSnapshot, PriceField};
use crate::engine::BacktestError;
use crate::events::OrderSide;
use crate::portfolio::PortfolioState;
use crate::scheduler::{ScheduleRule, ScheduleStage, Scheduler};
use crate::strategy::{OrderIntent, Strategy, StrategyContext, StrategyDecision};

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlatformScheduleFrequency {
    Weekly {
        weekday: Option<u32>,
        tradingday: Option<i32>,
    },
    Monthly {
        tradingday: i32,
    },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlatformRebalanceSchedule {
    pub frequency: PlatformScheduleFrequency,
}

impl PlatformRebalanceSchedule {
    fn as_schedule_rule(&self) -> ScheduleRule {
        match self.frequency {
            PlatformScheduleFrequency::Weekly {
                weekday: Some(weekday),
                ..
            } => ScheduleRule::weekly_by_weekday(
                "platform_periodic_rebalance",
                weekday,
                ScheduleStage::OnDay,
            ),
            PlatformScheduleFrequency::Weekly {
                tradingday: Some(tradingday),
                ..
            } => ScheduleRule::weekly_by_tradingday(
                "platform_periodic_rebalance",
                tradingday,
                ScheduleStage::OnDay,
            ),
            PlatformScheduleFrequency::Monthly { tradingday } => ScheduleRule::monthly(
                "platform_periodic_rebalance",
                tradingday,
                ScheduleStage::OnDay,
            ),
            PlatformScheduleFrequency::Weekly {
                weekday: None,
                tradingday: None,
            } => ScheduleRule::weekly_by_weekday(
                "platform_periodic_rebalance",
                1,
                ScheduleStage::OnDay,
            ),
        }
    }

    fn matches(&self, calendar: &crate::calendar::TradingCalendar, date: NaiveDate) -> bool {
        let scheduler = Scheduler::new(calendar);
        let rule = self.as_schedule_rule();
        scheduler
            .triggered_rules(date, ScheduleStage::OnDay, std::slice::from_ref(&rule))
            .into_iter()
            .next()
            .is_some()
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlatformExplicitOrderKind {
    Shares,
    LimitShares,
    Lots,
    LimitLots,
    Value,
    LimitValue,
    Percent,
    LimitPercent,
    TargetValue,
    LimitTargetValue,
    TargetPercent,
    LimitTargetPercent,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlatformExplicitCancelKind {
    Order,
    Symbol,
    All,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlatformTradeAction {
    Order {
        kind: PlatformExplicitOrderKind,
        symbol: String,
        amount_expr: String,
        limit_price_expr: Option<String>,
        when_expr: Option<String>,
        reason: String,
    },
    Cancel {
        kind: PlatformExplicitCancelKind,
        symbol: Option<String>,
        order_id_expr: Option<String>,
        when_expr: Option<String>,
        reason: String,
    },
}

#[derive(Debug, Clone)]
pub struct PlatformExprStrategyConfig {
    pub strategy_name: String,
    pub market: String,
    pub benchmark_symbol: String,
    pub signal_symbol: String,
    pub refresh_rate: usize,
    pub max_positions: usize,
    pub prelude: String,
    pub universe_exclude: Vec<String>,
    pub market_cap_field: String,
    pub market_cap_lower_expr: String,
    pub market_cap_upper_expr: String,
    pub selection_limit_expr: String,
    pub stock_filter_expr: String,
    pub buy_scale_expr: String,
    pub exposure_expr: String,
    pub stop_loss_expr: String,
    pub take_profit_expr: String,
    pub rank_by: String,
    pub rank_expr: String,
    pub rank_desc: bool,
    pub benchmark_short_ma_days: usize,
    pub benchmark_long_ma_days: usize,
    pub stock_short_ma_days: usize,
    pub stock_mid_ma_days: usize,
    pub stock_long_ma_days: usize,
    pub skip_month_day_ranges: Vec<(u32, u32, u32)>,
    pub rebalance_schedule: Option<PlatformRebalanceSchedule>,
    pub rotation_enabled: bool,
    pub explicit_actions: Vec<PlatformTradeAction>,
}

impl PlatformExprStrategyConfig {
    pub fn microcap_rotation() -> Self {
        Self {
            strategy_name: "microcap_rotation".to_string(),
            market: "CN_A".to_string(),
            benchmark_symbol: "000852.SH".to_string(),
            signal_symbol: "000001.SH".to_string(),
            refresh_rate: 15,
            max_positions: 40,
            prelude: r#"let stocknum = 40;
let ma_ratio = 1.0001;
fn band_low(index_close) {
  round((index_close - 2000) * 4 / 500 + 7)
}"#
            .to_string(),
            universe_exclude: vec![
                "paused".to_string(),
                "st".to_string(),
                "kcb".to_string(),
                "one_yuan".to_string(),
                "new_listing".to_string(),
            ],
            market_cap_field: "market_cap".to_string(),
            market_cap_lower_expr: "band_low(signal_close)".to_string(),
            market_cap_upper_expr: "band_low(signal_close) + 10".to_string(),
            selection_limit_expr: "stocknum".to_string(),
            stock_filter_expr:
                "stock_ma_short > stock_ma_mid * ma_ratio && stock_ma_mid > stock_ma_long"
                    .to_string(),
            buy_scale_expr: "1.0".to_string(),
            exposure_expr: "1.0".to_string(),
            stop_loss_expr: String::new(),
            take_profit_expr: String::new(),
            rank_by: "market_cap".to_string(),
            rank_expr: String::new(),
            rank_desc: false,
            benchmark_short_ma_days: 5,
            benchmark_long_ma_days: 10,
            stock_short_ma_days: 5,
            stock_mid_ma_days: 10,
            stock_long_ma_days: 20,
            skip_month_day_ranges: Vec::new(),
            rebalance_schedule: None,
            rotation_enabled: true,
            explicit_actions: Vec::new(),
        }
    }

    fn in_skip_window(&self, date: NaiveDate) -> bool {
        let month = date.month();
        let day = date.day();
        self.skip_month_day_ranges
            .iter()
            .any(|(m, start_day, end_day)| month == *m && day >= *start_day && day <= *end_day)
    }
}

#[derive(Default)]
struct ProjectedExecutionState {
    execution_cursors: BTreeMap<String, NaiveDateTime>,
    intraday_turnover: BTreeMap<String, u32>,
}

#[derive(Debug, Clone, Copy)]
struct ProjectedExecutionFill {
    price: f64,
    quantity: u32,
    next_cursor: NaiveDateTime,
}

#[derive(Debug, Clone)]
struct DayExpressionState {
    date: NaiveDate,
    signal_open: f64,
    signal_close: f64,
    benchmark_open: f64,
    benchmark_close: f64,
    benchmark_ma_short: f64,
    benchmark_ma_long: f64,
    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,
    current_exposure: f64,
    position_count: i64,
    max_positions: i64,
    refresh_rate: i64,
    year: i64,
    month: i64,
    quarter: i64,
    day_of_month: i64,
    day_of_year: i64,
    week_of_year: i64,
    weekday: i64,
    is_month_start: bool,
    is_month_end: bool,
    available_factor_names: BTreeSet<String>,
}

#[derive(Debug, Clone)]
struct StockExpressionState {
    symbol: String,
    market_cap: f64,
    free_float_cap: f64,
    pe_ttm: f64,
    volume: i64,
    tick_volume: i64,
    bid1_volume: i64,
    ask1_volume: i64,
    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,
    minimum_order_quantity: i64,
    order_step_size: i64,
    paused: bool,
    is_st: bool,
    is_kcb: bool,
    is_one_yuan: bool,
    is_new_listing: bool,
    allow_buy: bool,
    allow_sell: bool,
    touched_upper_limit: bool,
    touched_lower_limit: bool,
    listed_days: i64,
    stock_ma_short: f64,
    stock_ma_mid: f64,
    stock_ma_long: f64,
    stock_ma5: f64,
    stock_ma10: f64,
    stock_ma20: f64,
    stock_ma30: f64,
    stock_volume_ma5: f64,
    stock_volume_ma10: f64,
    stock_volume_ma20: f64,
    stock_volume_ma60: f64,
    extra_factors: BTreeMap<String, f64>,
}

#[derive(Debug, Clone)]
struct PositionExpressionState {
    avg_cost: f64,
    current_price: f64,
    holding_return: f64,
    quantity: i64,
    sellable_qty: i64,
}

pub struct PlatformExprStrategy {
    config: PlatformExprStrategyConfig,
    engine: Engine,
}

impl PlatformExprStrategy {
    pub fn new(config: PlatformExprStrategyConfig) -> Self {
        let mut engine = Engine::new();
        engine.register_fn("round", |value: f64| value.round());
        engine.register_fn("floor", |value: f64| value.floor());
        engine.register_fn("ceil", |value: f64| value.ceil());
        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 }
            },
        );
        engine.register_fn("contains", |value: &str, needle: &str| {
            value.contains(needle)
        });
        engine.register_fn("starts_with", |value: &str, prefix: &str| {
            value.starts_with(prefix)
        });
        engine.register_fn("ends_with", |value: &str, suffix: &str| {
            value.ends_with(suffix)
        });
        engine.register_fn("lower", |value: &str| value.to_lowercase());
        engine.register_fn("upper", |value: &str| value.to_uppercase());
        engine.register_fn("trim", |value: &str| value.trim().to_string());
        engine.register_fn("strlen", |value: &str| value.chars().count() as i64);
        Self { config, engine }
    }

    fn is_expression_identifier(name: &str) -> bool {
        let mut chars = name.chars();
        let Some(first) = chars.next() else {
            return false;
        };
        if !(first == '_' || first.is_ascii_alphabetic()) {
            return false;
        }
        chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
    }

    fn reserved_scope_names() -> BTreeSet<&'static str> {
        BTreeSet::from([
            "signal_close",
            "benchmark_close",
            "signal_ma5",
            "signal_ma10",
            "signal_ma20",
            "signal_ma30",
            "benchmark_ma5",
            "benchmark_ma10",
            "benchmark_ma20",
            "benchmark_ma30",
            "benchmark_ma_short",
            "benchmark_ma_long",
            "cash",
            "available_cash",
            "market_value",
            "total_equity",
            "current_exposure",
            "position_count",
            "max_positions",
            "refresh_rate",
            "year",
            "month",
            "quarter",
            "day_of_month",
            "day_of_year",
            "week_of_year",
            "weekday",
            "is_month_start",
            "is_month_end",
            "day_factors",
            "symbol",
            "market_cap",
            "free_float_cap",
            "pe_ttm",
            "volume",
            "tick_volume",
            "bid1_volume",
            "ask1_volume",
            "turnover_ratio",
            "effective_turnover_ratio",
            "open",
            "close",
            "last",
            "last_price",
            "prev_close",
            "upper_limit",
            "lower_limit",
            "price_tick",
            "round_lot",
            "paused",
            "is_st",
            "is_kcb",
            "is_one_yuan",
            "is_new_listing",
            "allow_buy",
            "allow_sell",
            "touched_upper_limit",
            "touched_lower_limit",
            "hit_upper_limit",
            "hit_lower_limit",
            "listed_days",
            "stock_ma_short",
            "stock_ma_mid",
            "stock_ma_long",
            "stock_ma5",
            "stock_ma10",
            "stock_ma20",
            "stock_ma30",
            "ma5",
            "ma10",
            "ma20",
            "ma30",
            "factors",
            "avg_cost",
            "current_price",
            "holding_return",
            "quantity",
            "sellable_qty",
            "profit_pct",
            "at_upper_limit",
            "at_lower_limit",
        ])
    }

    fn price_is_at_limit(price: f64, limit: f64, tick: f64) -> bool {
        if !price.is_finite() || !limit.is_finite() {
            return false;
        }
        let tolerance = tick.abs().max(1e-6);
        (price - limit).abs() <= tolerance
    }

    fn intraday_execution_start_time(&self) -> NaiveTime {
        NaiveTime::from_hms_opt(10, 18, 0).expect("valid 10:18")
    }

    fn buy_commission(&self, gross_amount: f64) -> f64 {
        ChinaAShareCostModel::default().commission_for(gross_amount)
    }

    fn sell_cost(&self, date: NaiveDate, gross_amount: f64) -> f64 {
        let model = ChinaAShareCostModel::default();
        model.commission_for(gross_amount)
            + model.stamp_tax_for(date, OrderSide::Sell, gross_amount)
    }

    fn round_lot_quantity(
        &self,
        quantity: u32,
        minimum_order_quantity: u32,
        order_step_size: u32,
    ) -> u32 {
        let step = order_step_size.max(1);
        let normalized = (quantity / step) * step;
        if normalized < minimum_order_quantity.max(1) {
            0
        } else {
            normalized
        }
    }

    fn decrement_order_quantity(
        &self,
        quantity: u32,
        minimum_order_quantity: u32,
        order_step_size: u32,
    ) -> u32 {
        let minimum = minimum_order_quantity.max(1);
        if quantity <= minimum {
            0
        } else {
            let next = quantity.saturating_sub(order_step_size.max(1));
            if next < minimum { 0 } else { next }
        }
    }

    fn projected_round_lot(&self, ctx: &StrategyContext<'_>, symbol: &str) -> u32 {
        ctx.data
            .instrument(symbol)
            .map(|instrument| instrument.effective_round_lot())
            .unwrap_or(100)
            .max(1)
    }

    fn projected_minimum_order_quantity(&self, ctx: &StrategyContext<'_>, symbol: &str) -> u32 {
        ctx.data
            .instrument(symbol)
            .map(|instrument| instrument.minimum_order_quantity())
            .unwrap_or(100)
            .max(1)
    }

    fn projected_order_step_size(&self, ctx: &StrategyContext<'_>, symbol: &str) -> u32 {
        ctx.data
            .instrument(symbol)
            .map(|instrument| instrument.order_step_size())
            .unwrap_or(100)
            .max(1)
    }

    fn projected_execution_price(&self, market: &DailyMarketSnapshot, side: OrderSide) -> f64 {
        match side {
            OrderSide::Buy => market.buy_price(PriceField::Last),
            OrderSide::Sell => market.sell_price(PriceField::Last),
        }
    }

    fn projected_execution_start_cursor(
        &self,
        date: NaiveDate,
        _symbol: &str,
        _execution_state: &ProjectedExecutionState,
    ) -> NaiveDateTime {
        date.and_time(self.intraday_execution_start_time())
    }

    fn projected_select_execution_fill(
        &self,
        ctx: &StrategyContext<'_>,
        date: NaiveDate,
        symbol: &str,
        side: OrderSide,
        requested_qty: u32,
        round_lot: u32,
        minimum_order_quantity: u32,
        order_step_size: u32,
        allow_odd_lot_sell: bool,
        cash_limit: Option<f64>,
        gross_limit: Option<f64>,
        execution_state: &ProjectedExecutionState,
    ) -> Option<ProjectedExecutionFill> {
        if requested_qty == 0 {
            return None;
        }

        if let Some(market) = ctx.data.market(date, symbol) {
            let execution_price = self.projected_execution_price(market, side);
            if execution_price.is_finite() && execution_price > 0.0 {
                let quantity = match side {
                    OrderSide::Buy => {
                        let cash = cash_limit.unwrap_or(f64::INFINITY);
                        let mut take_qty = self.round_lot_quantity(
                            requested_qty,
                            minimum_order_quantity,
                            order_step_size,
                        );
                        while take_qty > 0 {
                            let candidate_gross = execution_price * take_qty as f64;
                            if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
                                take_qty = self.decrement_order_quantity(
                                    take_qty,
                                    minimum_order_quantity,
                                    order_step_size,
                                );
                                continue;
                            }
                            let candidate_cash =
                                candidate_gross + self.buy_commission(candidate_gross);
                            if candidate_cash <= cash + 1e-6 {
                                break;
                            }
                            take_qty = self.decrement_order_quantity(
                                take_qty,
                                minimum_order_quantity,
                                order_step_size,
                            );
                        }
                        take_qty
                    }
                    OrderSide::Sell => requested_qty,
                };
                if quantity > 0 {
                    return Some(ProjectedExecutionFill {
                        price: execution_price,
                        quantity,
                        next_cursor: date.and_time(self.intraday_execution_start_time())
                            + Duration::seconds(1),
                    });
                }
            }
        }

        let start_cursor = self.projected_execution_start_cursor(date, symbol, execution_state);
        let quotes = ctx.data.execution_quotes_on(date, symbol);
        let mut filled_qty = 0_u32;
        let mut gross_amount = 0.0_f64;
        let mut last_timestamp = None;

        for quote in quotes {
            if quote.timestamp < start_cursor {
                continue;
            }
            let fallback_quote_price = match side {
                OrderSide::Buy => quote.buy_price(),
                OrderSide::Sell => quote.sell_price(),
            };
            let Some(quote_price) = fallback_quote_price else {
                continue;
            };
            let available_qty = match side {
                OrderSide::Buy => quote.ask1_volume,
                OrderSide::Sell => quote.bid1_volume,
            }
            .saturating_mul(round_lot.max(1) as u64)
            .min(u32::MAX as u64) as u32;
            if available_qty == 0 {
                continue;
            }

            let remaining_qty = requested_qty.saturating_sub(filled_qty);
            if remaining_qty == 0 {
                break;
            }
            let mut take_qty = remaining_qty.min(available_qty);
            if !(side == OrderSide::Sell && allow_odd_lot_sell && take_qty == remaining_qty) {
                take_qty =
                    self.round_lot_quantity(take_qty, minimum_order_quantity, order_step_size);
            }
            if take_qty == 0 {
                continue;
            }

            if let Some(cash) = cash_limit {
                while take_qty > 0 {
                    let candidate_gross = gross_amount + quote_price * take_qty as f64;
                    if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
                        take_qty = self.decrement_order_quantity(
                            take_qty,
                            minimum_order_quantity,
                            order_step_size,
                        );
                        continue;
                    }
                    if candidate_gross + self.buy_commission(candidate_gross) <= cash + 1e-6 {
                        break;
                    }
                    take_qty = self.decrement_order_quantity(
                        take_qty,
                        minimum_order_quantity,
                        order_step_size,
                    );
                }
                if take_qty == 0 {
                    break;
                }
            }

            gross_amount += quote_price * take_qty as f64;
            filled_qty += take_qty;
            last_timestamp = Some(quote.timestamp);
            if filled_qty >= requested_qty {
                break;
            }
        }

        if filled_qty == 0 {
            return None;
        }
        Some(ProjectedExecutionFill {
            price: gross_amount / filled_qty as f64,
            quantity: filled_qty,
            next_cursor: last_timestamp.unwrap_or(start_cursor) + Duration::seconds(1),
        })
    }

    fn project_target_zero(
        &self,
        ctx: &StrategyContext<'_>,
        projected: &mut PortfolioState,
        date: NaiveDate,
        symbol: &str,
        execution_state: &mut ProjectedExecutionState,
    ) -> Option<u32> {
        let quantity = projected.position(symbol)?.quantity;
        if quantity == 0 {
            return None;
        }
        let market = ctx.data.market(date, symbol)?;
        let round_lot = self.projected_round_lot(ctx, symbol);
        let minimum_order_quantity = self.projected_minimum_order_quantity(ctx, symbol);
        let order_step_size = self.projected_order_step_size(ctx, symbol);
        let fill = self
            .projected_select_execution_fill(
                ctx,
                date,
                symbol,
                OrderSide::Sell,
                quantity,
                round_lot,
                minimum_order_quantity,
                order_step_size,
                true,
                None,
                None,
                execution_state,
            )
            .unwrap_or(ProjectedExecutionFill {
                price: self.projected_execution_price(market, OrderSide::Sell),
                quantity,
                next_cursor: date.and_time(self.intraday_execution_start_time())
                    + Duration::seconds(1),
            });
        let gross_amount = fill.price * fill.quantity as f64;
        let net_cash = gross_amount - self.sell_cost(date, gross_amount);
        projected
            .position_mut(symbol)
            .sell(fill.quantity, fill.price)
            .ok()?;
        projected.apply_cash_delta(net_cash);
        *execution_state
            .intraday_turnover
            .entry(symbol.to_string())
            .or_default() += fill.quantity;
        execution_state
            .execution_cursors
            .insert(symbol.to_string(), fill.next_cursor);
        projected.prune_flat_positions();
        Some(fill.quantity)
    }

    fn project_order_value(
        &self,
        ctx: &StrategyContext<'_>,
        projected: &mut PortfolioState,
        date: NaiveDate,
        symbol: &str,
        order_value: f64,
        execution_state: &mut ProjectedExecutionState,
    ) -> u32 {
        if order_value <= 0.0 {
            return 0;
        }
        let round_lot = self.projected_round_lot(ctx, symbol);
        let minimum_order_quantity = self.projected_minimum_order_quantity(ctx, symbol);
        let order_step_size = self.projected_order_step_size(ctx, symbol);
        let market = match ctx.data.market(date, symbol) {
            Some(market) => market,
            None => return 0,
        };
        let sizing_price = market.price(PriceField::Last);
        if !sizing_price.is_finite() || sizing_price <= 0.0 {
            return 0;
        }
        let snapshot_requested_qty = self.round_lot_quantity(
            ((projected.cash().min(order_value)) / sizing_price).floor() as u32,
            minimum_order_quantity,
            order_step_size,
        );
        let execution_price = self.projected_execution_price(market, OrderSide::Buy);
        let mut quantity = snapshot_requested_qty;
        while quantity > 0 {
            let gross_amount = execution_price * quantity as f64;
            if gross_amount <= order_value + 400.0
                && gross_amount + self.buy_commission(gross_amount) <= projected.cash() + 1e-6
            {
                break;
            }
            quantity =
                self.decrement_order_quantity(quantity, minimum_order_quantity, order_step_size);
        }
        if quantity == 0 {
            return 0;
        }
        let fill = self
            .projected_select_execution_fill(
                ctx,
                date,
                symbol,
                OrderSide::Buy,
                quantity,
                round_lot,
                minimum_order_quantity,
                order_step_size,
                false,
                Some(projected.cash()),
                Some(order_value + 400.0),
                execution_state,
            )
            .unwrap_or(ProjectedExecutionFill {
                price: execution_price,
                quantity,
                next_cursor: date.and_time(self.intraday_execution_start_time())
                    + Duration::seconds(1),
            });
        let gross_amount = fill.price * fill.quantity as f64;
        let cash_out = gross_amount + self.buy_commission(gross_amount);
        if cash_out > projected.cash() + 1e-6 {
            return 0;
        }
        projected.apply_cash_delta(-cash_out);
        projected
            .position_mut(symbol)
            .buy(date, fill.quantity, fill.price);
        *execution_state
            .intraday_turnover
            .entry(symbol.to_string())
            .or_default() += fill.quantity;
        execution_state
            .execution_cursors
            .insert(symbol.to_string(), fill.next_cursor);
        fill.quantity
    }

    fn special_name(&self, ctx: &StrategyContext<'_>, symbol: &str) -> bool {
        let instrument_name = ctx
            .data
            .instruments()
            .get(symbol)
            .map(|instrument| instrument.name.as_str())
            .unwrap_or("");
        instrument_name.contains("ST")
            || instrument_name.contains('*')
            || instrument_name.contains('退')
    }

    fn day_state(
        &self,
        ctx: &StrategyContext<'_>,
        date: NaiveDate,
    ) -> Result<DayExpressionState, BacktestError> {
        let signal_open = ctx
            .data
            .market(date, &self.config.signal_symbol)
            .ok_or_else(|| BacktestError::MissingPrice {
                date,
                symbol: self.config.signal_symbol.clone(),
                field: "open",
            })?
            .open;
        let signal_close = ctx
            .data
            .market_decision_close(date, &self.config.signal_symbol)
            .ok_or_else(|| BacktestError::MissingPrice {
                date,
                symbol: self.config.signal_symbol.clone(),
                field: "decision_close",
            })?;
        let benchmark = ctx
            .data
            .benchmark(date)
            .ok_or(BacktestError::MissingBenchmark { date })?;
        let benchmark_open = benchmark.open;
        let benchmark_close = benchmark.close;
        let benchmark_ma_short = ctx
            .data
            .market_decision_close_moving_average(
                date,
                &self.config.signal_symbol,
                self.config.benchmark_short_ma_days,
            )
            .ok_or_else(|| {
                BacktestError::Execution(format!(
                    "insufficient benchmark short MA history for {} on {}",
                    self.config.signal_symbol, date
                ))
            })?;
        let benchmark_ma_long = ctx
            .data
            .market_decision_close_moving_average(
                date,
                &self.config.signal_symbol,
                self.config.benchmark_long_ma_days,
            )
            .ok_or_else(|| {
                BacktestError::Execution(format!(
                    "insufficient benchmark long MA history for {} on {}",
                    self.config.signal_symbol, date
                ))
            })?;
        let benchmark_ma5 = ctx
            .data
            .market_decision_close_moving_average(date, &self.config.signal_symbol, 5)
            .unwrap_or(benchmark_ma_short);
        let benchmark_ma10 = ctx
            .data
            .market_decision_close_moving_average(date, &self.config.signal_symbol, 10)
            .unwrap_or(benchmark_ma_long);
        let benchmark_ma20 = ctx
            .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::<f64>();
        let total_equity = cash + market_value;
        let current_exposure = if total_equity > 0.0 {
            market_value / total_equity
        } else {
            0.0
        };
        let next_day = date + Duration::days(1);
        let is_month_end = next_day.month() != date.month();

        Ok(DayExpressionState {
            date,
            signal_open,
            signal_close,
            benchmark_open,
            benchmark_close,
            benchmark_ma_short,
            benchmark_ma_long,
            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,
            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,
            quarter: ((date.month0() / 3) + 1) as i64,
            day_of_month: date.day() as i64,
            day_of_year: date.ordinal() as i64,
            week_of_year: date.iso_week().week() as i64,
            weekday: date.weekday().number_from_monday() as i64,
            is_month_start: date.day() == 1,
            is_month_end,
            available_factor_names: ctx
                .data
                .factor_snapshots_on(date)
                .into_iter()
                .flat_map(|row| row.extra_factors.keys().cloned())
                .collect(),
        })
    }

    fn stock_state(
        &self,
        ctx: &StrategyContext<'_>,
        date: NaiveDate,
        symbol: &str,
    ) -> Result<StockExpressionState, BacktestError> {
        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)
            .unwrap_or(0.0);
        let stock_ma_mid = ctx
            .data
            .market_decision_close_moving_average(date, symbol, self.config.stock_mid_ma_days)
            .unwrap_or(0.0);
        let stock_ma_long = ctx
            .data
            .market_decision_close_moving_average(date, symbol, self.config.stock_long_ma_days)
            .unwrap_or(0.0);
        let stock_ma5 = ctx
            .data
            .market_decision_close_moving_average(date, symbol, 5)
            .unwrap_or(stock_ma_short);
        let stock_ma10 = ctx
            .data
            .market_decision_close_moving_average(date, symbol, 10)
            .unwrap_or(stock_ma_mid);
        let stock_ma20 = ctx
            .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);
        let stock_volume_ma5 = ctx
            .data
            .market_decision_volume_moving_average(date, symbol, 5)
            .unwrap_or(market.volume as f64);
        let stock_volume_ma10 = ctx
            .data
            .market_decision_volume_moving_average(date, symbol, 10)
            .unwrap_or(stock_volume_ma5);
        let stock_volume_ma20 = ctx
            .data
            .market_decision_volume_moving_average(date, symbol, 20)
            .unwrap_or(stock_volume_ma10);
        let stock_volume_ma60 = ctx
            .data
            .market_decision_volume_moving_average(date, symbol, 60)
            .unwrap_or(stock_volume_ma20);
        let touched_upper_limit = factor
            .extra_factors
            .get("touched_upper_limit")
            .or_else(|| factor.extra_factors.get("hit_upper_limit"))
            .copied()
            .unwrap_or_default()
            >= 0.5;
        let touched_lower_limit = factor
            .extra_factors
            .get("touched_lower_limit")
            .or_else(|| factor.extra_factors.get("hit_lower_limit"))
            .copied()
            .unwrap_or_default()
            >= 0.5;

        Ok(StockExpressionState {
            symbol: symbol.to_string(),
            market_cap: factor.market_cap_bn,
            free_float_cap: factor.free_float_cap_bn,
            pe_ttm: factor.pe_ttm,
            volume: market.volume as i64,
            tick_volume: market.tick_volume as i64,
            bid1_volume: market.bid1_volume as i64,
            ask1_volume: market.ask1_volume as i64,
            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,
            minimum_order_quantity: instrument
                .map(|item| item.minimum_order_quantity())
                .unwrap_or(100) as i64,
            order_step_size: instrument.map(|item| item.order_step_size()).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,
            is_one_yuan: candidate.is_one_yuan || market.day_open <= 1.0,
            is_new_listing: candidate.is_new_listing,
            allow_buy: candidate.allow_buy,
            allow_sell: candidate.allow_sell,
            touched_upper_limit,
            touched_lower_limit,
            listed_days: if candidate.is_new_listing { 0 } else { 365 },
            stock_ma_short,
            stock_ma_mid,
            stock_ma_long,
            stock_ma5,
            stock_ma10,
            stock_ma20,
            stock_ma30,
            stock_volume_ma5,
            stock_volume_ma10,
            stock_volume_ma20,
            stock_volume_ma60,
            extra_factors: factor.extra_factors.clone(),
        })
    }

    fn eval_scope(
        &self,
        day: &DayExpressionState,
        stock: Option<&StockExpressionState>,
        position: Option<&PositionExpressionState>,
    ) -> Scope<'static> {
        let mut scope = Scope::new();
        scope.push("signal_open", day.signal_open);
        scope.push("signal_close", day.signal_close);
        scope.push("benchmark_open", day.benchmark_open);
        scope.push("benchmark_close", day.benchmark_close);
        scope.push("signal_ma5", day.signal_ma5);
        scope.push("signal_ma10", day.signal_ma10);
        scope.push("signal_ma20", day.signal_ma20);
        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);
        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("quarter", day.quarter);
        scope.push("day_of_month", day.day_of_month);
        scope.push("day_of_year", day.day_of_year);
        scope.push("week_of_year", day.week_of_year);
        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_open".into(), Dynamic::from(day.signal_open));
        day_factors.insert("signal_close".into(), Dynamic::from(day.signal_close));
        day_factors.insert("benchmark_open".into(), Dynamic::from(day.benchmark_open));
        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 {
            let at_upper_limit =
                Self::price_is_at_limit(stock.last, stock.upper_limit, stock.price_tick);
            let at_lower_limit =
                Self::price_is_at_limit(stock.last, stock.lower_limit, stock.price_tick);
            scope.push("symbol", stock.symbol.clone());
            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("volume", stock.volume);
            scope.push("tick_volume", stock.tick_volume);
            scope.push("bid1_volume", stock.bid1_volume);
            scope.push("ask1_volume", stock.ask1_volume);
            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);
            scope.push("last_price", stock.last);
            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("minimum_order_quantity", stock.minimum_order_quantity);
            scope.push("order_step_size", stock.order_step_size);
            scope.push("paused", stock.paused);
            scope.push("is_st", stock.is_st);
            scope.push("is_kcb", stock.is_kcb);
            scope.push("is_one_yuan", stock.is_one_yuan);
            scope.push("is_new_listing", stock.is_new_listing);
            scope.push("allow_buy", stock.allow_buy);
            scope.push("allow_sell", stock.allow_sell);
            scope.push("touched_upper_limit", stock.touched_upper_limit);
            scope.push("touched_lower_limit", stock.touched_lower_limit);
            scope.push("hit_upper_limit", stock.touched_upper_limit);
            scope.push("hit_lower_limit", stock.touched_lower_limit);
            scope.push("listed_days", stock.listed_days);
            scope.push("at_upper_limit", at_upper_limit);
            scope.push("at_lower_limit", at_lower_limit);
            scope.push("stock_ma_short", stock.stock_ma_short);
            scope.push("stock_ma_mid", stock.stock_ma_mid);
            scope.push("stock_ma_long", stock.stock_ma_long);
            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);
            scope.push("stock_volume_ma5", stock.stock_volume_ma5);
            scope.push("stock_volume_ma10", stock.stock_volume_ma10);
            scope.push("stock_volume_ma20", stock.stock_volume_ma20);
            scope.push("stock_volume_ma60", stock.stock_volume_ma60);
            scope.push("volume_ma5", stock.stock_volume_ma5);
            scope.push("volume_ma10", stock.stock_volume_ma10);
            scope.push("volume_ma20", stock.stock_volume_ma20);
            scope.push("volume_ma60", stock.stock_volume_ma60);
            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("volume".into(), Dynamic::from(stock.volume));
            factors.insert("tick_volume".into(), Dynamic::from(stock.tick_volume));
            factors.insert("bid1_volume".into(), Dynamic::from(stock.bid1_volume));
            factors.insert("ask1_volume".into(), Dynamic::from(stock.ask1_volume));
            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(
                "minimum_order_quantity".into(),
                Dynamic::from(stock.minimum_order_quantity),
            );
            factors.insert(
                "order_step_size".into(),
                Dynamic::from(stock.order_step_size),
            );
            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(
                "touched_upper_limit".into(),
                Dynamic::from(stock.touched_upper_limit),
            );
            factors.insert(
                "touched_lower_limit".into(),
                Dynamic::from(stock.touched_lower_limit),
            );
            factors.insert(
                "hit_upper_limit".into(),
                Dynamic::from(stock.touched_upper_limit),
            );
            factors.insert(
                "hit_lower_limit".into(),
                Dynamic::from(stock.touched_lower_limit),
            );
            factors.insert("listed_days".into(), Dynamic::from(stock.listed_days));
            factors.insert("at_upper_limit".into(), Dynamic::from(at_upper_limit));
            factors.insert("at_lower_limit".into(), Dynamic::from(at_lower_limit));
            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));
            factors.insert(
                "stock_volume_ma5".into(),
                Dynamic::from(stock.stock_volume_ma5),
            );
            factors.insert(
                "stock_volume_ma10".into(),
                Dynamic::from(stock.stock_volume_ma10),
            );
            factors.insert(
                "stock_volume_ma20".into(),
                Dynamic::from(stock.stock_volume_ma20),
            );
            factors.insert(
                "stock_volume_ma60".into(),
                Dynamic::from(stock.stock_volume_ma60),
            );
            factors.insert("volume_ma5".into(), Dynamic::from(stock.stock_volume_ma5));
            factors.insert("volume_ma10".into(), Dynamic::from(stock.stock_volume_ma10));
            factors.insert("volume_ma20".into(), Dynamic::from(stock.stock_volume_ma20));
            factors.insert("volume_ma60".into(), Dynamic::from(stock.stock_volume_ma60));
            for (key, value) in &stock.extra_factors {
                factors.insert(key.clone().into(), Dynamic::from(*value));
            }
            scope.push("factors", factors);
            let reserved_names = Self::reserved_scope_names();
            for (key, value) in &stock.extra_factors {
                if Self::is_expression_identifier(key) && !reserved_names.contains(key.as_str()) {
                    scope.push_dynamic(key.clone(), Dynamic::from(*value));
                }
            }
            for key in &day.available_factor_names {
                if Self::is_expression_identifier(key)
                    && !reserved_names.contains(key.as_str())
                    && !stock.extra_factors.contains_key(key)
                {
                    scope.push_dynamic(key.clone(), Dynamic::from(0.0));
                }
            }
        }
        if let Some(position) = position {
            scope.push("avg_cost", position.avg_cost);
            scope.push("current_price", position.current_price);
            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
    }

    fn eval_dynamic(
        &self,
        ctx: &StrategyContext<'_>,
        expr: &str,
        day: &DayExpressionState,
        stock: Option<&StockExpressionState>,
        position: Option<&PositionExpressionState>,
    ) -> Result<Dynamic, BacktestError> {
        let mut scope = self.eval_scope(day, stock, position);
        let normalized_expr = Self::normalize_expr(expr);
        let expanded_expr = self.expand_runtime_helpers(ctx, day, stock, &normalized_expr)?;
        let prelude_declared_identifiers = Self::declared_prelude_identifiers(&self.config.prelude);
        if let Some(item) = stock {
            let reserved_names = Self::reserved_scope_names();
            for identifier in Self::extract_identifier_candidates(&expanded_expr) {
                if reserved_names.contains(identifier.as_str())
                    || prelude_declared_identifiers.contains(&identifier)
                    || !day.available_factor_names.contains(&identifier)
                {
                    continue;
                }
                let value = item.extra_factors.get(&identifier).copied().unwrap_or(0.0);
                scope.push_dynamic(identifier, Dynamic::from(value));
            }
        }
        let factor_alias_prelude = stock
            .map(|item| {
                let reserved_names = Self::reserved_scope_names();
                item.extra_factors
                    .keys()
                    .filter(|key| {
                        Self::is_expression_identifier(key)
                            && !reserved_names.contains(key.as_str())
                    })
                    .map(|key| format!("let {key} = factors[\"{key}\"];"))
                    .collect::<Vec<_>>()
                    .join("\n")
            })
            .filter(|value| !value.trim().is_empty());
        let mut script_parts = Vec::new();
        if !self.config.prelude.trim().is_empty() {
            script_parts.push(self.config.prelude.trim().to_string());
        }
        if let Some(alias_prelude) = factor_alias_prelude {
            script_parts.push(alias_prelude);
        }
        script_parts.push(expanded_expr);
        let script = script_parts.join("\n");
        self.engine
            .eval_with_scope::<Dynamic>(&mut scope, &script)
            .map_err(|error| {
                BacktestError::Execution(format!("platform expr eval failed: {}", error))
            })
    }

    fn normalize_expr(expr: &str) -> String {
        Self::rewrite_ternary(expr.trim())
    }

    fn expand_runtime_helpers(
        &self,
        ctx: &StrategyContext<'_>,
        day: &DayExpressionState,
        stock: Option<&StockExpressionState>,
        expr: &str,
    ) -> Result<String, BacktestError> {
        let mut output = String::with_capacity(expr.len());
        let mut cursor = 0usize;
        while cursor < expr.len() {
            let Some(ch) = expr[cursor..].chars().next() else {
                break;
            };
            if !(ch == '_' || ch.is_ascii_alphabetic()) {
                output.push(ch);
                cursor += ch.len_utf8();
                continue;
            }
            let ident_start = cursor;
            cursor += ch.len_utf8();
            while cursor < expr.len() {
                let Some(next) = expr[cursor..].chars().next() else {
                    break;
                };
                if next == '_' || next.is_ascii_alphanumeric() {
                    cursor += next.len_utf8();
                } else {
                    break;
                }
            }
            let ident = &expr[ident_start..cursor];
            let whitespace_start = cursor;
            while cursor < expr.len() {
                let Some(next) = expr[cursor..].chars().next() else {
                    break;
                };
                if next.is_whitespace() {
                    cursor += next.len_utf8();
                } else {
                    break;
                }
            }
            let Some(next) = expr[cursor..].chars().next() else {
                output.push_str(&expr[ident_start..cursor]);
                break;
            };
            if next != '(' || !matches!(ident, "factor" | "day_factor" | "rolling_mean" | "sma") {
                output.push_str(&expr[ident_start..cursor]);
                continue;
            }
            let Some(close_idx) = Self::find_matching_paren(expr, cursor) else {
                return Err(BacktestError::Execution(format!(
                    "platform helper call not closed: {}",
                    &expr[ident_start..]
                )));
            };
            let inner = &expr[cursor + 1..close_idx];
            let replacement = self.resolve_runtime_helper(ctx, day, stock, ident, inner)?;
            output.push_str(&replacement);
            cursor = close_idx + 1;
            let _ = whitespace_start;
        }
        Ok(output)
    }

    fn resolve_runtime_helper(
        &self,
        ctx: &StrategyContext<'_>,
        day: &DayExpressionState,
        stock: Option<&StockExpressionState>,
        helper: &str,
        args_src: &str,
    ) -> Result<String, BacktestError> {
        let args = Self::split_top_level_args(args_src);
        match helper {
            "factor" => {
                let key = Self::parse_string_or_identifier(
                    args.first().map(String::as_str).unwrap_or_default(),
                )?;
                Ok(format!("factors[{}]", Self::quote_rhai_string(&key)))
            }
            "day_factor" => {
                let key = Self::parse_string_or_identifier(
                    args.first().map(String::as_str).unwrap_or_default(),
                )?;
                Ok(format!("day_factors[{}]", Self::quote_rhai_string(&key)))
            }
            "rolling_mean" | "sma" => {
                if args.len() != 2 {
                    return Err(BacktestError::Execution(format!(
                        "{helper} expects 2 arguments"
                    )));
                }
                let field = Self::parse_string_or_identifier(&args[0])?;
                let lookback = Self::parse_positive_usize(&args[1])?;
                let value = self.resolve_rolling_mean(ctx, day, stock, &field, lookback)?;
                Ok(format!("{value:.12}"))
            }
            other => Err(BacktestError::Execution(format!(
                "unsupported platform helper: {other}"
            ))),
        }
    }

    fn resolve_rolling_mean(
        &self,
        ctx: &StrategyContext<'_>,
        day: &DayExpressionState,
        stock: Option<&StockExpressionState>,
        field: &str,
        lookback: usize,
    ) -> Result<f64, BacktestError> {
        if lookback == 0 {
            return Err(BacktestError::Execution(
                "rolling_mean lookback must be positive".to_string(),
            ));
        }
        let value = match field {
            "benchmark_open" => ctx.data.benchmark_open_moving_average(day.date, lookback),
            "benchmark_close" => ctx.data.benchmark_moving_average(day.date, lookback),
            "signal_open" => {
                ctx.data
                    .market_open_moving_average(day.date, &self.config.signal_symbol, lookback)
            }
            "signal_close" => ctx.data.market_decision_close_moving_average(
                day.date,
                &self.config.signal_symbol,
                lookback,
            ),
            "signal_volume" => ctx.data.market_decision_volume_moving_average(
                day.date,
                &self.config.signal_symbol,
                lookback,
            ),
            other => {
                let stock = stock.ok_or_else(|| {
                    BacktestError::Execution(format!(
                        "rolling_mean(\"{other}\", {lookback}) requires stock context"
                    ))
                })?;
                ctx.data.market_decision_numeric_moving_average(
                    day.date,
                    &stock.symbol,
                    other,
                    lookback,
                )
            }
        };
        value.ok_or_else(|| {
            BacktestError::Execution(format!(
                "missing rolling mean for field {field} with lookback {lookback}"
            ))
        })
    }

    fn split_top_level_args(args: &str) -> Vec<String> {
        let mut parts = Vec::new();
        let mut start = 0usize;
        let mut paren_depth = 0i32;
        let mut brace_depth = 0i32;
        let mut bracket_depth = 0i32;
        let mut in_single_quote = false;
        let mut in_double_quote = false;
        let mut escaped = false;
        for (idx, ch) in args.char_indices() {
            if escaped {
                escaped = false;
                continue;
            }
            match ch {
                '\\' if in_single_quote || in_double_quote => {
                    escaped = true;
                }
                '\'' if !in_double_quote => {
                    in_single_quote = !in_single_quote;
                }
                '"' if !in_single_quote => {
                    in_double_quote = !in_double_quote;
                }
                _ if in_single_quote || in_double_quote => {}
                '(' => paren_depth += 1,
                ')' => paren_depth -= 1,
                '{' => brace_depth += 1,
                '}' => brace_depth -= 1,
                '[' => bracket_depth += 1,
                ']' => bracket_depth -= 1,
                ',' if paren_depth == 0 && brace_depth == 0 && bracket_depth == 0 => {
                    let part = args[start..idx].trim();
                    if !part.is_empty() {
                        parts.push(part.to_string());
                    }
                    start = idx + ch.len_utf8();
                }
                _ => {}
            }
        }
        let tail = args[start..].trim();
        if !tail.is_empty() {
            parts.push(tail.to_string());
        }
        parts
    }

    fn parse_string_or_identifier(raw: &str) -> Result<String, BacktestError> {
        let trimmed = raw.trim();
        if trimmed.is_empty() {
            return Err(BacktestError::Execution(
                "platform helper argument cannot be empty".to_string(),
            ));
        }
        if (trimmed.starts_with('"') && trimmed.ends_with('"'))
            || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
        {
            return Ok(trimmed[1..trimmed.len() - 1].to_string());
        }
        if Self::is_expression_identifier(trimmed) {
            return Ok(trimmed.to_string());
        }
        Err(BacktestError::Execution(format!(
            "platform helper expects a factor name or string literal, got {trimmed}"
        )))
    }

    fn parse_positive_usize(raw: &str) -> Result<usize, BacktestError> {
        let trimmed = raw.trim();
        let value = trimmed.parse::<usize>().map_err(|_| {
            BacktestError::Execution(format!(
                "platform helper expects a positive integer lookback, got {trimmed}"
            ))
        })?;
        if value == 0 {
            return Err(BacktestError::Execution(
                "platform helper lookback must be positive".to_string(),
            ));
        }
        Ok(value)
    }

    fn quote_rhai_string(value: &str) -> String {
        let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
        format!("\"{escaped}\"")
    }

    fn find_matching_paren(expr: &str, open_idx: usize) -> Option<usize> {
        let mut paren_depth = 0i32;
        let mut in_single_quote = false;
        let mut in_double_quote = false;
        let mut escaped = false;
        for (idx, ch) in expr[open_idx..].char_indices() {
            let absolute_idx = open_idx + idx;
            if escaped {
                escaped = false;
                continue;
            }
            match ch {
                '\\' if in_single_quote || in_double_quote => {
                    escaped = true;
                }
                '\'' if !in_double_quote => {
                    in_single_quote = !in_single_quote;
                }
                '"' if !in_single_quote => {
                    in_double_quote = !in_double_quote;
                }
                _ if in_single_quote || in_double_quote => {}
                '(' => paren_depth += 1,
                ')' => {
                    paren_depth -= 1;
                    if paren_depth == 0 {
                        return Some(absolute_idx);
                    }
                }
                _ => {}
            }
        }
        None
    }

    fn declared_prelude_identifiers(prelude: &str) -> BTreeSet<String> {
        prelude
            .lines()
            .filter_map(|line| {
                let trimmed = line.trim_start();
                let body = if let Some(rest) = trimmed.strip_prefix("let ") {
                    rest
                } else if let Some(rest) = trimmed.strip_prefix("fn ") {
                    rest
                } else {
                    return None;
                };
                let identifier: String = body
                    .chars()
                    .take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_')
                    .collect();
                (!identifier.is_empty()).then_some(identifier)
            })
            .collect()
    }

    fn extract_identifier_candidates(expr: &str) -> BTreeSet<String> {
        let mut identifiers = BTreeSet::new();
        let mut chars = expr.chars().peekable();
        let mut in_single_quote = false;
        let mut in_double_quote = false;
        let mut escaped = false;
        while let Some(ch) = chars.next() {
            if escaped {
                escaped = false;
                continue;
            }
            if ch == '\\' && (in_single_quote || in_double_quote) {
                escaped = true;
                continue;
            }
            if ch == '\'' && !in_double_quote {
                in_single_quote = !in_single_quote;
                continue;
            }
            if ch == '"' && !in_single_quote {
                in_double_quote = !in_double_quote;
                continue;
            }
            if in_single_quote || in_double_quote {
                continue;
            }
            if ch.is_ascii_alphabetic() || ch == '_' {
                let mut identifier = String::from(ch);
                while let Some(next) = chars.peek().copied() {
                    if next.is_ascii_alphanumeric() || next == '_' {
                        identifier.push(next);
                        chars.next();
                    } else {
                        break;
                    }
                }
                identifiers.insert(identifier);
            }
        }
        identifiers
    }

    fn rewrite_ternary(expr: &str) -> String {
        let Some((question_idx, colon_idx)) = Self::find_top_level_ternary(expr) else {
            return expr.trim().to_string();
        };
        let condition = Self::rewrite_ternary(expr[..question_idx].trim());
        let when_true = Self::rewrite_ternary(expr[question_idx + 1..colon_idx].trim());
        let when_false = Self::rewrite_ternary(expr[colon_idx + 1..].trim());
        format!(
            "if {} {{ {} }} else {{ {} }}",
            condition, when_true, when_false
        )
    }

    fn find_top_level_ternary(expr: &str) -> Option<(usize, usize)> {
        let mut paren_depth = 0i32;
        let mut brace_depth = 0i32;
        let mut bracket_depth = 0i32;
        let mut in_single_quote = false;
        let mut in_double_quote = false;
        let mut escaped = false;
        let mut question_stack = Vec::new();

        for (idx, ch) in expr.char_indices() {
            if escaped {
                escaped = false;
                continue;
            }
            match ch {
                '\\' if in_single_quote || in_double_quote => {
                    escaped = true;
                }
                '\'' if !in_double_quote => {
                    in_single_quote = !in_single_quote;
                }
                '"' if !in_single_quote => {
                    in_double_quote = !in_double_quote;
                }
                _ if in_single_quote || in_double_quote => {}
                '(' => paren_depth += 1,
                ')' => paren_depth -= 1,
                '{' => brace_depth += 1,
                '}' => brace_depth -= 1,
                '[' => bracket_depth += 1,
                ']' => bracket_depth -= 1,
                '?' if paren_depth == 0 && brace_depth == 0 && bracket_depth == 0 => {
                    question_stack.push(idx);
                }
                ':' if paren_depth == 0 && brace_depth == 0 && bracket_depth == 0 => {
                    if let Some(question_idx) = question_stack.pop() {
                        if question_stack.is_empty() {
                            return Some((question_idx, idx));
                        }
                    }
                }
                _ => {}
            }
        }
        None
    }

    fn eval_float(
        &self,
        ctx: &StrategyContext<'_>,
        expr: &str,
        day: &DayExpressionState,
        stock: Option<&StockExpressionState>,
        position: Option<&PositionExpressionState>,
    ) -> Result<f64, BacktestError> {
        let value = self.eval_dynamic(ctx, expr, day, stock, position)?;
        if let Some(number) = value.clone().try_cast::<f64>() {
            return Ok(number);
        }
        if let Some(number) = value.clone().try_cast::<i64>() {
            return Ok(number as f64);
        }
        if let Some(boolean) = value.try_cast::<bool>() {
            return Ok(if boolean { 1.0 } else { 0.0 });
        }
        Err(BacktestError::Execution(format!(
            "platform expr did not produce a number: {}",
            expr
        )))
    }

    fn eval_bool(
        &self,
        ctx: &StrategyContext<'_>,
        expr: &str,
        day: &DayExpressionState,
        stock: Option<&StockExpressionState>,
        position: Option<&PositionExpressionState>,
    ) -> Result<bool, BacktestError> {
        let value = self.eval_dynamic(ctx, expr, day, stock, position)?;
        if let Some(boolean) = value.clone().try_cast::<bool>() {
            return Ok(boolean);
        }
        if let Some(number) = value.clone().try_cast::<f64>() {
            return Ok(number != 0.0);
        }
        if let Some(number) = value.try_cast::<i64>() {
            return Ok(number != 0);
        }
        Err(BacktestError::Execution(format!(
            "platform expr did not produce a bool: {}",
            expr
        )))
    }

    fn trading_ratio(
        &self,
        ctx: &StrategyContext<'_>,
        day: &DayExpressionState,
    ) -> Result<f64, BacktestError> {
        self.eval_float(ctx, &self.config.exposure_expr, day, None, None)
            .map(|value| value.clamp(0.0, 1.0))
    }

    fn market_cap_band(
        &self,
        ctx: &StrategyContext<'_>,
        day: &DayExpressionState,
    ) -> Result<(f64, f64), BacktestError> {
        let low = self.eval_float(ctx, &self.config.market_cap_lower_expr, day, None, None)?;
        let high = self.eval_float(ctx, &self.config.market_cap_upper_expr, day, None, None)?;
        Ok((low.min(high), low.max(high)))
    }

    fn selection_limit(
        &self,
        ctx: &StrategyContext<'_>,
        day: &DayExpressionState,
    ) -> Result<usize, BacktestError> {
        let value = self.eval_float(ctx, &self.config.selection_limit_expr, day, None, None)?;
        Ok(value.round().max(1.0) as usize)
    }

    fn buy_scale(
        &self,
        ctx: &StrategyContext<'_>,
        day: &DayExpressionState,
        stock: &StockExpressionState,
    ) -> Result<f64, BacktestError> {
        if self.config.buy_scale_expr.trim().is_empty() {
            return Ok(1.0);
        }
        self.eval_float(ctx, &self.config.buy_scale_expr, day, Some(stock), None)
            .map(|value| value.clamp(0.0, 1.0))
    }

    fn eval_i32(
        &self,
        ctx: &StrategyContext<'_>,
        expr: &str,
        day: &DayExpressionState,
        stock: Option<&StockExpressionState>,
        position: Option<&PositionExpressionState>,
    ) -> Result<i32, BacktestError> {
        let value = self.eval_float(ctx, expr, day, stock, position)?;
        if !value.is_finite() {
            return Err(BacktestError::Execution(format!(
                "platform expr did not produce a finite integer: {}",
                expr
            )));
        }
        Ok(value.round().clamp(i32::MIN as f64, i32::MAX as f64) as i32)
    }

    fn eval_u64(
        &self,
        ctx: &StrategyContext<'_>,
        expr: &str,
        day: &DayExpressionState,
        stock: Option<&StockExpressionState>,
        position: Option<&PositionExpressionState>,
    ) -> Result<u64, BacktestError> {
        let value = self.eval_float(ctx, expr, day, stock, position)?;
        if !value.is_finite() {
            return Err(BacktestError::Execution(format!(
                "platform expr did not produce a finite order id: {}",
                expr
            )));
        }
        Ok(value.round().max(0.0).min(u64::MAX as f64) as u64)
    }

    fn action_stock_state(
        &self,
        ctx: &StrategyContext<'_>,
        date: NaiveDate,
        symbol: Option<&str>,
    ) -> Result<Option<StockExpressionState>, BacktestError> {
        let Some(symbol) = symbol else {
            return Ok(None);
        };
        if ctx.data.market(date, symbol).is_none()
            || ctx.data.factor(date, symbol).is_none()
            || ctx.data.candidate(date, symbol).is_none()
        {
            return Ok(None);
        }
        self.stock_state(ctx, date, symbol).map(Some)
    }

    fn action_when_matches(
        &self,
        ctx: &StrategyContext<'_>,
        day: &DayExpressionState,
        stock: Option<&StockExpressionState>,
        expr: Option<&str>,
    ) -> Result<bool, BacktestError> {
        let Some(expr) = expr.map(str::trim).filter(|value| !value.is_empty()) else {
            return Ok(true);
        };
        self.eval_bool(ctx, expr, day, stock, None)
    }

    fn explicit_action_intents(
        &self,
        ctx: &StrategyContext<'_>,
        date: NaiveDate,
        day: &DayExpressionState,
    ) -> Result<Vec<OrderIntent>, BacktestError> {
        let mut intents = Vec::new();
        for action in &self.config.explicit_actions {
            match action {
                PlatformTradeAction::Order {
                    kind,
                    symbol,
                    amount_expr,
                    limit_price_expr,
                    when_expr,
                    reason,
                } => {
                    let stock_state = self.action_stock_state(ctx, date, Some(symbol))?;
                    if !self.action_when_matches(
                        ctx,
                        day,
                        stock_state.as_ref(),
                        when_expr.as_deref(),
                    )? {
                        continue;
                    }
                    match kind {
                        PlatformExplicitOrderKind::Shares => {
                            let quantity =
                                self.eval_i32(ctx, amount_expr, day, stock_state.as_ref(), None)?;
                            if quantity == 0 {
                                continue;
                            }
                            intents.push(OrderIntent::Shares {
                                symbol: symbol.clone(),
                                quantity,
                                reason: reason.clone(),
                            });
                        }
                        PlatformExplicitOrderKind::LimitShares => {
                            let quantity =
                                self.eval_i32(ctx, amount_expr, day, stock_state.as_ref(), None)?;
                            if quantity == 0 {
                                continue;
                            }
                            let limit_price = self.eval_float(
                                ctx,
                                limit_price_expr.as_deref().unwrap_or_default(),
                                day,
                                stock_state.as_ref(),
                                None,
                            )?;
                            intents.push(OrderIntent::LimitShares {
                                symbol: symbol.clone(),
                                quantity,
                                limit_price,
                                reason: reason.clone(),
                            });
                        }
                        PlatformExplicitOrderKind::Lots => {
                            let lots =
                                self.eval_i32(ctx, amount_expr, day, stock_state.as_ref(), None)?;
                            if lots == 0 {
                                continue;
                            }
                            intents.push(OrderIntent::Lots {
                                symbol: symbol.clone(),
                                lots,
                                reason: reason.clone(),
                            });
                        }
                        PlatformExplicitOrderKind::LimitLots => {
                            let lots =
                                self.eval_i32(ctx, amount_expr, day, stock_state.as_ref(), None)?;
                            if lots == 0 {
                                continue;
                            }
                            let limit_price = self.eval_float(
                                ctx,
                                limit_price_expr.as_deref().unwrap_or_default(),
                                day,
                                stock_state.as_ref(),
                                None,
                            )?;
                            intents.push(OrderIntent::LimitLots {
                                symbol: symbol.clone(),
                                lots,
                                limit_price,
                                reason: reason.clone(),
                            });
                        }
                        PlatformExplicitOrderKind::Value => {
                            let value =
                                self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
                            if value.abs() <= f64::EPSILON {
                                continue;
                            }
                            intents.push(OrderIntent::Value {
                                symbol: symbol.clone(),
                                value,
                                reason: reason.clone(),
                            });
                        }
                        PlatformExplicitOrderKind::LimitValue => {
                            let value =
                                self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
                            if value.abs() <= f64::EPSILON {
                                continue;
                            }
                            let limit_price = self.eval_float(
                                ctx,
                                limit_price_expr.as_deref().unwrap_or_default(),
                                day,
                                stock_state.as_ref(),
                                None,
                            )?;
                            intents.push(OrderIntent::LimitValue {
                                symbol: symbol.clone(),
                                value,
                                limit_price,
                                reason: reason.clone(),
                            });
                        }
                        PlatformExplicitOrderKind::Percent => {
                            let percent =
                                self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
                            if percent.abs() <= f64::EPSILON {
                                continue;
                            }
                            intents.push(OrderIntent::Percent {
                                symbol: symbol.clone(),
                                percent,
                                reason: reason.clone(),
                            });
                        }
                        PlatformExplicitOrderKind::LimitPercent => {
                            let percent =
                                self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
                            if percent.abs() <= f64::EPSILON {
                                continue;
                            }
                            let limit_price = self.eval_float(
                                ctx,
                                limit_price_expr.as_deref().unwrap_or_default(),
                                day,
                                stock_state.as_ref(),
                                None,
                            )?;
                            intents.push(OrderIntent::LimitPercent {
                                symbol: symbol.clone(),
                                percent,
                                limit_price,
                                reason: reason.clone(),
                            });
                        }
                        PlatformExplicitOrderKind::TargetValue => {
                            let target_value =
                                self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
                            intents.push(OrderIntent::TargetValue {
                                symbol: symbol.clone(),
                                target_value,
                                reason: reason.clone(),
                            });
                        }
                        PlatformExplicitOrderKind::LimitTargetValue => {
                            let target_value =
                                self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
                            let limit_price = self.eval_float(
                                ctx,
                                limit_price_expr.as_deref().unwrap_or_default(),
                                day,
                                stock_state.as_ref(),
                                None,
                            )?;
                            intents.push(OrderIntent::LimitTargetValue {
                                symbol: symbol.clone(),
                                target_value,
                                limit_price,
                                reason: reason.clone(),
                            });
                        }
                        PlatformExplicitOrderKind::TargetPercent => {
                            let target_percent =
                                self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
                            intents.push(OrderIntent::TargetPercent {
                                symbol: symbol.clone(),
                                target_percent,
                                reason: reason.clone(),
                            });
                        }
                        PlatformExplicitOrderKind::LimitTargetPercent => {
                            let target_percent =
                                self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
                            let limit_price = self.eval_float(
                                ctx,
                                limit_price_expr.as_deref().unwrap_or_default(),
                                day,
                                stock_state.as_ref(),
                                None,
                            )?;
                            intents.push(OrderIntent::LimitTargetPercent {
                                symbol: symbol.clone(),
                                target_percent,
                                limit_price,
                                reason: reason.clone(),
                            });
                        }
                    }
                }
                PlatformTradeAction::Cancel {
                    kind,
                    symbol,
                    order_id_expr,
                    when_expr,
                    reason,
                } => {
                    let stock_state = self.action_stock_state(ctx, date, symbol.as_deref())?;
                    if !self.action_when_matches(
                        ctx,
                        day,
                        stock_state.as_ref(),
                        when_expr.as_deref(),
                    )? {
                        continue;
                    }
                    match kind {
                        PlatformExplicitCancelKind::Order => {
                            let order_id = self.eval_u64(
                                ctx,
                                order_id_expr.as_deref().unwrap_or_default(),
                                day,
                                stock_state.as_ref(),
                                None,
                            )?;
                            if order_id == 0 {
                                continue;
                            }
                            intents.push(OrderIntent::CancelOrder {
                                order_id,
                                reason: reason.clone(),
                            });
                        }
                        PlatformExplicitCancelKind::Symbol => {
                            let Some(symbol) = symbol.clone() else {
                                continue;
                            };
                            intents.push(OrderIntent::CancelSymbol {
                                symbol,
                                reason: reason.clone(),
                            });
                        }
                        PlatformExplicitCancelKind::All => {
                            intents.push(OrderIntent::CancelAll {
                                reason: reason.clone(),
                            });
                        }
                    }
                }
            }
        }
        Ok(intents)
    }

    fn stock_passes_expr(
        &self,
        ctx: &StrategyContext<'_>,
        day: &DayExpressionState,
        stock: &StockExpressionState,
    ) -> Result<bool, BacktestError> {
        if self.config.stock_filter_expr.trim().is_empty() {
            return Ok(true);
        }
        self.eval_bool(ctx, &self.config.stock_filter_expr, day, Some(stock), None)
    }

    fn field_value(&self, row: &EligibleUniverseSnapshot) -> f64 {
        match self.config.market_cap_field.as_str() {
            "free_float_cap" | "free_float_market_cap" => row.free_float_cap_bn,
            _ => row.market_cap_bn,
        }
    }

    fn stock_numeric_field_value(
        &self,
        candidate: &EligibleUniverseSnapshot,
        stock: &StockExpressionState,
        field: &str,
    ) -> Option<f64> {
        match field {
            "market_cap" => Some(stock.market_cap),
            "free_float_cap" | "free_float_market_cap" => Some(stock.free_float_cap),
            "pe_ttm" => Some(stock.pe_ttm),
            "volume" => Some(stock.volume as f64),
            "tick_volume" => Some(stock.tick_volume as f64),
            "bid1_volume" => Some(stock.bid1_volume as f64),
            "ask1_volume" => Some(stock.ask1_volume as f64),
            "turnover_ratio" => Some(stock.turnover_ratio),
            "effective_turnover_ratio" => Some(stock.effective_turnover_ratio),
            "open" => Some(stock.open),
            "close" => Some(stock.close),
            "last" | "last_price" => Some(stock.last),
            "prev_close" => Some(stock.prev_close),
            "upper_limit" => Some(stock.upper_limit),
            "lower_limit" => Some(stock.lower_limit),
            "price_tick" => Some(stock.price_tick),
            "round_lot" => Some(stock.round_lot as f64),
            "minimum_order_quantity" => Some(stock.minimum_order_quantity as f64),
            "order_step_size" => Some(stock.order_step_size as f64),
            "listed_days" => Some(stock.listed_days as f64),
            "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),
            "stock_volume_ma5" | "volume_ma5" => Some(stock.stock_volume_ma5),
            "stock_volume_ma10" | "volume_ma10" => Some(stock.stock_volume_ma10),
            "stock_volume_ma20" | "volume_ma20" => Some(stock.stock_volume_ma20),
            "stock_volume_ma60" | "volume_ma60" => Some(stock.stock_volume_ma60),
            "allow_buy" => Some(if stock.allow_buy { 1.0 } else { 0.0 }),
            "allow_sell" => Some(if stock.allow_sell { 1.0 } else { 0.0 }),
            "touched_upper_limit" | "hit_upper_limit" => {
                Some(if stock.touched_upper_limit { 1.0 } else { 0.0 })
            }
            "touched_lower_limit" | "hit_lower_limit" => {
                Some(if stock.touched_lower_limit { 1.0 } else { 0.0 })
            }
            "paused" => Some(if stock.paused { 1.0 } else { 0.0 }),
            "is_st" => Some(if stock.is_st { 1.0 } else { 0.0 }),
            "is_kcb" => Some(if stock.is_kcb { 1.0 } else { 0.0 }),
            "is_one_yuan" => Some(if stock.is_one_yuan { 1.0 } else { 0.0 }),
            "is_new_listing" => Some(if stock.is_new_listing { 1.0 } else { 0.0 }),
            "candidate_market_cap" => Some(candidate.market_cap_bn),
            "candidate_free_float_cap" => Some(candidate.free_float_cap_bn),
            other => stock.extra_factors.get(other).copied(),
        }
    }

    fn selection_field_value(
        &self,
        candidate: &EligibleUniverseSnapshot,
        stock: &StockExpressionState,
    ) -> f64 {
        self.stock_numeric_field_value(candidate, stock, self.config.market_cap_field.as_str())
            .unwrap_or_else(|| self.field_value(candidate))
    }

    fn rank_value(
        &self,
        ctx: &StrategyContext<'_>,
        day: &DayExpressionState,
        candidate: &EligibleUniverseSnapshot,
        stock: &StockExpressionState,
    ) -> Result<f64, BacktestError> {
        if !self.config.rank_expr.trim().is_empty() {
            return self.eval_float(ctx, &self.config.rank_expr, day, Some(stock), None);
        }
        Ok(self
            .stock_numeric_field_value(candidate, stock, self.config.rank_by.as_str())
            .unwrap_or_else(|| self.field_value(candidate)))
    }

    fn can_sell_position(&self, ctx: &StrategyContext<'_>, date: NaiveDate, symbol: &str) -> bool {
        let Some(position) = ctx.portfolio.position(symbol) else {
            return false;
        };
        if position.quantity == 0 || position.sellable_qty(date) == 0 {
            return false;
        }
        let Ok(market) = ctx.data.require_market(date, symbol) else {
            return false;
        };
        let Ok(candidate) = ctx.data.require_candidate(date, symbol) else {
            return false;
        };
        let lower_limit_check_price = market.price(PriceField::Last);
        !(market.paused
            || candidate.is_paused
            || !candidate.allow_sell
            || market.is_at_lower_limit_price(lower_limit_check_price))
    }

    fn buy_rejection_reason(
        &self,
        ctx: &StrategyContext<'_>,
        date: NaiveDate,
        symbol: &str,
        stock: &StockExpressionState,
    ) -> Result<Option<String>, BacktestError> {
        let market = ctx.data.require_market(date, symbol)?;
        let candidate = ctx.data.require_candidate(date, symbol)?;

        let excludes = &self.config.universe_exclude;
        if excludes.iter().any(|item| item == "paused") && (market.paused || candidate.is_paused) {
            return Ok(Some("paused".to_string()));
        }
        if excludes.iter().any(|item| item == "st") && stock.is_st {
            return Ok(Some("st_or_special_name".to_string()));
        }
        if excludes.iter().any(|item| item == "kcb") && candidate.is_kcb {
            return Ok(Some("kcb".to_string()));
        }
        if excludes.iter().any(|item| item == "new_listing") && candidate.is_new_listing {
            return Ok(Some("new_listing".to_string()));
        }
        if excludes.iter().any(|item| item == "one_yuan") && stock.is_one_yuan {
            return Ok(Some("one_yuan".to_string()));
        }
        if !candidate.allow_buy {
            return Ok(Some("buy_disabled".to_string()));
        }
        if market.is_at_upper_limit_price(market.day_open)
            || market.is_at_upper_limit_price(market.buy_price(PriceField::Last))
        {
            return Ok(Some("upper_limit".to_string()));
        }
        if market.is_at_lower_limit_price(market.day_open)
            || market.is_at_lower_limit_price(market.sell_price(PriceField::Last))
        {
            return Ok(Some("lower_limit".to_string()));
        }
        Ok(None)
    }

    fn select_symbols(
        &self,
        ctx: &StrategyContext<'_>,
        date: NaiveDate,
        day: &DayExpressionState,
        band_low: f64,
        band_high: f64,
        limit: usize,
    ) -> Result<(Vec<String>, Vec<String>), BacktestError> {
        let universe = ctx.data.eligible_universe_on(date);
        let mut diagnostics = Vec::new();
        let mut candidates = Vec::new();
        for candidate in universe.iter().cloned() {
            let stock = self.stock_state(ctx, date, &candidate.symbol)?;
            let field_value = self.selection_field_value(&candidate, &stock);
            if field_value < band_low || field_value > band_high {
                continue;
            }
            let rank_value = self.rank_value(ctx, day, &candidate, &stock)?;
            candidates.push((candidate, stock, rank_value));
        }
        candidates.sort_by(|lhs, rhs| {
            let lhs_value = lhs.2;
            let rhs_value = rhs.2;
            let ordering = if self.config.rank_desc {
                rhs_value
                    .partial_cmp(&lhs_value)
                    .unwrap_or(std::cmp::Ordering::Equal)
            } else {
                lhs_value
                    .partial_cmp(&rhs_value)
                    .unwrap_or(std::cmp::Ordering::Equal)
            };
            if ordering == std::cmp::Ordering::Equal {
                lhs.0.symbol.cmp(&rhs.0.symbol)
            } else {
                ordering
            }
        });

        let mut selected = Vec::new();
        for (candidate, stock, _) in candidates {
            if let Some(reason) = self.buy_rejection_reason(ctx, date, &candidate.symbol, &stock)? {
                if diagnostics.len() < 12 {
                    diagnostics.push(format!("{} rejected by {}", candidate.symbol, reason));
                }
                continue;
            }
            if !self.stock_passes_expr(ctx, day, &stock)? {
                if diagnostics.len() < 12 {
                    diagnostics.push(format!("{} rejected by stock_expr", candidate.symbol));
                }
                continue;
            }
            selected.push(candidate.symbol.clone());
            if selected.len() >= limit {
                break;
            }
        }

        Ok((selected, diagnostics))
    }

    fn stop_take_action(
        &self,
        ctx: &StrategyContext<'_>,
        date: NaiveDate,
        day: &DayExpressionState,
        symbol: &str,
    ) -> Result<(bool, bool), BacktestError> {
        let Some(position) = ctx.portfolio.position(symbol) else {
            return Ok((false, false));
        };
        if self.config.stop_loss_expr.trim().is_empty()
            && self.config.take_profit_expr.trim().is_empty()
        {
            return Ok((false, false));
        }
        if position.quantity == 0 || position.average_cost <= 0.0 {
            return Ok((false, false));
        }
        let stock = self.stock_state(ctx, date, symbol)?;
        let current_price = stock.last;
        let holding_return = if position.average_cost > 0.0 {
            current_price / position.average_cost - 1.0
        } else {
            0.0
        };
        let position_state = PositionExpressionState {
            avg_cost: position.average_cost,
            current_price,
            holding_return,
            quantity: position.quantity as i64,
            sellable_qty: position.sellable_qty(date) as i64,
        };
        let stop_hit = if self.config.stop_loss_expr.trim().is_empty() {
            false
        } else {
            let stop_result = self.eval_dynamic(
                ctx,
                &self.config.stop_loss_expr,
                day,
                Some(&stock),
                Some(&position_state),
            )?;
            if let Some(boolean) = stop_result.clone().try_cast::<bool>() {
                boolean
            } else if let Some(multiplier) = stop_result.clone().try_cast::<f64>() {
                current_price <= position.average_cost * multiplier
            } else if let Some(multiplier) = stop_result.try_cast::<i64>() {
                current_price <= position.average_cost * multiplier as f64
            } else {
                false
            }
        };
        let profit_hit = if self.config.take_profit_expr.trim().is_empty() {
            false
        } else {
            let take_result = self.eval_dynamic(
                ctx,
                &self.config.take_profit_expr,
                day,
                Some(&stock),
                Some(&position_state),
            )?;
            if let Some(boolean) = take_result.clone().try_cast::<bool>() {
                boolean
            } else if let Some(multiplier) = take_result.clone().try_cast::<f64>() {
                !ctx.data
                    .require_market(date, symbol)?
                    .is_at_upper_limit_price(current_price)
                    && current_price / position.average_cost > multiplier
            } else if let Some(multiplier) = take_result.try_cast::<i64>() {
                !ctx.data
                    .require_market(date, symbol)?
                    .is_at_upper_limit_price(current_price)
                    && current_price / position.average_cost > multiplier as f64
            } else {
                false
            }
        };
        Ok((stop_hit, profit_hit))
    }
}

impl Strategy for PlatformExprStrategy {
    fn name(&self) -> &str {
        self.config.strategy_name.as_str()
    }

    fn on_day(&mut self, ctx: &StrategyContext<'_>) -> Result<StrategyDecision, BacktestError> {
        let date = ctx.execution_date;
        if self.config.in_skip_window(date) {
            return Ok(StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: ctx.portfolio.positions().keys().cloned().collect(),
                order_intents: ctx
                    .portfolio
                    .positions()
                    .keys()
                    .cloned()
                    .map(|symbol| OrderIntent::TargetValue {
                        symbol,
                        target_value: 0.0,
                        reason: "seasonal_stop_window".to_string(),
                    })
                    .collect(),
                notes: vec![format!("seasonal stop window on {}", date)],
                diagnostics: vec!["platform expr skip window forced all cash".to_string()],
            });
        }

        let day = self.day_state(ctx, date)?;
        let explicit_action_intents = self.explicit_action_intents(ctx, date, &day)?;
        let mut selection_notes = Vec::new();
        let trading_ratio = if self.config.rotation_enabled {
            self.trading_ratio(ctx, &day)?
        } else {
            0.0
        };
        let (band_low, band_high) = if self.config.rotation_enabled {
            self.market_cap_band(ctx, &day)?
        } else {
            (0.0, 0.0)
        };
        let selection_limit = if self.config.rotation_enabled {
            self.selection_limit(ctx, &day)?
                .min(self.config.max_positions.max(1))
        } else {
            0
        };
        let stock_list = if self.config.rotation_enabled {
            let (stock_list, notes) =
                self.select_symbols(ctx, date, &day, band_low, band_high, selection_limit)?;
            selection_notes = notes;
            stock_list
        } else {
            Vec::new()
        };
        let periodic_rebalance = if self.config.rotation_enabled {
            if let Some(schedule) = &self.config.rebalance_schedule {
                schedule.matches(ctx.data.calendar(), date)
            } else {
                ctx.decision_index % self.config.refresh_rate == 0
            }
        } else {
            false
        };
        let mut projected = ctx.portfolio.clone();
        let mut projected_execution_state = ProjectedExecutionState::default();
        let mut order_intents = Vec::new();
        let mut exit_symbols = BTreeSet::new();

        for position in ctx.portfolio.positions().values() {
            if position.quantity == 0 || position.average_cost <= 0.0 {
                continue;
            }
            let (stop_hit, profit_hit) =
                self.stop_take_action(ctx, date, &day, &position.symbol)?;
            let can_sell = self.can_sell_position(ctx, date, &position.symbol);
            if stop_hit || profit_hit {
                let sell_reason = if stop_hit {
                    "stop_loss_exit"
                } else {
                    "take_profit_exit"
                };
                exit_symbols.insert(position.symbol.clone());
                order_intents.push(OrderIntent::TargetValue {
                    symbol: position.symbol.clone(),
                    target_value: 0.0,
                    reason: sell_reason.to_string(),
                });
                if can_sell {
                    self.project_target_zero(
                        ctx,
                        &mut projected,
                        date,
                        &position.symbol,
                        &mut projected_execution_state,
                    );
                }

                if self.config.rotation_enabled && projected.positions().len() < selection_limit {
                    let remaining_slots = selection_limit - projected.positions().len();
                    if remaining_slots > 0 {
                        let replacement_cash =
                            projected.cash() * trading_ratio / remaining_slots as f64;
                        for symbol in &stock_list {
                            if symbol == &position.symbol
                                || projected.positions().contains_key(symbol)
                            {
                                continue;
                            }
                            let stock = self.stock_state(ctx, date, symbol)?;
                            if self
                                .buy_rejection_reason(ctx, date, symbol, &stock)?
                                .is_some()
                            {
                                continue;
                            }
                            if !self.stock_passes_expr(ctx, &day, &stock)? {
                                continue;
                            }
                            let replacement_cash =
                                replacement_cash * self.buy_scale(ctx, &day, &stock)?;
                            if replacement_cash <= 0.0 {
                                continue;
                            }
                            order_intents.push(OrderIntent::Value {
                                symbol: symbol.clone(),
                                value: replacement_cash,
                                reason: format!("replacement_after_{}", sell_reason),
                            });
                            self.project_order_value(
                                ctx,
                                &mut projected,
                                date,
                                symbol,
                                replacement_cash,
                                &mut projected_execution_state,
                            );
                            break;
                        }
                    }
                }
            }
        }

        if periodic_rebalance {
            let pre_rebalance_symbols = projected
                .positions()
                .keys()
                .cloned()
                .collect::<BTreeSet<_>>();
            for symbol in pre_rebalance_symbols.iter() {
                if stock_list.iter().any(|candidate| candidate == symbol) {
                    continue;
                }
                if !self.can_sell_position(ctx, date, symbol) {
                    continue;
                }
                order_intents.push(OrderIntent::TargetValue {
                    symbol: symbol.clone(),
                    target_value: 0.0,
                    reason: "periodic_rebalance_sell".to_string(),
                });
                self.project_target_zero(
                    ctx,
                    &mut projected,
                    date,
                    symbol,
                    &mut projected_execution_state,
                );
            }

            let fixed_buy_cash = projected.cash() * trading_ratio / selection_limit as f64;
            for symbol in &stock_list {
                if projected.positions().len() >= selection_limit {
                    break;
                }
                if pre_rebalance_symbols.contains(symbol)
                    || projected.positions().contains_key(symbol)
                {
                    continue;
                }
                let stock = self.stock_state(ctx, date, symbol)?;
                if self
                    .buy_rejection_reason(ctx, date, symbol, &stock)?
                    .is_some()
                {
                    continue;
                }
                if !self.stock_passes_expr(ctx, &day, &stock)? {
                    continue;
                }
                let buy_cash = fixed_buy_cash * self.buy_scale(ctx, &day, &stock)?;
                if buy_cash <= 0.0 {
                    continue;
                }
                order_intents.push(OrderIntent::Value {
                    symbol: symbol.clone(),
                    value: buy_cash,
                    reason: "periodic_rebalance_buy".to_string(),
                });
                self.project_order_value(
                    ctx,
                    &mut projected,
                    date,
                    symbol,
                    buy_cash,
                    &mut projected_execution_state,
                );
            }
        }

        if !explicit_action_intents.is_empty() {
            order_intents.extend(explicit_action_intents);
        }

        let mut diagnostics = vec![
            if self.config.rotation_enabled {
                format!(
                    "platform_expr signal={} last={:.2} ma_short={:.2} ma_long={:.2} band={:.2}-{:.2} tr={:.2}",
                    self.config.signal_symbol,
                    day.signal_close,
                    day.benchmark_ma_short,
                    day.benchmark_ma_long,
                    band_low,
                    band_high,
                    trading_ratio
                )
            } else {
                format!(
                    "platform_expr signal={} last={:.2} explicit_actions={} rotation=false",
                    self.config.signal_symbol,
                    day.signal_close,
                    self.config.explicit_actions.len()
                )
            },
            format!(
                "selected={} periodic_rebalance={} exits={} projected_positions={} intents={} limit={}",
                stock_list.len(),
                periodic_rebalance,
                exit_symbols.len(),
                projected.positions().len(),
                order_intents.len(),
                selection_limit
            ),
            "platform strategy script executed through expression runtime + bid1/ask1 snapshot execution".to_string(),
        ];
        diagnostics.extend(selection_notes);

        let notes = vec![
            format!("stock_list={}", stock_list.len()),
            format!("projected_positions={}", projected.positions().len()),
        ];

        Ok(StrategyDecision {
            rebalance: false,
            target_weights: BTreeMap::new(),
            exit_symbols,
            order_intents,
            notes,
            diagnostics,
        })
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;

    use chrono::NaiveDate;

    use super::{
        PlatformExplicitCancelKind, PlatformExplicitOrderKind, PlatformExprStrategy,
        PlatformExprStrategyConfig, PlatformRebalanceSchedule, PlatformScheduleFrequency,
        PlatformTradeAction,
    };
    use crate::{
        BenchmarkSnapshot, CandidateEligibility, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
        Instrument, PortfolioState, Strategy, StrategyContext, TradingCalendar,
    };

    fn d(year: i32, month: u32, day: u32) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("valid date")
    }

    fn sample_calendar() -> TradingCalendar {
        TradingCalendar::new(vec![
            d(2025, 1, 30),
            d(2025, 1, 31),
            d(2025, 2, 3),
            d(2025, 2, 4),
            d(2025, 2, 7),
        ])
    }

    #[test]
    fn platform_rebalance_schedule_matches_weekly_weekday() {
        let calendar = sample_calendar();
        let schedule = PlatformRebalanceSchedule {
            frequency: PlatformScheduleFrequency::Weekly {
                weekday: Some(5),
                tradingday: None,
            },
        };
        assert!(schedule.matches(&calendar, d(2025, 1, 31)));
        assert!(!schedule.matches(&calendar, d(2025, 2, 3)));
    }

    #[test]
    fn platform_rebalance_schedule_matches_monthly_tradingday() {
        let calendar = sample_calendar();
        let schedule = PlatformRebalanceSchedule {
            frequency: PlatformScheduleFrequency::Monthly { tradingday: 1 },
        };
        assert!(schedule.matches(&calendar, d(2025, 2, 3)));
        assert!(!schedule.matches(&calendar, d(2025, 2, 4)));
    }

    #[test]
    fn platform_strategy_emits_explicit_actions_when_rotation_is_disabled() {
        let date = d(2025, 2, 3);
        let data = DataSet::from_components(
            vec![Instrument {
                symbol: "000001.SZ".to_string(),
                name: "Ping An Bank".to_string(),
                board: "SZSE".to_string(),
                round_lot: 100,
                listed_at: Some(d(2010, 1, 1)),
                delisted_at: None,
                status: "active".to_string(),
            }],
            vec![DailyMarketSnapshot {
                date,
                symbol: "000001.SZ".to_string(),
                timestamp: Some("10:18:00".to_string()),
                day_open: 10.0,
                open: 10.0,
                high: 10.2,
                low: 9.9,
                close: 10.1,
                last_price: 10.05,
                bid1: 10.04,
                ask1: 10.05,
                prev_close: 9.95,
                volume: 1_000_000,
                tick_volume: 5_000,
                bid1_volume: 1_000,
                ask1_volume: 1_000,
                trading_phase: Some("continuous".to_string()),
                paused: false,
                upper_limit: 10.94,
                lower_limit: 8.96,
                price_tick: 0.01,
            }],
            vec![DailyFactorSnapshot {
                date,
                symbol: "000001.SZ".to_string(),
                market_cap_bn: 12.0,
                free_float_cap_bn: 10.0,
                pe_ttm: 8.0,
                turnover_ratio: Some(22.0),
                effective_turnover_ratio: Some(18.0),
                extra_factors: BTreeMap::new(),
            }],
            vec![CandidateEligibility {
                date,
                symbol: "000001.SZ".to_string(),
                is_st: false,
                is_new_listing: false,
                is_paused: false,
                allow_buy: true,
                allow_sell: true,
                is_kcb: false,
                is_one_yuan: false,
            }],
            vec![BenchmarkSnapshot {
                date,
                benchmark: "000852.SH".to_string(),
                open: 1000.0,
                close: 1002.0,
                prev_close: 998.0,
                volume: 1_000_000,
            }],
        )
        .expect("dataset");
        let portfolio = PortfolioState::new(1_000_000.0);
        let ctx = StrategyContext {
            execution_date: date,
            decision_date: date,
            decision_index: 0,
            data: &data,
            portfolio: &portfolio,
        };
        let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
        cfg.signal_symbol = "000001.SZ".to_string();
        cfg.rotation_enabled = false;
        cfg.benchmark_short_ma_days = 1;
        cfg.benchmark_long_ma_days = 1;
        cfg.explicit_actions = vec![
            PlatformTradeAction::Order {
                kind: PlatformExplicitOrderKind::Value,
                symbol: "000001.SZ".to_string(),
                amount_expr: "cash * 0.1".to_string(),
                limit_price_expr: None,
                when_expr: Some("allow_buy && !touched_upper_limit".to_string()),
                reason: "platform_explicit_value".to_string(),
            },
            PlatformTradeAction::Cancel {
                kind: PlatformExplicitCancelKind::Symbol,
                symbol: Some("000001.SZ".to_string()),
                order_id_expr: None,
                when_expr: Some("allow_buy".to_string()),
                reason: "platform_cancel_symbol".to_string(),
            },
        ];
        let mut strategy = PlatformExprStrategy::new(cfg);

        let decision = strategy.on_day(&ctx).expect("platform decision");

        assert_eq!(decision.order_intents.len(), 2);
        match &decision.order_intents[0] {
            crate::strategy::OrderIntent::Value {
                symbol,
                value,
                reason,
            } => {
                assert_eq!(symbol, "000001.SZ");
                assert!((*value - 100_000.0).abs() < 1e-6);
                assert_eq!(reason, "platform_explicit_value");
            }
            other => panic!("unexpected first explicit order intent: {other:?}"),
        }
        match &decision.order_intents[1] {
            crate::strategy::OrderIntent::CancelSymbol { symbol, reason } => {
                assert_eq!(symbol, "000001.SZ");
                assert_eq!(reason, "platform_cancel_symbol");
            }
            other => panic!("unexpected explicit cancel intent: {other:?}"),
        }
        assert!(
            decision
                .diagnostics
                .iter()
                .any(|item| item.contains("rotation=false"))
        );
    }
}
