From f056aa346873d8311b615ae2565371e987475304 Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 23 Apr 2026 21:58:38 -0700 Subject: [PATCH] Add futures exchange validators --- crates/fidc-core/src/engine.rs | 134 +++++++++++- crates/fidc-core/src/lib.rs | 2 +- crates/fidc-core/src/strategy_ai.rs | 9 + crates/fidc-core/tests/engine_hooks.rs | 285 ++++++++++++++++++++++++- docs/rqalpha-gap-roadmap.md | 8 +- 5 files changed, 426 insertions(+), 12 deletions(-) diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index f1137aa..7e5a5ce 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -48,6 +48,25 @@ pub struct BacktestConfig { pub execution_price_field: PriceField, } +#[derive(Debug, Clone, Copy)] +pub struct FuturesValidationConfig { + pub enforce_active_instrument: bool, + pub enforce_trading_phase: bool, + pub enforce_limit_price_tick: bool, + pub enforce_price_limits: bool, +} + +impl Default for FuturesValidationConfig { + fn default() -> Self { + Self { + enforce_active_instrument: true, + enforce_trading_phase: true, + enforce_limit_price_tick: true, + enforce_price_limits: true, + } + } +} + #[derive(Debug, Clone, Serialize)] pub struct DailyEquityPoint { #[serde(with = "date_format")] @@ -294,6 +313,7 @@ pub struct BacktestEngine { futures_expirations: BTreeMap>, futures_settlement_price_mode: String, futures_cost_model: FuturesTransactionCostModel, + futures_validation_config: FuturesValidationConfig, } impl BacktestEngine { @@ -318,6 +338,7 @@ impl BacktestEngine { futures_expirations: BTreeMap::new(), futures_settlement_price_mode: "close".to_string(), futures_cost_model: FuturesTransactionCostModel::default(), + futures_validation_config: FuturesValidationConfig::default(), } } @@ -377,6 +398,11 @@ impl BacktestEngine { self } + pub fn with_futures_validation_config(mut self, config: FuturesValidationConfig) -> Self { + self.futures_validation_config = config; + self + } + pub fn process_event_bus_mut(&mut self) -> &mut ProcessEventBus { &mut self.process_event_bus } @@ -832,12 +858,12 @@ where ); }; - if let Some(reason) = self.validate_futures_submission(&intent) { + let original_requested = intent.quantity; + let mut intent = self.resolve_futures_trading_parameters(date, intent); + if let Some(reason) = self.validate_futures_submission(date, &intent) { return self.reject_futures_order(date, order_id, intent, reason); } - let original_requested = intent.quantity; - let mut intent = self.resolve_futures_trading_parameters(date, intent); let fill = self.resolve_futures_fill(date, &intent); let Some((execution_price, fill_quantity)) = fill else { if intent.allow_pending || intent.limit_price.is_some() { @@ -1020,14 +1046,76 @@ where report } - fn validate_futures_submission(&self, intent: &FuturesOrderIntent) -> Option { + fn validate_futures_submission( + &self, + date: NaiveDate, + intent: &FuturesOrderIntent, + ) -> Option { if intent.quantity == 0 { return Some("zero futures quantity".to_string()); } + if self.futures_validation_config.enforce_active_instrument { + if let Some(instrument) = self.data.instrument(&intent.symbol) { + if !instrument.is_active_on(date) { + return Some(format!( + "inactive futures instrument symbol={} date={date}", + intent.symbol + )); + } + } + } + if self.futures_validation_config.enforce_trading_phase { + if let Some(snapshot) = self.data.market(date, &intent.symbol) { + if snapshot.paused { + return Some(format!( + "paused futures instrument symbol={}", + intent.symbol + )); + } + if !futures_trading_phase_allows_orders(snapshot.trading_phase.as_deref()) { + return Some(format!( + "futures trading phase does not allow orders symbol={} phase={}", + intent.symbol, + snapshot.trading_phase.as_deref().unwrap_or("") + )); + } + } + } if let Some(limit_price) = intent.limit_price { if !limit_price.is_finite() || limit_price <= 0.0 { return Some("invalid futures limit price".to_string()); } + if self.futures_validation_config.enforce_limit_price_tick { + let tick = self.futures_price_tick(date, &intent.symbol); + if !price_is_tick_aligned(limit_price, tick) { + return Some(format!( + "futures limit price not aligned to tick symbol={} price={limit_price:.6} tick={tick:.6}", + intent.symbol + )); + } + } + if self.futures_validation_config.enforce_price_limits { + if let Some(snapshot) = self.data.market(date, &intent.symbol) { + if snapshot.upper_limit.is_finite() + && snapshot.upper_limit > 0.0 + && limit_price > snapshot.upper_limit + 1e-9 + { + return Some(format!( + "futures limit price above upper limit symbol={} price={limit_price:.6} upper={:.6}", + intent.symbol, snapshot.upper_limit + )); + } + if snapshot.lower_limit.is_finite() + && snapshot.lower_limit > 0.0 + && limit_price < snapshot.lower_limit - 1e-9 + { + return Some(format!( + "futures limit price below lower limit symbol={} price={limit_price:.6} lower={:.6}", + intent.symbol, snapshot.lower_limit + )); + } + } + } for order in &self.futures_open_orders { if order.intent.symbol != intent.symbol || order.intent.side() == intent.side() { continue; @@ -1048,6 +1136,20 @@ where None } + fn futures_price_tick(&self, date: NaiveDate, symbol: &str) -> f64 { + self.data + .futures_trading_parameter(date, symbol) + .map(|params| params.price_tick) + .filter(|tick| tick.is_finite() && *tick > 0.0) + .or_else(|| { + self.data + .market(date, symbol) + .map(|snapshot| snapshot.effective_price_tick()) + }) + .unwrap_or(1.0) + .max(1e-9) + } + fn resolve_futures_trading_parameters( &self, date: NaiveDate, @@ -3226,6 +3328,30 @@ fn analyzer_ratio_change(start: f64, end: f64) -> f64 { } } +fn price_is_tick_aligned(price: f64, tick: f64) -> bool { + if !price.is_finite() || !tick.is_finite() || tick <= 0.0 { + return false; + } + let ratio = price / tick; + (ratio - ratio.round()).abs() <= 1e-6 +} + +fn futures_trading_phase_allows_orders(phase: Option<&str>) -> bool { + let Some(phase) = phase.map(str::trim).filter(|value| !value.is_empty()) else { + return true; + }; + matches!( + phase.to_ascii_lowercase().as_str(), + "continuous" + | "trading" + | "trade" + | "open_auction" + | "auction" + | "call_auction" + | "opening_auction" + ) +} + fn futures_limit_satisfied(side: OrderSide, price: f64, limit_price: Option) -> bool { let Some(limit_price) = limit_price else { return price.is_finite() && price > 0.0; diff --git a/crates/fidc-core/src/lib.rs b/crates/fidc-core/src/lib.rs index c9821af..7fccf90 100644 --- a/crates/fidc-core/src/lib.rs +++ b/crates/fidc-core/src/lib.rs @@ -28,7 +28,7 @@ pub use data::{ pub use engine::{ AnalyzerMonthlyReturnRow, AnalyzerPositionRow, AnalyzerReport, AnalyzerRiskSummary, AnalyzerTradeRow, BacktestConfig, BacktestDayProgress, BacktestEngine, BacktestError, - BacktestResult, DailyEquityPoint, + BacktestResult, DailyEquityPoint, FuturesValidationConfig, }; pub use event_bus::{BacktestProcessMod, BacktestProcessModLoader, ProcessEventBus}; pub use events::{ diff --git a/crates/fidc-core/src/strategy_ai.rs b/crates/fidc-core/src/strategy_ai.rs index f7d1f21..341837d 100644 --- a/crates/fidc-core/src/strategy_ai.rs +++ b/crates/fidc-core/src/strategy_ai.rs @@ -122,6 +122,10 @@ pub fn built_in_strategy_manual() -> StrategyAiManual { title: "execution.matching_type / execution.slippage".to_string(), detail: "设置撮合模式和滑点。支持 execution.matching_type(\"next_tick_last\" | \"next_tick_best_own\" | \"next_tick_best_counterparty\" | \"counterparty_offer\" | \"vwap\" | \"current_bar_close\" | \"next_bar_open\" | \"open_auction\")。其中 next_tick_last 使用 tick 的 last_price;next_tick_best_own / next_tick_best_counterparty 会按 L1 买一卖一近似 rqalpha 的 tick 最优价语义;counterparty_offer 在存在 order_book_depth 多档盘口数据时会按真实档位逐档扫单并计算加权成交价,不存在 depth 时回退 L1 对手方报价;vwap 会在盘中执行价链路上聚合多笔成交为单条 VWAP 成交;open_auction 使用当日集合竞价开盘价 day_open 进行撮合,且不额外施加滑点,并按竞价成交量而不是盘口一档流动性限制成交;滑点支持 execution.slippage(\"none\") / execution.slippage(\"price_ratio\", 0.001) / execution.slippage(\"tick_size\", 1) / execution.slippage(\"limit_price\"),其中 limit_price 会在限价单成交时按挂单价模拟 rqalpha 的最坏成交价。".to_string(), }, + ManualSection { + title: "期货提交校验".to_string(), + detail: "期货订单进入撮合前会先执行账户与交易规则校验:合约必须在上市/退市日期范围内,日行情不能停牌,trading_phase 需处于 continuous/trading/open_auction/auction/call_auction/opening_auction 等可交易阶段,限价必须为正且按 futures_trading_parameters.price_tick 或日行情 price_tick 对齐,并且不能越过 upper_limit/lower_limit;随后继续检查反向挂单自成交风险、保证金和可平数量。服务层可通过 FuturesValidationConfig 分别关闭 active instrument、trading phase、limit price tick、price limit 校验,用于兼容特殊数据源,但默认全部开启。".to_string(), + }, ManualSection { title: "trading.rotation / order.* / cancel.* / update_universe / subscribe".to_string(), detail: "支持显式下单、撤单、AlgoOrder、动态 universe 和账户资金动作。可以用 trading.rotation(false) 关闭默认轮动链路,再用 trading.stage(\"open_auction\" | \"on_day\") 指定执行阶段;需要模拟 rqalpha 的 tick 订阅保护时,可写 trading.subscription_guard(true),未订阅 symbol 的显式订单会被拦截,TargetPortfolioSmart + AlgoOrder 会过滤未订阅标的。用 trading.schedule.daily().at([\"10:18\"]) / trading.schedule.weekly(weekday=5).at([\"10:18\"]) / trading.schedule.weekly(tradingday=-1).at([\"10:18\"]) / trading.schedule.monthly(tradingday=1).at([\"10:18\"]) 指定触发频率和分钟级 time_rule,然后写 order.shares(\"600000.SH\", 1000)、order.target_shares(\"600000.SH\", 2000)、order.value(\"600000.SH\", cash * 0.25)、order.target_percent(\"600000.SH\", 0.05)、order.limit_value(\"600000.SH\", cash * 0.25, open * 0.99)、order.vwap_value(\"600000.SH\", cash * 0.25, \"09:31\", \"09:40\")、order.twap_percent(\"600000.SH\", 0.05, \"10:00\", \"10:30\")、order.target_portfolio_smart(weights={\"600000.SH\": 0.3, \"000001.SZ\": 0.2}, order_prices=VWAPOrder(930, 940), valuation_prices={\"600000.SH\": prev_close})、order.target_portfolio_smart(weights={\"600000.SH\": 0.3, \"000001.SZ\": 0.2}, order_prices={\"600000.SH\": open * 0.99}, valuation_prices={\"600000.SH\": prev_close})、cancel.order(12345)、cancel.symbol(\"600000.SH\")、cancel.all()、update_universe([\"600000.SH\", \"000001.SZ\"])、subscribe([\"000001.SZ\"])、unsubscribe([\"000001.SZ\"])、account.deposit_withdraw(100000, receiving_days=0)、account.finance_repay(50000)、account.set_management_fee_rate(0.001)。其中 order.target_shares(...) 对应 rqalpha 的 order_to,order.target_portfolio_smart(...) 对应 rqalpha 的 order_target_portfolio_smart 批量目标权重语义;account.deposit_withdraw(...) 和 account.finance_repay(...) 对应 RQAlpha 账户出入金与融资/还款语义;order_prices 既可以是逐标的限价映射,也可以是 VWAPOrder/TWAPOrder 这类全局 AlgoOrder;order.vwap_* / order.twap_* 对应 rqalpha 的 AlgoOrder 时间窗订单风格,而 update_universe/subscribe/unsubscribe 对应 rqalpha 的动态 universe 与订阅接口。symbol 使用标准证券代码;数量、金额、仓位、时间窗、限价、order_id 和 symbol 列表都支持表达式;这些语句也支持放进 when/unless 条件块。".to_string(), @@ -243,6 +247,11 @@ pub fn built_in_strategy_manual() -> StrategyAiManual { detail: "可选多档盘口数据源,字段为 date,symbol,timestamp,level,bid_price,bid_volume,ask_price,ask_volume。存在该数据时,期货 counterparty_offer / next_tick_best_counterparty 可按真实多档盘口逐档扫单;不存在时不会伪造 depth。".to_string(), fields: vec![], }, + ManualFactorSource { + table: "futures_trading_parameters.csv / futures_trading_parameters/".to_string(), + detail: "期货交易参数数据源,字段包括 symbol,effective_date,contract_multiplier,long_margin_rate,short_margin_rate,commission_type,open_commission_ratio,close_commission_ratio,close_today_commission_ratio,price_tick。回测会按交易日自动选择不晚于当前日期的最新参数,用于保证金、手续费和限价 tick 校验。".to_string(), + fields: vec![], + }, ], examples: vec![ ManualExample { diff --git a/crates/fidc-core/tests/engine_hooks.rs b/crates/fidc-core/tests/engine_hooks.rs index 7b0c241..0ae142c 100644 --- a/crates/fidc-core/tests/engine_hooks.rs +++ b/crates/fidc-core/tests/engine_hooks.rs @@ -8,10 +8,10 @@ use fidc_core::{ BenchmarkSnapshot, BrokerSimulator, CandidateEligibility, ChinaAShareCostModel, ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, FuturesAccountState, FuturesCommissionType, FuturesContractSpec, FuturesDirection, FuturesOrderIntent, - FuturesTradingParameter, Instrument, IntradayExecutionQuote, IntradayOrderBookDepthLevel, - MatchingType, OpenOrderView, OrderIntent, OrderSide, OrderStatus, PortfolioState, PriceField, - ProcessEvent, ProcessEventBus, ProcessEventKind, ScheduleRule, ScheduleStage, ScheduleTimeRule, - Strategy, StrategyContext, StrategyDecision, + FuturesTradingParameter, FuturesValidationConfig, Instrument, IntradayExecutionQuote, + IntradayOrderBookDepthLevel, MatchingType, OpenOrderView, OrderIntent, OrderSide, OrderStatus, + PortfolioState, PriceField, ProcessEvent, ProcessEventBus, ProcessEventKind, ScheduleRule, + ScheduleStage, ScheduleTimeRule, Strategy, StrategyContext, StrategyDecision, }; fn d(year: i32, month: u32, day: u32) -> NaiveDate { @@ -408,6 +408,99 @@ impl Strategy for FuturesLimitOrderStrategy { } } +struct FuturesInvalidTickLimitStrategy; + +impl Strategy for FuturesInvalidTickLimitStrategy { + fn name(&self) -> &str { + "futures-invalid-tick-limit" + } + + fn on_day( + &mut self, + ctx: &StrategyContext<'_>, + ) -> Result { + if ctx.execution_date != d(2025, 1, 2) { + return Ok(StrategyDecision::default()); + } + Ok(StrategyDecision { + order_intents: vec![OrderIntent::Futures { + intent: FuturesOrderIntent::limit_open( + "IF2501", + FuturesDirection::Long, + FuturesContractSpec::new(1.0, 0.0, 0.0), + 1, + 3988.13, + 0.0, + "bad tick limit", + ), + }], + ..StrategyDecision::default() + }) + } +} + +struct FuturesClosedPhaseOrderStrategy; + +impl Strategy for FuturesClosedPhaseOrderStrategy { + fn name(&self) -> &str { + "futures-closed-phase-order" + } + + fn on_day( + &mut self, + ctx: &StrategyContext<'_>, + ) -> Result { + if ctx.execution_date != d(2025, 1, 2) { + return Ok(StrategyDecision::default()); + } + Ok(StrategyDecision { + order_intents: vec![OrderIntent::Futures { + intent: FuturesOrderIntent::open( + "IF2501", + FuturesDirection::Long, + FuturesContractSpec::new(1.0, 0.0, 0.0), + 1, + 4000.0, + 0.0, + "closed phase order", + ), + }], + ..StrategyDecision::default() + }) + } +} + +struct FuturesAboveUpperLimitStrategy; + +impl Strategy for FuturesAboveUpperLimitStrategy { + fn name(&self) -> &str { + "futures-above-upper-limit" + } + + fn on_day( + &mut self, + ctx: &StrategyContext<'_>, + ) -> Result { + if ctx.execution_date != d(2025, 1, 2) { + return Ok(StrategyDecision::default()); + } + Ok(StrategyDecision { + order_intents: vec![OrderIntent::Futures { + intent: FuturesOrderIntent::limit_open( + "IF2501", + FuturesDirection::Long, + FuturesContractSpec::new(1.0, 0.0, 0.0), + 1, + 5000.0, + 0.0, + "outside upper limit", + ), + }], + ..StrategyDecision::default() + }) + } +} + struct FuturesDepthLimitOrderStrategy; impl Strategy for FuturesDepthLimitOrderStrategy { @@ -1431,6 +1524,190 @@ fn engine_matches_pending_futures_limit_order_with_data_driven_costs() { assert!((position.contract_multiplier - 300.0).abs() < 1e-6); } +#[test] +fn engine_rejects_futures_limit_orders_not_aligned_to_tick() { + let broker = BrokerSimulator::new_with_execution_price( + ChinaAShareCostModel::default(), + ChinaEquityRuleHooks::default(), + PriceField::Open, + ); + let mut engine = BacktestEngine::new( + two_day_futures_data(), + FuturesInvalidTickLimitStrategy, + broker, + BacktestConfig { + initial_cash: 100_000.0, + benchmark_code: "000300.SH".to_string(), + start_date: Some(d(2025, 1, 2)), + end_date: Some(d(2025, 1, 2)), + decision_lag_trading_days: 0, + execution_price_field: PriceField::Open, + }, + ) + .with_futures_initial_cash(1_000_000.0); + + let result = engine.run().expect("backtest succeeds"); + + assert!(result.order_events.iter().any(|event| { + event.symbol == "IF2501" + && event.status == OrderStatus::Rejected + && event.reason.contains("not aligned to tick") + })); +} + +#[test] +fn engine_allows_disabling_futures_limit_tick_validation() { + let broker = BrokerSimulator::new_with_execution_price( + ChinaAShareCostModel::default(), + ChinaEquityRuleHooks::default(), + PriceField::Open, + ); + let mut engine = BacktestEngine::new( + two_day_futures_data(), + FuturesInvalidTickLimitStrategy, + broker, + BacktestConfig { + initial_cash: 100_000.0, + benchmark_code: "000300.SH".to_string(), + start_date: Some(d(2025, 1, 2)), + end_date: Some(d(2025, 1, 3)), + decision_lag_trading_days: 0, + execution_price_field: PriceField::Open, + }, + ) + .with_futures_initial_cash(1_000_000.0) + .with_futures_validation_config(FuturesValidationConfig { + enforce_limit_price_tick: false, + ..FuturesValidationConfig::default() + }); + + let result = engine.run().expect("backtest succeeds"); + + assert!( + result + .order_events + .iter() + .any(|event| event.symbol == "IF2501" && event.status == OrderStatus::Pending) + ); + assert!(result.order_events.iter().any(|event| { + event.symbol == "IF2501" + && event.status == OrderStatus::Filled + && event.filled_quantity == 1 + })); + let fill = result + .fills + .iter() + .find(|fill| fill.symbol == "IF2501") + .expect("futures fill"); + assert!((fill.price - 3988.0).abs() < 1e-6); +} + +#[test] +fn engine_rejects_futures_limit_orders_outside_price_limits() { + let broker = BrokerSimulator::new_with_execution_price( + ChinaAShareCostModel::default(), + ChinaEquityRuleHooks::default(), + PriceField::Open, + ); + let mut engine = BacktestEngine::new( + two_day_futures_data(), + FuturesAboveUpperLimitStrategy, + broker, + BacktestConfig { + initial_cash: 100_000.0, + benchmark_code: "000300.SH".to_string(), + start_date: Some(d(2025, 1, 2)), + end_date: Some(d(2025, 1, 2)), + decision_lag_trading_days: 0, + execution_price_field: PriceField::Open, + }, + ) + .with_futures_initial_cash(1_000_000.0); + + let result = engine.run().expect("backtest succeeds"); + + assert!(result.order_events.iter().any(|event| { + event.symbol == "IF2501" + && event.status == OrderStatus::Rejected + && event.reason.contains("above upper limit") + })); +} + +#[test] +fn engine_rejects_futures_orders_when_trading_phase_is_closed() { + let date = d(2025, 1, 2); + let mut future_market = market_row(date, "IF2501", 4000.0, 4000.0); + future_market.trading_phase = Some("closed".to_string()); + let data = DataSet::from_components_with_actions_quotes_and_futures( + vec![ + Instrument { + symbol: "000001.SZ".to_string(), + name: "Anchor".to_string(), + board: "SZ".to_string(), + round_lot: 100, + listed_at: Some(d(2020, 1, 1)), + delisted_at: None, + status: "active".to_string(), + }, + Instrument { + symbol: "IF2501".to_string(), + name: "IF".to_string(), + board: "FUTURE".to_string(), + round_lot: 1, + listed_at: Some(d(2024, 1, 1)), + delisted_at: None, + status: "active".to_string(), + }, + ], + vec![market_row(date, "000001.SZ", 10.0, 10.0), future_market], + vec![factor_row(date, "000001.SZ", BTreeMap::new())], + vec![candidate_row(date, "000001.SZ")], + vec![benchmark_row(date)], + Vec::new(), + Vec::new(), + vec![FuturesTradingParameter { + symbol: "IF2501".to_string(), + effective_date: Some(date), + contract_multiplier: 300.0, + long_margin_rate: 0.12, + short_margin_rate: 0.14, + commission_type: FuturesCommissionType::ByVolume, + open_commission_ratio: 2.5, + close_commission_ratio: 2.0, + close_today_commission_ratio: 3.0, + price_tick: 0.2, + }], + ) + .expect("futures dataset"); + let broker = BrokerSimulator::new_with_execution_price( + ChinaAShareCostModel::default(), + ChinaEquityRuleHooks::default(), + PriceField::Open, + ); + let mut engine = BacktestEngine::new( + data, + FuturesClosedPhaseOrderStrategy, + broker, + BacktestConfig { + initial_cash: 100_000.0, + benchmark_code: "000300.SH".to_string(), + start_date: Some(date), + end_date: Some(date), + decision_lag_trading_days: 0, + execution_price_field: PriceField::Open, + }, + ) + .with_futures_initial_cash(1_000_000.0); + + let result = engine.run().expect("backtest succeeds"); + + assert!(result.order_events.iter().any(|event| { + event.symbol == "IF2501" + && event.status == OrderStatus::Rejected + && event.reason.contains("trading phase") + })); +} + #[test] fn engine_sweeps_futures_order_book_depth_when_available() { let date = d(2025, 1, 2); diff --git a/docs/rqalpha-gap-roadmap.md b/docs/rqalpha-gap-roadmap.md index 22c7641..28efedf 100644 --- a/docs/rqalpha-gap-roadmap.md +++ b/docs/rqalpha-gap-roadmap.md @@ -50,7 +50,7 @@ Parity gaps found by this pass and current closure state: | P1 | Futures trading parameter data source | RQAlpha loads contract multiplier, margin ratios, commission type, open/close/close-today commission ratios, settlement/prev-settlement, tick size, listed/de-listed dates, and dominant contracts from data proxy. | Closed for engine-side trading-parameter ingestion/resolution via `futures_trading_parameters.csv` or component data. | Add more exchange metadata columns only when source data exposes them. | | P1 | Futures transaction cost decider | RQAlpha supports by-money/by-volume futures commission with separate open, close, and close-today rates and a commission multiplier. | Closed. `FuturesTransactionCostModel` calculates by-money/by-volume open/close/close-today costs from trading parameters. | None. | | P1 | Futures settlement price mode | RQAlpha can settle futures by `settlement` or `close`, including previous-settlement fields. | Closed. Engine supports configurable settlement price mode and resolves settlement/prev-settlement from factor fields with close/prev_close fallback. | Add dedicated settlement columns if the storage layer later separates them from factors. | -| P1 | Frontend risk validators for futures | RQAlpha applies cash/margin, position closable, price-limit, trading-status, and self-trade validators before order submission. | Closed for zero quantity, invalid limit price, self-trade crossing risk, paused/no executable price, price-limit, margin, and close-position rejection diagnostics. | Add exchange-specific validators only as needed. | +| P1 | Frontend risk validators for futures | RQAlpha applies cash/margin, position closable, price-limit, trading-status, and self-trade validators before order submission. | Closed for zero quantity, invalid limit price, active-contract, trading-phase, tick-aligned limit price, price-limit, self-trade crossing risk, paused/no executable price, margin, and close-position rejection diagnostics. These submission validators are controlled by `FuturesValidationConfig` so service-level callers can relax individual checks for compatibility tests or vendor-specific rules. | Add more exchange metadata columns only when source data exposes them. | | P2 | RQData helper APIs | RQAlpha exposes `get_dividend`, `get_split`, `get_yield_curve`, `get_factor`, `get_margin_stocks`, `get_securities_margin`, `get_dominant_future`, and dominant futures price APIs. | Closed. These APIs are available through `DataSet` and `StrategyContext`; platform expressions also expose focused helpers such as `dividend_cash`, `factor_value`, `yield_curve`, `is_margin_stock`, `dominant_future`, and `dominant_future_price`. | Add more DSL aliases only when users need specific names. | | P2 | Analyzer/report parity | RQAlpha analyser can export richer trades, positions, benchmark, monthly returns, risk, and summary artifacts. | Closed for normalized trades, positions, monthly returns, risk summary, equity curve, benchmark series, metrics, and JSON report bundle via `BacktestResult::analyzer_report(_json)`. | UI/service download endpoints can serialize this report directly. | | P3 | Mod/config/plugin architecture | RQAlpha has pluggable mods, event bus extension points, and many config toggles. | Closed for a lightweight engine-native model: `BacktestProcessMod`, `BacktestProcessModLoader`, enabled-name installation, and event-bus lifecycle hooks. It intentionally avoids RQAlpha's Python global mod loader. | Add concrete production mods/toggles as requirements appear. | @@ -149,6 +149,8 @@ Parity gaps found by this pass and current closure state: - [x] futures trading-parameter data source and automatic cost/margin resolver - [x] futures settlement/prev-settlement data integration and settlement mode - [x] futures-aware submission validators and self-trade checks +- [x] configurable futures active-contract, trading-phase, price-tick, and + price-limit submission validators - [x] optional true multi-level futures order-book depth data and sweep matching ### Phase 10: Advanced data API parity @@ -204,5 +206,5 @@ Parity gaps found by this pass and current closure state: Active implementation target: P0-P2 parity items are implemented in the engine core, and P3 now has a lightweight event-driven extension loader. Remaining future work should be driven by concrete production strategy or UI requirements, -especially exchange-specific validators and optional vendor-specific depth -fields. +especially optional vendor-specific depth fields, additional exchange metadata +columns, or exact UI-required RQAlpha intermediate event names.