Add futures exchange validators

This commit is contained in:
boris
2026-04-23 21:58:38 -07:00
parent 895aee1388
commit f056aa3468
5 changed files with 426 additions and 12 deletions

View File

@@ -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<S, C, R> {
futures_expirations: BTreeMap<NaiveDate, BTreeMap<String, f64>>,
futures_settlement_price_mode: String,
futures_cost_model: FuturesTransactionCostModel,
futures_validation_config: FuturesValidationConfig,
}
impl<S, C, R> BacktestEngine<S, C, R> {
@@ -318,6 +338,7 @@ impl<S, C, R> BacktestEngine<S, C, R> {
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<S, C, R> BacktestEngine<S, C, R> {
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<String> {
fn validate_futures_submission(
&self,
date: NaiveDate,
intent: &FuturesOrderIntent,
) -> Option<String> {
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<f64>) -> bool {
let Some(limit_price) = limit_price else {
return price.is_finite() && price > 0.0;

View File

@@ -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::{

View File

@@ -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_pricenext_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_toorder.target_portfolio_smart(...) 对应 rqalpha 的 order_target_portfolio_smart 批量目标权重语义account.deposit_withdraw(...) 和 account.finance_repay(...) 对应 RQAlpha 账户出入金与融资/还款语义order_prices 既可以是逐标的限价映射,也可以是 VWAPOrder/TWAPOrder 这类全局 AlgoOrderorder.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 {

View File

@@ -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<StrategyDecision, fidc_core::BacktestError> {
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<StrategyDecision, fidc_core::BacktestError> {
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<StrategyDecision, fidc_core::BacktestError> {
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);

View File

@@ -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.