Close RQAlpha P0-P2 parity gaps

This commit is contained in:
boris
2026-04-23 21:07:59 -07:00
parent 6be87c9982
commit beb9c7a7ae
8 changed files with 1830 additions and 86 deletions

View File

@@ -197,6 +197,14 @@ impl<C, R> BrokerSimulator<C, R> {
self self
} }
pub fn matching_type(&self) -> MatchingType {
self.matching_type
}
pub fn execution_price_field(&self) -> PriceField {
self.execution_price_field
}
pub fn open_order_views(&self) -> Vec<OpenOrderView> { pub fn open_order_views(&self) -> Vec<OpenOrderView> {
self.open_orders self.open_orders
.borrow() .borrow()

View File

@@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use crate::calendar::TradingCalendar; use crate::calendar::TradingCalendar;
use crate::futures::{FuturesCommissionType, FuturesTradingParameter};
use crate::instrument::Instrument; use crate::instrument::Instrument;
mod date_format { mod date_format {
@@ -345,6 +346,51 @@ pub struct PriceBar {
pub ask1_volume: u64, pub ask1_volume: u64,
} }
#[derive(Debug, Clone, Serialize)]
pub struct DividendRecord {
#[serde(with = "date_format")]
pub ex_dividend_date: NaiveDate,
#[serde(with = "date_format")]
pub payable_date: NaiveDate,
pub symbol: String,
pub dividend_cash_before_tax: f64,
pub round_lot: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct SplitRecord {
#[serde(with = "date_format")]
pub ex_dividend_date: NaiveDate,
pub symbol: String,
pub split_ratio: f64,
}
#[derive(Debug, Clone, Serialize)]
pub struct FactorValue {
#[serde(with = "date_format")]
pub date: NaiveDate,
pub symbol: String,
pub field: String,
pub value: f64,
}
#[derive(Debug, Clone, Serialize)]
pub struct SecuritiesMarginRecord {
#[serde(with = "date_format")]
pub date: NaiveDate,
pub symbol: String,
pub field: String,
pub value: f64,
}
#[derive(Debug, Clone, Serialize)]
pub struct YieldCurvePoint {
#[serde(with = "date_format")]
pub date: NaiveDate,
pub tenor: String,
pub value: f64,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct EligibleUniverseSnapshot { pub struct EligibleUniverseSnapshot {
pub symbol: String, pub symbol: String,
@@ -620,6 +666,7 @@ pub struct DataSet {
benchmark_series_cache: BenchmarkPriceSeries, benchmark_series_cache: BenchmarkPriceSeries,
eligible_universe_by_date: BTreeMap<NaiveDate, Vec<EligibleUniverseSnapshot>>, eligible_universe_by_date: BTreeMap<NaiveDate, Vec<EligibleUniverseSnapshot>>,
benchmark_code: String, benchmark_code: String,
futures_params_by_symbol: HashMap<String, Vec<FuturesTradingParameter>>,
} }
impl DataSet { impl DataSet {
@@ -641,7 +688,13 @@ impl DataSet {
} else { } else {
Vec::new() Vec::new()
}; };
Self::from_components_with_actions_and_quotes( let futures_params_path = path.join("futures_trading_parameters.csv");
let futures_params = if futures_params_path.exists() {
read_futures_trading_parameters(&futures_params_path)?
} else {
Vec::new()
};
Self::from_components_with_actions_quotes_and_futures(
instruments, instruments,
market, market,
factors, factors,
@@ -649,6 +702,7 @@ impl DataSet {
benchmarks, benchmarks,
corporate_actions, corporate_actions,
execution_quotes, execution_quotes,
futures_params,
) )
} }
@@ -670,7 +724,13 @@ impl DataSet {
} else { } else {
Vec::new() Vec::new()
}; };
Self::from_components_with_actions_and_quotes( let futures_params_dir = path.join("futures_trading_parameters");
let futures_params = if futures_params_dir.exists() {
read_partitioned_dir(&futures_params_dir, read_futures_trading_parameters)?
} else {
Vec::new()
};
Self::from_components_with_actions_quotes_and_futures(
instruments, instruments,
market, market,
factors, factors,
@@ -678,6 +738,7 @@ impl DataSet {
benchmarks, benchmarks,
corporate_actions, corporate_actions,
execution_quotes, execution_quotes,
futures_params,
) )
} }
@@ -726,6 +787,28 @@ impl DataSet {
benchmarks: Vec<BenchmarkSnapshot>, benchmarks: Vec<BenchmarkSnapshot>,
corporate_actions: Vec<CorporateAction>, corporate_actions: Vec<CorporateAction>,
execution_quotes: Vec<IntradayExecutionQuote>, execution_quotes: Vec<IntradayExecutionQuote>,
) -> Result<Self, DataSetError> {
Self::from_components_with_actions_quotes_and_futures(
instruments,
market,
factors,
candidates,
benchmarks,
corporate_actions,
execution_quotes,
Vec::new(),
)
}
pub fn from_components_with_actions_quotes_and_futures(
instruments: Vec<Instrument>,
market: Vec<DailyMarketSnapshot>,
factors: Vec<DailyFactorSnapshot>,
candidates: Vec<CandidateEligibility>,
benchmarks: Vec<BenchmarkSnapshot>,
corporate_actions: Vec<CorporateAction>,
execution_quotes: Vec<IntradayExecutionQuote>,
futures_params: Vec<FuturesTradingParameter>,
) -> Result<Self, DataSetError> { ) -> Result<Self, DataSetError> {
let benchmark_code = collect_benchmark_code(&benchmarks)?; let benchmark_code = collect_benchmark_code(&benchmarks)?;
let calendar = TradingCalendar::new(benchmarks.iter().map(|item| item.date).collect()); let calendar = TradingCalendar::new(benchmarks.iter().map(|item| item.date).collect());
@@ -764,6 +847,7 @@ impl DataSet {
BenchmarkPriceSeries::new(&benchmark_by_date.values().cloned().collect::<Vec<_>>()); BenchmarkPriceSeries::new(&benchmark_by_date.values().cloned().collect::<Vec<_>>());
let eligible_universe_by_date = let eligible_universe_by_date =
build_eligible_universe(&factor_by_date, &candidate_index, &market_index); build_eligible_universe(&factor_by_date, &candidate_index, &market_index);
let futures_params_by_symbol = build_futures_params_index(futures_params);
Ok(Self { Ok(Self {
instruments, instruments,
@@ -781,6 +865,7 @@ impl DataSet {
benchmark_series_cache, benchmark_series_cache,
eligible_universe_by_date, eligible_universe_by_date,
benchmark_code, benchmark_code,
futures_params_by_symbol,
}) })
} }
@@ -870,6 +955,38 @@ impl DataSet {
self.benchmark_by_date.values().cloned().collect() self.benchmark_by_date.values().cloned().collect()
} }
pub fn futures_trading_parameter(
&self,
date: NaiveDate,
symbol: &str,
) -> Option<&FuturesTradingParameter> {
self.futures_params_by_symbol.get(symbol).and_then(|rows| {
rows.iter()
.rev()
.find(|row| row.effective_date.is_none_or(|effective| effective <= date))
})
}
pub fn futures_settlement_price(
&self,
date: NaiveDate,
symbol: &str,
mode: &str,
) -> Option<f64> {
let snapshot = self.market(date, symbol)?;
match normalize_field(mode).as_str() {
"settlement" | "settle" => self
.factor_numeric_value(date, symbol, "settlement")
.or_else(|| self.factor_numeric_value(date, symbol, "settle"))
.or(Some(snapshot.close)),
"prev_settlement" | "pre_settlement" => self
.factor_numeric_value(date, symbol, "prev_settlement")
.or_else(|| self.factor_numeric_value(date, symbol, "pre_settlement"))
.or(Some(snapshot.prev_close)),
_ => Some(snapshot.close),
}
}
pub fn history_bars( pub fn history_bars(
&self, &self,
date: NaiveDate, date: NaiveDate,
@@ -994,6 +1111,218 @@ impl DataSet {
}) })
} }
pub fn get_dividend(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
) -> Vec<DividendRecord> {
let mut rows = self
.corporate_actions_by_date
.range(start..=end)
.flat_map(|(_, actions)| actions.iter())
.filter(|action| action.symbol == symbol && action.share_cash.abs() > f64::EPSILON)
.map(|action| DividendRecord {
ex_dividend_date: action.date,
payable_date: action.payable_date.unwrap_or(action.date),
symbol: action.symbol.clone(),
dividend_cash_before_tax: action.share_cash,
round_lot: self
.instrument(symbol)
.map(Instrument::effective_round_lot)
.unwrap_or(100),
})
.collect::<Vec<_>>();
rows.sort_by_key(|row| row.ex_dividend_date);
rows
}
pub fn get_split(&self, symbol: &str, start: NaiveDate, end: NaiveDate) -> Vec<SplitRecord> {
let mut rows = self
.corporate_actions_by_date
.range(start..=end)
.flat_map(|(_, actions)| actions.iter())
.filter(|action| action.symbol == symbol && (action.split_ratio() - 1.0).abs() > 1e-12)
.map(|action| SplitRecord {
ex_dividend_date: action.date,
symbol: action.symbol.clone(),
split_ratio: action.split_ratio(),
})
.collect::<Vec<_>>();
rows.sort_by_key(|row| row.ex_dividend_date);
rows
}
pub fn get_factor(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
field: &str,
) -> Vec<FactorValue> {
if start > end {
return Vec::new();
}
let field = normalize_field(field);
let mut rows = self
.factor_by_date
.range(start..=end)
.flat_map(|(_, snapshots)| snapshots.iter())
.filter(|snapshot| snapshot.symbol == symbol)
.filter_map(|snapshot| {
factor_numeric_value(snapshot, &field).map(|value| FactorValue {
date: snapshot.date,
symbol: snapshot.symbol.clone(),
field: field.clone(),
value,
})
})
.collect::<Vec<_>>();
rows.sort_by_key(|row| row.date);
rows
}
pub fn get_yield_curve(
&self,
start: NaiveDate,
end: NaiveDate,
tenor: Option<&str>,
) -> Vec<YieldCurvePoint> {
if start > end {
return Vec::new();
}
let tenor_filter = tenor.map(normalize_field);
let mut rows = Vec::new();
for (date, snapshots) in self.factor_by_date.range(start..=end) {
for snapshot in snapshots {
for (field, value) in &snapshot.extra_factors {
let normalized = normalize_field(field);
let Some(raw_tenor) = normalized
.strip_prefix("yield_curve_")
.or_else(|| normalized.strip_prefix("yc_"))
else {
continue;
};
if tenor_filter
.as_ref()
.is_some_and(|expected| expected != raw_tenor)
{
continue;
}
rows.push(YieldCurvePoint {
date: *date,
tenor: raw_tenor.to_string(),
value: *value,
});
}
}
}
rows.sort_by(|left, right| {
left.date
.cmp(&right.date)
.then(left.tenor.cmp(&right.tenor))
});
rows
}
pub fn get_margin_stocks(&self, date: NaiveDate, margin_type: &str) -> Vec<String> {
let field = match normalize_field(margin_type).as_str() {
"stock" => "margin_stock",
"cash" => "margin_cash",
_ => "margin_all",
};
let mut symbols = self
.factor_by_date
.get(&date)
.map(|rows| {
rows.iter()
.filter(|row| {
row.extra_factors
.get(field)
.or_else(|| row.extra_factors.get("margin_all"))
.is_some_and(|value| *value > 0.0)
})
.map(|row| row.symbol.clone())
.collect::<Vec<_>>()
})
.unwrap_or_default();
if symbols.is_empty() {
symbols = self
.active_instruments(
date,
&self
.instruments
.keys()
.map(String::as_str)
.collect::<Vec<_>>(),
)
.into_iter()
.filter(|instrument| !instrument.board.eq_ignore_ascii_case("FUTURE"))
.map(|instrument| instrument.symbol.clone())
.collect();
}
symbols.sort();
symbols.dedup();
symbols
}
pub fn get_securities_margin(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
field: &str,
) -> Vec<SecuritiesMarginRecord> {
self.get_factor(symbol, start, end, field)
.into_iter()
.map(|row| SecuritiesMarginRecord {
date: row.date,
symbol: row.symbol,
field: row.field,
value: row.value,
})
.collect()
}
pub fn get_dominant_future(&self, underlying_symbol: &str, date: NaiveDate) -> Option<String> {
let underlying = normalize_field(underlying_symbol);
let mut candidates = self
.futures_params_by_symbol
.keys()
.filter(|symbol| normalize_field(symbol).starts_with(&underlying))
.filter(|symbol| {
self.futures_trading_parameter(date, symbol.as_str())
.is_some()
})
.cloned()
.collect::<Vec<_>>();
if candidates.is_empty() {
candidates = self
.instruments
.values()
.filter(|instrument| instrument.board.eq_ignore_ascii_case("FUTURE"))
.filter(|instrument| normalize_field(&instrument.symbol).starts_with(&underlying))
.filter(|instrument| instrument.is_active_on(date))
.map(|instrument| instrument.symbol.clone())
.collect();
}
candidates.sort();
candidates.into_iter().next()
}
pub fn get_dominant_future_price(
&self,
underlying_symbol: &str,
start: NaiveDate,
end: NaiveDate,
frequency: &str,
) -> Vec<PriceBar> {
let Some(symbol) = self.get_dominant_future(underlying_symbol, end) else {
return Vec::new();
};
self.get_price(&symbol, start, end, frequency)
}
pub fn get_price( pub fn get_price(
&self, &self,
symbol: &str, symbol: &str,
@@ -1649,6 +1978,41 @@ fn read_execution_quotes(path: &Path) -> Result<Vec<IntradayExecutionQuote>, Dat
Ok(quotes) Ok(quotes)
} }
fn read_futures_trading_parameters(
path: &Path,
) -> Result<Vec<FuturesTradingParameter>, DataSetError> {
let rows = read_rows(path)?;
let mut params = Vec::new();
for row in rows {
let first = row.get(0)?.trim();
let (effective_date, symbol_index) = if NaiveDate::parse_from_str(first, "%Y-%m-%d").is_ok()
{
(row.parse_optional_date(0)?, 1)
} else {
(None, 0)
};
params.push(FuturesTradingParameter {
effective_date,
symbol: row.get(symbol_index)?.to_string(),
contract_multiplier: row.parse_optional_f64(symbol_index + 1).unwrap_or(1.0),
long_margin_rate: row.parse_optional_f64(symbol_index + 2).unwrap_or(0.0),
short_margin_rate: row.parse_optional_f64(symbol_index + 3).unwrap_or(0.0),
commission_type: row
.fields
.get(symbol_index + 4)
.map(|value| FuturesCommissionType::parse(value))
.unwrap_or(FuturesCommissionType::ByMoney),
open_commission_ratio: row.parse_optional_f64(symbol_index + 5).unwrap_or(0.0),
close_commission_ratio: row.parse_optional_f64(symbol_index + 6).unwrap_or(0.0),
close_today_commission_ratio: row
.parse_optional_f64(symbol_index + 7)
.unwrap_or_else(|| row.parse_optional_f64(symbol_index + 6).unwrap_or(0.0)),
price_tick: row.parse_optional_f64(symbol_index + 8).unwrap_or(1.0),
});
}
Ok(params)
}
struct CsvRow { struct CsvRow {
path: String, path: String,
line: usize, line: usize,
@@ -1934,6 +2298,19 @@ fn build_market_series(
.collect() .collect()
} }
fn build_futures_params_index(
rows: Vec<FuturesTradingParameter>,
) -> HashMap<String, Vec<FuturesTradingParameter>> {
let mut grouped = HashMap::<String, Vec<FuturesTradingParameter>>::new();
for row in rows {
grouped.entry(row.symbol.clone()).or_default().push(row);
}
for rows in grouped.values_mut() {
rows.sort_by_key(|row| row.effective_date);
}
grouped
}
fn build_execution_quote_index( fn build_execution_quote_index(
execution_quotes: Vec<IntradayExecutionQuote>, execution_quotes: Vec<IntradayExecutionQuote>,
) -> HashMap<(NaiveDate, String), Vec<IntradayExecutionQuote>> { ) -> HashMap<(NaiveDate, String), Vec<IntradayExecutionQuote>> {

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use chrono::NaiveDate; use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use crate::events::{ use crate::events::{
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent, AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
@@ -69,6 +70,108 @@ pub struct FuturesContractSpec {
pub short_margin_rate: f64, pub short_margin_rate: f64,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FuturesCommissionType {
ByMoney,
ByVolume,
}
impl FuturesCommissionType {
pub fn parse(value: &str) -> Self {
match value.trim().to_ascii_lowercase().as_str() {
"by_volume" | "volume" | "byvolume" => Self::ByVolume,
_ => Self::ByMoney,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::ByMoney => "by_money",
Self::ByVolume => "by_volume",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FuturesTradingParameter {
pub symbol: String,
pub effective_date: Option<NaiveDate>,
pub contract_multiplier: f64,
pub long_margin_rate: f64,
pub short_margin_rate: f64,
pub commission_type: FuturesCommissionType,
pub open_commission_ratio: f64,
pub close_commission_ratio: f64,
pub close_today_commission_ratio: f64,
pub price_tick: f64,
}
impl FuturesTradingParameter {
pub fn spec(&self) -> FuturesContractSpec {
FuturesContractSpec::new(
self.contract_multiplier,
self.long_margin_rate,
self.short_margin_rate,
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct FuturesTransactionCostModel {
pub commission_multiplier: f64,
}
impl Default for FuturesTransactionCostModel {
fn default() -> Self {
Self {
commission_multiplier: 1.0,
}
}
}
impl FuturesTransactionCostModel {
pub fn calculate(
&self,
params: &FuturesTradingParameter,
effect: FuturesPositionEffect,
price: f64,
quantity: u32,
close_today_quantity: u32,
) -> f64 {
if quantity == 0 || !price.is_finite() || price <= 0.0 {
return 0.0;
}
let quantity = quantity as f64;
let close_today_quantity = close_today_quantity.min(quantity as u32) as f64;
let close_yesterday_quantity = (quantity - close_today_quantity).max(0.0);
let raw = match params.commission_type {
FuturesCommissionType::ByMoney => match effect {
FuturesPositionEffect::Open => {
price * quantity * params.contract_multiplier * params.open_commission_ratio
}
FuturesPositionEffect::Close
| FuturesPositionEffect::CloseToday
| FuturesPositionEffect::CloseYesterday => {
price
* params.contract_multiplier
* (close_yesterday_quantity * params.close_commission_ratio
+ close_today_quantity * params.close_today_commission_ratio)
}
},
FuturesCommissionType::ByVolume => match effect {
FuturesPositionEffect::Open => quantity * params.open_commission_ratio,
FuturesPositionEffect::Close
| FuturesPositionEffect::CloseToday
| FuturesPositionEffect::CloseYesterday => {
close_yesterday_quantity * params.close_commission_ratio
+ close_today_quantity * params.close_today_commission_ratio
}
},
};
raw.max(0.0) * self.commission_multiplier.max(0.0)
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FuturesOrderIntent { pub struct FuturesOrderIntent {
pub symbol: String, pub symbol: String,
@@ -78,6 +181,8 @@ pub struct FuturesOrderIntent {
pub quantity: u32, pub quantity: u32,
pub price: f64, pub price: f64,
pub transaction_cost: f64, pub transaction_cost: f64,
pub limit_price: Option<f64>,
pub allow_pending: bool,
pub reason: String, pub reason: String,
} }
@@ -99,6 +204,8 @@ impl FuturesOrderIntent {
quantity, quantity,
price, price,
transaction_cost, transaction_cost,
limit_price: None,
allow_pending: false,
reason: reason.into(), reason: reason.into(),
} }
} }
@@ -121,10 +228,80 @@ impl FuturesOrderIntent {
quantity, quantity,
price, price,
transaction_cost, transaction_cost,
limit_price: None,
allow_pending: false,
reason: reason.into(), reason: reason.into(),
} }
} }
pub fn limit_open(
symbol: impl Into<String>,
direction: FuturesDirection,
spec: FuturesContractSpec,
quantity: u32,
limit_price: f64,
transaction_cost: f64,
reason: impl Into<String>,
) -> Self {
Self::open(
symbol,
direction,
spec,
quantity,
limit_price,
transaction_cost,
reason,
)
.with_limit_price(limit_price)
}
pub fn limit_close(
symbol: impl Into<String>,
direction: FuturesDirection,
effect: FuturesPositionEffect,
spec: FuturesContractSpec,
quantity: u32,
limit_price: f64,
transaction_cost: f64,
reason: impl Into<String>,
) -> Self {
Self::close(
symbol,
direction,
effect,
spec,
quantity,
limit_price,
transaction_cost,
reason,
)
.with_limit_price(limit_price)
}
pub fn with_limit_price(mut self, limit_price: f64) -> Self {
self.limit_price = limit_price
.is_finite()
.then_some(limit_price)
.filter(|v| *v > 0.0);
self.allow_pending = self.limit_price.is_some();
self
}
pub fn with_allow_pending(mut self, allow_pending: bool) -> Self {
self.allow_pending = allow_pending;
self
}
pub fn with_price(mut self, price: f64) -> Self {
self.price = price;
self
}
pub fn with_transaction_cost(mut self, transaction_cost: f64) -> Self {
self.transaction_cost = transaction_cost;
self
}
pub fn side(&self) -> OrderSide { pub fn side(&self) -> OrderSide {
if self.effect == FuturesPositionEffect::Open { if self.effect == FuturesPositionEffect::Open {
self.direction.open_side() self.direction.open_side()
@@ -132,6 +309,29 @@ impl FuturesOrderIntent {
self.direction.close_side() self.direction.close_side()
} }
} }
pub fn with_trading_parameter(
mut self,
params: &FuturesTradingParameter,
cost_model: FuturesTransactionCostModel,
) -> Self {
self.spec = params.spec();
if self.transaction_cost <= 0.0 {
let close_today_quantity = if self.effect == FuturesPositionEffect::CloseToday {
self.quantity
} else {
0
};
self.transaction_cost = cost_model.calculate(
params,
self.effect,
self.price,
self.quantity,
close_today_quantity,
);
}
self
}
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]

View File

@@ -21,12 +21,13 @@ pub use calendar::TradingCalendar;
pub use cost::{ChinaAShareCostModel, CostModel, TradingCost}; pub use cost::{ChinaAShareCostModel, CostModel, TradingCost};
pub use data::{ pub use data::{
BenchmarkSnapshot, CandidateEligibility, CorporateAction, DailyFactorSnapshot, BenchmarkSnapshot, CandidateEligibility, CorporateAction, DailyFactorSnapshot,
DailyMarketSnapshot, DailySnapshotBundle, DataSet, DataSetError, EligibleUniverseSnapshot, DailyMarketSnapshot, DailySnapshotBundle, DataSet, DataSetError, DividendRecord,
IntradayExecutionQuote, PriceBar, PriceField, EligibleUniverseSnapshot, FactorValue, IntradayExecutionQuote, PriceBar, PriceField,
SecuritiesMarginRecord, SplitRecord, YieldCurvePoint,
}; };
pub use engine::{ pub use engine::{
BacktestConfig, BacktestDayProgress, BacktestEngine, BacktestError, BacktestResult, AnalyzerPositionRow, AnalyzerReport, AnalyzerTradeRow, BacktestConfig, BacktestDayProgress,
DailyEquityPoint, BacktestEngine, BacktestError, BacktestResult, DailyEquityPoint,
}; };
pub use event_bus::ProcessEventBus; pub use event_bus::ProcessEventBus;
pub use events::{ pub use events::{
@@ -34,8 +35,9 @@ pub use events::{
ProcessEventKind, ProcessEventKind,
}; };
pub use futures::{ pub use futures::{
FuturesAccountState, FuturesContractSpec, FuturesDirection, FuturesExecutionReport, FuturesAccountState, FuturesCommissionType, FuturesContractSpec, FuturesDirection,
FuturesOrderIntent, FuturesPosition, FuturesPositionEffect, FuturesExecutionReport, FuturesOrderIntent, FuturesPosition, FuturesPositionEffect,
FuturesTradingParameter, FuturesTransactionCostModel,
}; };
pub use instrument::Instrument; pub use instrument::Instrument;
pub use metrics::{BacktestMetrics, compute_backtest_metrics}; pub use metrics::{BacktestMetrics, compute_backtest_metrics};

View File

@@ -7,7 +7,10 @@ use std::sync::OnceLock;
use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime}; use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime};
use crate::cost::ChinaAShareCostModel; use crate::cost::ChinaAShareCostModel;
use crate::data::{DailyMarketSnapshot, DataSet, IntradayExecutionQuote, PriceBar, PriceField}; use crate::data::{
DailyMarketSnapshot, DataSet, DividendRecord, FactorValue, IntradayExecutionQuote, PriceBar,
PriceField, SecuritiesMarginRecord, SplitRecord, YieldCurvePoint,
};
use crate::engine::BacktestError; use crate::engine::BacktestError;
use crate::events::{FillEvent, OrderEvent, OrderSide, OrderStatus, ProcessEvent}; use crate::events::{FillEvent, OrderEvent, OrderSide, OrderStatus, ProcessEvent};
use crate::futures::{FuturesAccountState, FuturesOrderIntent}; use crate::futures::{FuturesAccountState, FuturesOrderIntent};
@@ -601,6 +604,72 @@ impl StrategyContext<'_> {
self.data.get_price(symbol, start, end, frequency) self.data.get_price(symbol, start, end, frequency)
} }
pub fn get_dividend(&self, symbol: &str, start: NaiveDate) -> Vec<DividendRecord> {
let end = self
.data
.previous_trading_date(self.execution_date, 1)
.unwrap_or(self.execution_date);
self.data.get_dividend(symbol, start, end)
}
pub fn get_split(&self, symbol: &str, start: NaiveDate) -> Vec<SplitRecord> {
let end = self
.data
.previous_trading_date(self.execution_date, 1)
.unwrap_or(self.execution_date);
self.data.get_split(symbol, start, end)
}
pub fn get_factor(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
field: &str,
) -> Vec<FactorValue> {
self.data.get_factor(symbol, start, end, field)
}
pub fn get_yield_curve(
&self,
start: NaiveDate,
end: NaiveDate,
tenor: Option<&str>,
) -> Vec<YieldCurvePoint> {
self.data.get_yield_curve(start, end, tenor)
}
pub fn get_margin_stocks(&self, margin_type: &str) -> Vec<String> {
self.data
.get_margin_stocks(self.execution_date, margin_type)
}
pub fn get_securities_margin(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
field: &str,
) -> Vec<SecuritiesMarginRecord> {
self.data.get_securities_margin(symbol, start, end, field)
}
pub fn get_dominant_future(&self, underlying_symbol: &str) -> Option<String> {
self.data
.get_dominant_future(underlying_symbol, self.execution_date)
}
pub fn get_dominant_future_price(
&self,
underlying_symbol: &str,
start: NaiveDate,
end: NaiveDate,
frequency: &str,
) -> Vec<PriceBar> {
self.data
.get_dominant_future_price(underlying_symbol, start, end, frequency)
}
pub fn has_subscriptions(&self) -> bool { pub fn has_subscriptions(&self) -> bool {
!self.subscriptions.is_empty() !self.subscriptions.is_empty()
} }

View File

@@ -6,10 +6,10 @@ use chrono::{NaiveDate, NaiveDateTime};
use fidc_core::{ use fidc_core::{
BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility, BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility,
ChinaAShareCostModel, ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, ChinaAShareCostModel, ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
FuturesAccountState, FuturesContractSpec, FuturesDirection, FuturesOrderIntent, Instrument, FuturesAccountState, FuturesCommissionType, FuturesContractSpec, FuturesDirection,
IntradayExecutionQuote, OpenOrderView, OrderIntent, OrderSide, OrderStatus, PortfolioState, FuturesOrderIntent, FuturesTradingParameter, Instrument, IntradayExecutionQuote, OpenOrderView,
PriceField, ProcessEventKind, ScheduleRule, ScheduleStage, ScheduleTimeRule, Strategy, OrderIntent, OrderSide, OrderStatus, PortfolioState, PriceField, ProcessEventKind,
StrategyContext, StrategyDecision, ScheduleRule, ScheduleStage, ScheduleTimeRule, Strategy, StrategyContext, StrategyDecision,
}; };
fn d(year: i32, month: u32, day: u32) -> NaiveDate { fn d(year: i32, month: u32, day: u32) -> NaiveDate {
@@ -97,6 +97,147 @@ fn single_day_anchor_data(date: NaiveDate) -> DataSet {
.expect("dataset") .expect("dataset")
} }
fn market_row(date: NaiveDate, symbol: &str, open: f64, close: f64) -> DailyMarketSnapshot {
DailyMarketSnapshot {
date,
symbol: symbol.to_string(),
timestamp: Some(format!("{date} 10:18:00")),
day_open: open,
open,
high: open.max(close),
low: open.min(close),
close,
last_price: close,
bid1: close,
ask1: close,
prev_close: open,
volume: 1_000_000,
tick_volume: 1_000_000,
bid1_volume: 1_000_000,
ask1_volume: 1_000_000,
trading_phase: Some("continuous".to_string()),
paused: false,
upper_limit: open * 1.1,
lower_limit: open * 0.9,
price_tick: 0.2,
}
}
fn factor_row(
date: NaiveDate,
symbol: &str,
extra_factors: BTreeMap<String, f64>,
) -> DailyFactorSnapshot {
DailyFactorSnapshot {
date,
symbol: symbol.to_string(),
market_cap_bn: 100.0,
free_float_cap_bn: 80.0,
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
extra_factors,
}
}
fn candidate_row(date: NaiveDate, symbol: &str) -> CandidateEligibility {
CandidateEligibility {
date,
symbol: symbol.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,
}
}
fn benchmark_row(date: NaiveDate) -> BenchmarkSnapshot {
BenchmarkSnapshot {
date,
benchmark: "000300.SH".to_string(),
open: 100.0,
close: 100.0,
prev_close: 99.0,
volume: 1_000_000,
}
}
fn two_day_futures_data() -> DataSet {
let d1 = d(2025, 1, 2);
let d2 = d(2025, 1, 3);
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(d1, "000001.SZ", 10.0, 10.0),
market_row(d2, "000001.SZ", 10.0, 10.0),
market_row(d1, "IF2501", 4000.0, 4000.0),
market_row(d2, "IF2501", 3988.0, 3990.0),
],
vec![
factor_row(
d1,
"000001.SZ",
BTreeMap::from([
("custom_alpha".to_string(), 7.0),
("margin_all".to_string(), 1.0),
("yield_curve_1y".to_string(), 0.02),
]),
),
factor_row(
d2,
"000001.SZ",
BTreeMap::from([
("custom_alpha".to_string(), 8.0),
("margin_all".to_string(), 1.0),
("yield_curve_1y".to_string(), 0.021),
]),
),
],
vec![
candidate_row(d1, "000001.SZ"),
candidate_row(d2, "000001.SZ"),
],
vec![benchmark_row(d1), benchmark_row(d2)],
Vec::new(),
Vec::new(),
vec![FuturesTradingParameter {
symbol: "IF2501".to_string(),
effective_date: Some(d1),
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")
}
struct HookProbeStrategy { struct HookProbeStrategy {
log: Rc<RefCell<Vec<String>>>, log: Rc<RefCell<Vec<String>>>,
} }
@@ -234,6 +375,73 @@ impl Strategy for FuturesOrderStrategy {
} }
} }
struct FuturesLimitOrderStrategy;
impl Strategy for FuturesLimitOrderStrategy {
fn name(&self) -> &str {
"futures-limit-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::limit_open(
"IF2501",
FuturesDirection::Long,
FuturesContractSpec::new(1.0, 0.0, 0.0),
2,
3990.0,
0.0,
"wait for pullback",
),
}],
..StrategyDecision::default()
})
}
}
struct AdvancedDataApiProbeStrategy {
observed: Rc<RefCell<Vec<String>>>,
}
impl Strategy for AdvancedDataApiProbeStrategy {
fn name(&self) -> &str {
"data-api-probe"
}
fn on_day(
&mut self,
ctx: &StrategyContext<'_>,
) -> Result<StrategyDecision, fidc_core::BacktestError> {
let factors = ctx.get_factor(
"000001.SZ",
ctx.execution_date,
ctx.execution_date,
"custom_alpha",
);
let margin_stocks = ctx.get_margin_stocks("all");
let yield_curve = ctx.get_yield_curve(ctx.execution_date, ctx.execution_date, Some("1y"));
let dominant = ctx.get_dominant_future("IF").unwrap_or_default();
let dominant_prices =
ctx.get_dominant_future_price("IF", ctx.execution_date, ctx.execution_date, "1d");
self.observed.borrow_mut().push(format!(
"factor={:.0};margin={};yield={:.3};dominant={};prices={}",
factors.first().map(|row| row.value).unwrap_or_default(),
margin_stocks.join(","),
yield_curve.first().map(|row| row.value).unwrap_or_default(),
dominant,
dominant_prices.len()
));
Ok(StrategyDecision::default())
}
}
struct ScheduledProbeStrategy { struct ScheduledProbeStrategy {
log: Rc<RefCell<Vec<String>>>, log: Rc<RefCell<Vec<String>>>,
process_log: Rc<RefCell<Vec<String>>>, process_log: Rc<RefCell<Vec<String>>>,
@@ -1098,6 +1306,126 @@ fn engine_settles_configured_futures_expiration_at_settlement() {
})); }));
} }
#[test]
fn engine_aggregates_futures_account_into_nav_and_metrics() {
let date = d(2025, 1, 2);
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks::default(),
PriceField::Open,
);
let mut engine = BacktestEngine::new(
single_day_anchor_data(date),
FuturesOrderStrategy,
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(500_000.0);
let result = engine.run().expect("backtest succeeds");
assert_eq!(result.metrics.initial_cash, 600_000.0);
assert!((result.equity_curve[0].total_equity - 599_988.0).abs() < 1e-6);
assert!((result.metrics.total_assets - 599_988.0).abs() < 1e-6);
assert_eq!(result.analyzer_report().trades.len(), result.fills.len());
assert!(
result
.analyzer_report_json()
.expect("report json")
.contains("\"trades\"")
);
}
#[test]
fn engine_matches_pending_futures_limit_order_with_data_driven_costs() {
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks::default(),
PriceField::Open,
);
let mut engine = BacktestEngine::new(
two_day_futures_data(),
FuturesLimitOrderStrategy,
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);
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 == 2
}));
let fill = result
.fills
.iter()
.find(|fill| fill.symbol == "IF2501")
.expect("futures fill");
assert!((fill.price - 3988.0).abs() < 1e-6);
assert!((fill.commission - 5.0).abs() < 1e-6);
let futures_account = engine.futures_account().expect("future account");
let position = futures_account
.position("IF2501", FuturesDirection::Long)
.expect("long futures position");
assert_eq!(position.quantity, 2);
assert!((position.contract_multiplier - 300.0).abs() < 1e-6);
}
#[test]
fn strategy_context_exposes_advanced_rqdata_helpers() {
let observed = Rc::new(RefCell::new(Vec::new()));
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks::default(),
PriceField::Open,
);
let mut engine = BacktestEngine::new(
two_day_futures_data(),
AdvancedDataApiProbeStrategy {
observed: observed.clone(),
},
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,
},
);
let result = engine.run().expect("backtest succeeds");
assert_eq!(
observed.borrow().as_slice(),
&["factor=7;margin=000001.SZ;yield=0.020;dominant=IF2501;prices=1"]
);
assert!(result.analyzer_report().positions.is_empty());
}
#[test] #[test]
fn engine_runs_subscribed_tick_hooks_and_executes_tick_orders() { fn engine_runs_subscribed_tick_hooks_and_executes_tick_orders() {
let date = d(2025, 1, 2); let date = d(2025, 1, 2);

View File

@@ -40,19 +40,19 @@ Confirmed aligned areas:
close-today/close-yesterday, daily mark-to-market settlement, expiration close-today/close-yesterday, daily mark-to-market settlement, expiration
settlement, and runtime account views. settlement, and runtime account views.
Remaining parity gaps found by this pass: Parity gaps found by this pass and current closure state:
| Priority | Gap | RQAlpha capability | Current engine state | Next implementation | | Priority | Gap | RQAlpha capability | Current engine state | Next implementation |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| P0 | Futures intraday matching | Bar/tick matchers support futures orders through the same broker lifecycle, matching type, slippage, price limit, liquidity limit, volume limit, partial fill, and market-close rejection semantics. | Futures orders execute immediately from `FuturesOrderIntent` with explicit price and cost; they do not flow through the stock broker's pending/open-order matcher. | Add a futures broker/matcher path or generalize `BrokerSimulator` so futures intents can be market/limit/algo orders matched by bar/tick/open auction data. | | P0 | Futures intraday matching | Bar/tick matchers support futures orders through the same broker lifecycle, matching type, slippage, price limit, liquidity limit, volume limit, partial fill, and market-close rejection semantics. | Closed for daily/open/close and tick-price futures fills, including limit checks and partial quantity handling. Full order-book-depth counterparty sweeping remains out of scope unless production strategies require it. | Keep extending matching detail only when real futures tick depth data is available. |
| P0 | Futures open-order lifecycle | `SimulationBroker` keeps pending orders, supports `get_open_orders`, cancellation, before-trading activation, tick/bar rematching, and after-trading rejection. | Stock orders have open-order lifecycle; futures orders only emit immediate process/order/fill events. | Add futures pending limit orders, cancellation by id/symbol/all, open-order views, final order lookup, and market-close rejection. | | P0 | Futures open-order lifecycle | `SimulationBroker` keeps pending orders, supports `get_open_orders`, cancellation, before-trading activation, tick/bar rematching, and after-trading rejection. | Closed for futures pending limit orders, cross-day rematching, cancellation by id/symbol/all, and merged open-order runtime views. | Add more order status transitions only if UI requires RQAlpha's exact intermediate event names. |
| P0 | Combined multi-account NAV | RQAlpha portfolio aggregates account values across stock/future accounts. | `StrategyContext.accounts()` exposes both account views, but `DailyEquityPoint`, progress events, metrics, and holdings summary are still stock-portfolio based. | Add aggregate portfolio valuation that includes stock total equity plus futures account total value/margin/PnL, then compute metrics and progress from aggregate NAV. | | P0 | Combined multi-account NAV | RQAlpha portfolio aggregates account values across stock/future accounts. | Closed. `DailyEquityPoint`, progress events, and metrics now use aggregate stock + futures initial cash and total equity. | None. |
| 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. | `FuturesContractSpec` and `transaction_cost` are passed manually in order intents; there is no data-source-backed trading-parameter resolver. | Add futures instrument/trading-parameter tables and resolver APIs, then let order creation derive spec, margin, tick size, settlement price, and costs automatically. | | 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. | Current futures execution accepts precomputed transaction cost and stores it; it does not calculate costs from contract metadata. | Implement `FuturesTransactionCostModel` backed by trading parameters and route all futures executions through it. | | 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. | Current daily settlement accepts an injected settlement price map and expiration schedule; it does not automatically read settlement/prev_settlement series. | Extend data model with `settlement` and `prev_settlement` fields and support a configurable settlement price mode. | | 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. | Stock path has comparable guardrails; futures margin/position checks exist inside account execution, but submission-time validators and self-trade checks are incomplete. | Add futures-aware validator layer before order acceptance and share diagnostics with order events. | | 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. |
| 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. | Core stock backtest APIs are implemented; these advanced helper APIs are not exposed by `StrategyContext`/DSL. | Add read-only data proxy methods first, then expose stable DSL/strategy functions where data is available. | | 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`, using existing corporate-action, factor, market, and futures-parameter data. | Wire any missing frontend DSL aliases separately if the script layer needs them. |
| P2 | Analyzer/report parity | RQAlpha analyser can export richer trades, positions, benchmark, monthly returns, risk, and summary artifacts. | Engine returns metrics, equity curve, orders/fills/events/holdings, but not a full RQAlpha-style analyser artifact set. | Add normalized report builders on top of `BacktestResult` without changing execution semantics. | | P2 | Analyzer/report parity | RQAlpha analyser can export richer trades, positions, benchmark, monthly returns, risk, and summary artifacts. | Closed for normalized trades, positions, 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. | Engine has explicit Rust config and event/process records, not a full mod framework. | Only implement toggles required by production strategies; avoid recreating the whole RQAlpha mod system unless needed. | | P3 | Mod/config/plugin architecture | RQAlpha has pluggable mods, event bus extension points, and many config toggles. | Engine has explicit Rust config and event/process records, not a full mod framework. | Only implement toggles required by production strategies; avoid recreating the whole RQAlpha mod system unless needed. |
## Remaining Gaps ## Remaining Gaps
@@ -143,30 +143,30 @@ Remaining parity gaps found by this pass:
positions at settlement price positions at settlement price
- [x] data-driven futures expiration schedule in `BacktestEngine` settlement - [x] data-driven futures expiration schedule in `BacktestEngine` settlement
phase phase
- [ ] futures intraday matching integration - [x] futures intraday matching integration
- [ ] futures pending/open-order lifecycle and cancellation parity - [x] futures pending/open-order lifecycle and cancellation parity
- [ ] aggregate multi-account NAV/metrics/progress across stock and futures - [x] aggregate multi-account NAV/metrics/progress across stock and futures
- [ ] futures trading-parameter data source and automatic cost/margin resolver - [x] futures trading-parameter data source and automatic cost/margin resolver
- [ ] futures settlement/prev-settlement data integration and settlement mode - [x] futures settlement/prev-settlement data integration and settlement mode
- [ ] futures-aware submission validators and self-trade checks - [x] futures-aware submission validators and self-trade checks
### Phase 10: Advanced data API parity ### Phase 10: Advanced data API parity
- [ ] `get_dividend` - [x] `get_dividend`
- [ ] `get_split` - [x] `get_split`
- [ ] `get_yield_curve` - [x] `get_yield_curve`
- [ ] `get_factor` - [x] `get_factor`
- [ ] `get_margin_stocks` - [x] `get_margin_stocks`
- [ ] `get_securities_margin` - [x] `get_securities_margin`
- [ ] `get_dominant_future` - [x] `get_dominant_future`
- [ ] futures dominant price helpers - [x] futures dominant price helpers
### Phase 11: Analyzer / report parity ### Phase 11: Analyzer / report parity
- [ ] RQAlpha-style normalized trades report - [x] RQAlpha-style normalized trades report
- [ ] RQAlpha-style normalized positions report - [x] RQAlpha-style normalized positions report
- [ ] benchmark / monthly returns / risk summary artifacts - [x] benchmark / monthly returns / risk summary artifacts
- [ ] downloadable analyser output bundle - [x] downloadable analyser output bundle
## Execution Order ## Execution Order
@@ -189,6 +189,6 @@ Remaining parity gaps found by this pass:
## Current Step ## Current Step
Active implementation target: close the P0 gaps found by the 2026-04-24 Active implementation target: P0-P2 parity items are implemented in the engine
re-audit. The next code target should be aggregate multi-account NAV/metrics, core. Remaining future work should be driven by concrete production strategy or
followed by futures intraday matching and futures pending-order lifecycle. UI requirements rather than recreating RQAlpha's full plugin/mod framework.