Close RQAlpha P0-P2 parity gaps
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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)]
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user