Add futures depth matching and mod loader
This commit is contained in:
@@ -261,6 +261,43 @@ pub struct IntradayExecutionQuote {
|
|||||||
pub trading_phase: Option<String>,
|
pub trading_phase: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct IntradayOrderBookDepthLevel {
|
||||||
|
#[serde(with = "date_format")]
|
||||||
|
pub date: NaiveDate,
|
||||||
|
pub symbol: String,
|
||||||
|
#[serde(with = "datetime_format")]
|
||||||
|
pub timestamp: NaiveDateTime,
|
||||||
|
pub level: u8,
|
||||||
|
pub bid_price: f64,
|
||||||
|
pub bid_volume: u64,
|
||||||
|
pub ask_price: f64,
|
||||||
|
pub ask_volume: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntradayOrderBookDepthLevel {
|
||||||
|
pub fn executable_price(&self, side: crate::events::OrderSide) -> Option<f64> {
|
||||||
|
match side {
|
||||||
|
crate::events::OrderSide::Buy if self.ask_price.is_finite() && self.ask_price > 0.0 => {
|
||||||
|
Some(self.ask_price)
|
||||||
|
}
|
||||||
|
crate::events::OrderSide::Sell
|
||||||
|
if self.bid_price.is_finite() && self.bid_price > 0.0 =>
|
||||||
|
{
|
||||||
|
Some(self.bid_price)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn executable_volume(&self, side: crate::events::OrderSide) -> u64 {
|
||||||
|
match side {
|
||||||
|
crate::events::OrderSide::Buy => self.ask_volume,
|
||||||
|
crate::events::OrderSide::Sell => self.bid_volume,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl IntradayExecutionQuote {
|
impl IntradayExecutionQuote {
|
||||||
pub fn buy_price(&self) -> Option<f64> {
|
pub fn buy_price(&self) -> Option<f64> {
|
||||||
if self.ask1.is_finite() && self.ask1 > 0.0 {
|
if self.ask1.is_finite() && self.ask1 > 0.0 {
|
||||||
@@ -661,6 +698,7 @@ pub struct DataSet {
|
|||||||
candidate_index: HashMap<(NaiveDate, String), CandidateEligibility>,
|
candidate_index: HashMap<(NaiveDate, String), CandidateEligibility>,
|
||||||
corporate_actions_by_date: BTreeMap<NaiveDate, Vec<CorporateAction>>,
|
corporate_actions_by_date: BTreeMap<NaiveDate, Vec<CorporateAction>>,
|
||||||
execution_quotes_index: HashMap<(NaiveDate, String), Vec<IntradayExecutionQuote>>,
|
execution_quotes_index: HashMap<(NaiveDate, String), Vec<IntradayExecutionQuote>>,
|
||||||
|
order_book_depth_index: HashMap<(NaiveDate, String), Vec<IntradayOrderBookDepthLevel>>,
|
||||||
benchmark_by_date: BTreeMap<NaiveDate, BenchmarkSnapshot>,
|
benchmark_by_date: BTreeMap<NaiveDate, BenchmarkSnapshot>,
|
||||||
market_series_by_symbol: HashMap<String, SymbolPriceSeries>,
|
market_series_by_symbol: HashMap<String, SymbolPriceSeries>,
|
||||||
benchmark_series_cache: BenchmarkPriceSeries,
|
benchmark_series_cache: BenchmarkPriceSeries,
|
||||||
@@ -694,7 +732,13 @@ impl DataSet {
|
|||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
Self::from_components_with_actions_quotes_and_futures(
|
let order_book_depth_path = path.join("order_book_depth.csv");
|
||||||
|
let order_book_depth = if order_book_depth_path.exists() {
|
||||||
|
read_order_book_depth(&order_book_depth_path)?
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
Self::from_components_with_actions_quotes_futures_and_depth(
|
||||||
instruments,
|
instruments,
|
||||||
market,
|
market,
|
||||||
factors,
|
factors,
|
||||||
@@ -703,6 +747,7 @@ impl DataSet {
|
|||||||
corporate_actions,
|
corporate_actions,
|
||||||
execution_quotes,
|
execution_quotes,
|
||||||
futures_params,
|
futures_params,
|
||||||
|
order_book_depth,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,7 +775,13 @@ impl DataSet {
|
|||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
Self::from_components_with_actions_quotes_and_futures(
|
let order_book_depth_dir = path.join("order_book_depth");
|
||||||
|
let order_book_depth = if order_book_depth_dir.exists() {
|
||||||
|
read_partitioned_dir(&order_book_depth_dir, read_order_book_depth)?
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
Self::from_components_with_actions_quotes_futures_and_depth(
|
||||||
instruments,
|
instruments,
|
||||||
market,
|
market,
|
||||||
factors,
|
factors,
|
||||||
@@ -739,6 +790,7 @@ impl DataSet {
|
|||||||
corporate_actions,
|
corporate_actions,
|
||||||
execution_quotes,
|
execution_quotes,
|
||||||
futures_params,
|
futures_params,
|
||||||
|
order_book_depth,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -809,6 +861,30 @@ impl DataSet {
|
|||||||
corporate_actions: Vec<CorporateAction>,
|
corporate_actions: Vec<CorporateAction>,
|
||||||
execution_quotes: Vec<IntradayExecutionQuote>,
|
execution_quotes: Vec<IntradayExecutionQuote>,
|
||||||
futures_params: Vec<FuturesTradingParameter>,
|
futures_params: Vec<FuturesTradingParameter>,
|
||||||
|
) -> Result<Self, DataSetError> {
|
||||||
|
Self::from_components_with_actions_quotes_futures_and_depth(
|
||||||
|
instruments,
|
||||||
|
market,
|
||||||
|
factors,
|
||||||
|
candidates,
|
||||||
|
benchmarks,
|
||||||
|
corporate_actions,
|
||||||
|
execution_quotes,
|
||||||
|
futures_params,
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_components_with_actions_quotes_futures_and_depth(
|
||||||
|
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>,
|
||||||
|
order_book_depth: Vec<IntradayOrderBookDepthLevel>,
|
||||||
) -> 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());
|
||||||
@@ -837,6 +913,7 @@ impl DataSet {
|
|||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
let corporate_actions_by_date = group_by_date(corporate_actions, |item| item.date);
|
let corporate_actions_by_date = group_by_date(corporate_actions, |item| item.date);
|
||||||
let execution_quotes_index = build_execution_quote_index(execution_quotes);
|
let execution_quotes_index = build_execution_quote_index(execution_quotes);
|
||||||
|
let order_book_depth_index = build_order_book_depth_index(order_book_depth);
|
||||||
|
|
||||||
let benchmark_by_date = benchmarks
|
let benchmark_by_date = benchmarks
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -860,6 +937,7 @@ impl DataSet {
|
|||||||
candidate_index,
|
candidate_index,
|
||||||
corporate_actions_by_date,
|
corporate_actions_by_date,
|
||||||
execution_quotes_index,
|
execution_quotes_index,
|
||||||
|
order_book_depth_index,
|
||||||
benchmark_by_date,
|
benchmark_by_date,
|
||||||
market_series_by_symbol,
|
market_series_by_symbol,
|
||||||
benchmark_series_cache,
|
benchmark_series_cache,
|
||||||
@@ -936,6 +1014,17 @@ impl DataSet {
|
|||||||
.unwrap_or(&[])
|
.unwrap_or(&[])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn order_book_depth_on(
|
||||||
|
&self,
|
||||||
|
date: NaiveDate,
|
||||||
|
symbol: &str,
|
||||||
|
) -> &[IntradayOrderBookDepthLevel] {
|
||||||
|
self.order_book_depth_index
|
||||||
|
.get(&(date, symbol.to_string()))
|
||||||
|
.map(Vec::as_slice)
|
||||||
|
.unwrap_or(&[])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn execution_quotes_on_date(&self, date: NaiveDate) -> Vec<IntradayExecutionQuote> {
|
pub fn execution_quotes_on_date(&self, date: NaiveDate) -> Vec<IntradayExecutionQuote> {
|
||||||
let mut quotes = self
|
let mut quotes = self
|
||||||
.execution_quotes_index
|
.execution_quotes_index
|
||||||
@@ -1978,6 +2067,27 @@ fn read_execution_quotes(path: &Path) -> Result<Vec<IntradayExecutionQuote>, Dat
|
|||||||
Ok(quotes)
|
Ok(quotes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_order_book_depth(path: &Path) -> Result<Vec<IntradayOrderBookDepthLevel>, DataSetError> {
|
||||||
|
let rows = read_rows(path)?;
|
||||||
|
let mut levels = Vec::new();
|
||||||
|
for row in rows {
|
||||||
|
levels.push(IntradayOrderBookDepthLevel {
|
||||||
|
date: row.parse_date(0)?,
|
||||||
|
symbol: row.get(1)?.to_string(),
|
||||||
|
timestamp: row.parse_datetime(2)?,
|
||||||
|
level: row
|
||||||
|
.parse_optional_u32(3)
|
||||||
|
.unwrap_or(1)
|
||||||
|
.clamp(1, u8::MAX as u32) as u8,
|
||||||
|
bid_price: row.parse_optional_f64(4).unwrap_or_default(),
|
||||||
|
bid_volume: row.parse_optional_u64(5).unwrap_or_default(),
|
||||||
|
ask_price: row.parse_optional_f64(6).unwrap_or_default(),
|
||||||
|
ask_volume: row.parse_optional_u64(7).unwrap_or_default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(levels)
|
||||||
|
}
|
||||||
|
|
||||||
fn read_futures_trading_parameters(
|
fn read_futures_trading_parameters(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
) -> Result<Vec<FuturesTradingParameter>, DataSetError> {
|
) -> Result<Vec<FuturesTradingParameter>, DataSetError> {
|
||||||
@@ -2329,6 +2439,28 @@ fn build_execution_quote_index(
|
|||||||
grouped
|
grouped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_order_book_depth_index(
|
||||||
|
order_book_depth: Vec<IntradayOrderBookDepthLevel>,
|
||||||
|
) -> HashMap<(NaiveDate, String), Vec<IntradayOrderBookDepthLevel>> {
|
||||||
|
let mut grouped = HashMap::<(NaiveDate, String), Vec<IntradayOrderBookDepthLevel>>::new();
|
||||||
|
for level in order_book_depth {
|
||||||
|
grouped
|
||||||
|
.entry((level.date, level.symbol.clone()))
|
||||||
|
.or_default()
|
||||||
|
.push(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
for levels in grouped.values_mut() {
|
||||||
|
levels.sort_by(|left, right| {
|
||||||
|
left.timestamp
|
||||||
|
.cmp(&right.timestamp)
|
||||||
|
.then(left.level.cmp(&right.level))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
grouped
|
||||||
|
}
|
||||||
|
|
||||||
fn build_eligible_universe(
|
fn build_eligible_universe(
|
||||||
factor_by_date: &BTreeMap<NaiveDate, Vec<DailyFactorSnapshot>>,
|
factor_by_date: &BTreeMap<NaiveDate, Vec<DailyFactorSnapshot>>,
|
||||||
candidate_index: &HashMap<(NaiveDate, String), CandidateEligibility>,
|
candidate_index: &HashMap<(NaiveDate, String), CandidateEligibility>,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use thiserror::Error;
|
|||||||
use crate::broker::{BrokerExecutionReport, BrokerSimulator, MatchingType};
|
use crate::broker::{BrokerExecutionReport, BrokerSimulator, MatchingType};
|
||||||
use crate::cost::CostModel;
|
use crate::cost::CostModel;
|
||||||
use crate::data::{BenchmarkSnapshot, DataSet, DataSetError, PriceField};
|
use crate::data::{BenchmarkSnapshot, DataSet, DataSetError, PriceField};
|
||||||
use crate::event_bus::{BacktestProcessMod, ProcessEventBus};
|
use crate::event_bus::{BacktestProcessMod, BacktestProcessModLoader, ProcessEventBus};
|
||||||
use crate::events::{
|
use crate::events::{
|
||||||
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
||||||
ProcessEventKind,
|
ProcessEventKind,
|
||||||
@@ -401,6 +401,22 @@ impl<S, C, R> BacktestEngine<S, C, R> {
|
|||||||
{
|
{
|
||||||
self.process_event_bus.install_mod(module);
|
self.process_event_bus.install_mod(module);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn install_process_mod_loader(
|
||||||
|
&mut self,
|
||||||
|
loader: &mut BacktestProcessModLoader,
|
||||||
|
) -> Vec<String> {
|
||||||
|
self.process_event_bus.install_mod_loader(loader)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install_enabled_process_mods(
|
||||||
|
&mut self,
|
||||||
|
loader: &mut BacktestProcessModLoader,
|
||||||
|
enabled_names: &[String],
|
||||||
|
) -> Vec<String> {
|
||||||
|
self.process_event_bus
|
||||||
|
.install_enabled_mods(loader, enabled_names)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, C, R> BacktestEngine<S, C, R>
|
impl<S, C, R> BacktestEngine<S, C, R>
|
||||||
@@ -1119,6 +1135,15 @@ where
|
|||||||
intent: &FuturesOrderIntent,
|
intent: &FuturesOrderIntent,
|
||||||
) -> Option<(f64, u32)> {
|
) -> Option<(f64, u32)> {
|
||||||
let snapshot = self.data.market(date, &intent.symbol);
|
let snapshot = self.data.market(date, &intent.symbol);
|
||||||
|
if matches!(
|
||||||
|
self.broker.matching_type(),
|
||||||
|
MatchingType::NextTickBestCounterparty | MatchingType::CounterpartyOffer
|
||||||
|
) {
|
||||||
|
let depth = self.data.order_book_depth_on(date, &intent.symbol);
|
||||||
|
if !depth.is_empty() {
|
||||||
|
return self.resolve_futures_depth_fill(date, intent, snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
let quotes = self.data.execution_quotes_on(date, &intent.symbol);
|
let quotes = self.data.execution_quotes_on(date, &intent.symbol);
|
||||||
for quote in quotes {
|
for quote in quotes {
|
||||||
let price = match self.broker.matching_type() {
|
let price = match self.broker.matching_type() {
|
||||||
@@ -1172,6 +1197,63 @@ where
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_futures_depth_fill(
|
||||||
|
&self,
|
||||||
|
date: NaiveDate,
|
||||||
|
intent: &FuturesOrderIntent,
|
||||||
|
snapshot: Option<&crate::data::DailyMarketSnapshot>,
|
||||||
|
) -> Option<(f64, u32)> {
|
||||||
|
let depth = self.data.order_book_depth_on(date, &intent.symbol);
|
||||||
|
let mut cursor = 0usize;
|
||||||
|
while cursor < depth.len() {
|
||||||
|
let timestamp = depth[cursor].timestamp;
|
||||||
|
let start = cursor;
|
||||||
|
while cursor < depth.len() && depth[cursor].timestamp == timestamp {
|
||||||
|
cursor += 1;
|
||||||
|
}
|
||||||
|
let mut levels = depth[start..cursor].iter().collect::<Vec<_>>();
|
||||||
|
levels.sort_by(|left, right| left.level.cmp(&right.level));
|
||||||
|
|
||||||
|
let mut filled_quantity = 0_u32;
|
||||||
|
let mut gross_amount = 0.0_f64;
|
||||||
|
for level in levels {
|
||||||
|
let Some(price) = level.executable_price(intent.side()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let can_trade = if let Some(snapshot) = snapshot {
|
||||||
|
self.futures_price_can_trade(snapshot, intent.side(), price, intent.limit_price)
|
||||||
|
} else {
|
||||||
|
futures_limit_satisfied(intent.side(), price, intent.limit_price)
|
||||||
|
};
|
||||||
|
if !can_trade {
|
||||||
|
if intent.limit_price.is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let available_quantity =
|
||||||
|
level.executable_volume(intent.side()).min(u32::MAX as u64) as u32;
|
||||||
|
if available_quantity == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let remaining = intent.quantity.saturating_sub(filled_quantity);
|
||||||
|
if remaining == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let take_quantity = remaining.min(available_quantity);
|
||||||
|
gross_amount += price * take_quantity as f64;
|
||||||
|
filled_quantity += take_quantity;
|
||||||
|
if filled_quantity >= intent.quantity {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if filled_quantity > 0 {
|
||||||
|
return Some((gross_amount / filled_quantity as f64, filled_quantity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn futures_price_can_trade(
|
fn futures_price_can_trade(
|
||||||
&self,
|
&self,
|
||||||
snapshot: &crate::data::DailyMarketSnapshot,
|
snapshot: &crate::data::DailyMarketSnapshot,
|
||||||
|
|||||||
@@ -9,6 +9,65 @@ pub trait BacktestProcessMod {
|
|||||||
fn install(&mut self, bus: &mut ProcessEventBus);
|
fn install(&mut self, bus: &mut ProcessEventBus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct BacktestProcessModLoader {
|
||||||
|
modules: Vec<Box<dyn BacktestProcessMod>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BacktestProcessModLoader {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register<M>(&mut self, module: M)
|
||||||
|
where
|
||||||
|
M: BacktestProcessMod + 'static,
|
||||||
|
{
|
||||||
|
self.modules.push(Box::new(module));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn module_names(&self) -> Vec<String> {
|
||||||
|
self.modules
|
||||||
|
.iter()
|
||||||
|
.map(|module| module.name().to_string())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install_all(&mut self, bus: &mut ProcessEventBus) -> Vec<String> {
|
||||||
|
self.modules
|
||||||
|
.iter_mut()
|
||||||
|
.map(|module| {
|
||||||
|
let name = module.name().to_string();
|
||||||
|
module.install(bus);
|
||||||
|
name
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install_enabled(
|
||||||
|
&mut self,
|
||||||
|
bus: &mut ProcessEventBus,
|
||||||
|
enabled_names: &[String],
|
||||||
|
) -> Vec<String> {
|
||||||
|
if enabled_names.is_empty() {
|
||||||
|
return self.install_all(bus);
|
||||||
|
}
|
||||||
|
self.modules
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|module| {
|
||||||
|
enabled_names
|
||||||
|
.iter()
|
||||||
|
.any(|name| name.eq_ignore_ascii_case(module.name()))
|
||||||
|
})
|
||||||
|
.map(|module| {
|
||||||
|
let name = module.name().to_string();
|
||||||
|
module.install(bus);
|
||||||
|
name
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ProcessEventBus {
|
pub struct ProcessEventBus {
|
||||||
listeners: BTreeMap<ProcessEventKind, Vec<ProcessEventListener>>,
|
listeners: BTreeMap<ProcessEventKind, Vec<ProcessEventListener>>,
|
||||||
@@ -54,6 +113,18 @@ impl ProcessEventBus {
|
|||||||
module.install(self);
|
module.install(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn install_mod_loader(&mut self, loader: &mut BacktestProcessModLoader) -> Vec<String> {
|
||||||
|
loader.install_all(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install_enabled_mods(
|
||||||
|
&mut self,
|
||||||
|
loader: &mut BacktestProcessModLoader,
|
||||||
|
enabled_names: &[String],
|
||||||
|
) -> Vec<String> {
|
||||||
|
loader.install_enabled(self, enabled_names)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn publish(&mut self, event: &ProcessEvent) {
|
pub fn publish(&mut self, event: &ProcessEvent) {
|
||||||
if let Some(listeners) = self.listeners.get_mut(&event.kind) {
|
if let Some(listeners) = self.listeners.get_mut(&event.kind) {
|
||||||
for listener in listeners {
|
for listener in listeners {
|
||||||
|
|||||||
@@ -22,15 +22,15 @@ pub use cost::{ChinaAShareCostModel, CostModel, TradingCost};
|
|||||||
pub use data::{
|
pub use data::{
|
||||||
BenchmarkSnapshot, CandidateEligibility, CorporateAction, DailyFactorSnapshot,
|
BenchmarkSnapshot, CandidateEligibility, CorporateAction, DailyFactorSnapshot,
|
||||||
DailyMarketSnapshot, DailySnapshotBundle, DataSet, DataSetError, DividendRecord,
|
DailyMarketSnapshot, DailySnapshotBundle, DataSet, DataSetError, DividendRecord,
|
||||||
EligibleUniverseSnapshot, FactorValue, IntradayExecutionQuote, PriceBar, PriceField,
|
EligibleUniverseSnapshot, FactorValue, IntradayExecutionQuote, IntradayOrderBookDepthLevel,
|
||||||
SecuritiesMarginRecord, SplitRecord, YieldCurvePoint,
|
PriceBar, PriceField, SecuritiesMarginRecord, SplitRecord, YieldCurvePoint,
|
||||||
};
|
};
|
||||||
pub use engine::{
|
pub use engine::{
|
||||||
AnalyzerMonthlyReturnRow, AnalyzerPositionRow, AnalyzerReport, AnalyzerRiskSummary,
|
AnalyzerMonthlyReturnRow, AnalyzerPositionRow, AnalyzerReport, AnalyzerRiskSummary,
|
||||||
AnalyzerTradeRow, BacktestConfig, BacktestDayProgress, BacktestEngine, BacktestError,
|
AnalyzerTradeRow, BacktestConfig, BacktestDayProgress, BacktestEngine, BacktestError,
|
||||||
BacktestResult, DailyEquityPoint,
|
BacktestResult, DailyEquityPoint,
|
||||||
};
|
};
|
||||||
pub use event_bus::{BacktestProcessMod, ProcessEventBus};
|
pub use event_bus::{BacktestProcessMod, BacktestProcessModLoader, ProcessEventBus};
|
||||||
pub use events::{
|
pub use events::{
|
||||||
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
||||||
ProcessEventKind,
|
ProcessEventKind,
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
|||||||
},
|
},
|
||||||
ManualSection {
|
ManualSection {
|
||||||
title: "execution.matching_type / execution.slippage".to_string(),
|
title: "execution.matching_type / execution.slippage".to_string(),
|
||||||
detail: "设置撮合模式和滑点。支持 execution.matching_type(\"next_tick_last\" | \"next_tick_best_own\" | \"next_tick_best_counterparty\" | \"counterparty_offer\" | \"vwap\" | \"current_bar_close\" | \"next_bar_open\" | \"open_auction\")。其中 next_tick_last 使用 tick 的 last_price;next_tick_best_own / next_tick_best_counterparty 会按 L1 买一卖一近似 rqalpha 的 tick 最优价语义,counterparty_offer 当前也按 L1 对手方报价近似实现;vwap 会在盘中执行价链路上聚合多笔成交为单条 VWAP 成交;open_auction 使用当日集合竞价开盘价 day_open 进行撮合,且不额外施加滑点,并按竞价成交量而不是盘口一档流动性限制成交;滑点支持 execution.slippage(\"none\") / execution.slippage(\"price_ratio\", 0.001) / execution.slippage(\"tick_size\", 1) / execution.slippage(\"limit_price\"),其中 limit_price 会在限价单成交时按挂单价模拟 rqalpha 的最坏成交价。".to_string(),
|
detail: "设置撮合模式和滑点。支持 execution.matching_type(\"next_tick_last\" | \"next_tick_best_own\" | \"next_tick_best_counterparty\" | \"counterparty_offer\" | \"vwap\" | \"current_bar_close\" | \"next_bar_open\" | \"open_auction\")。其中 next_tick_last 使用 tick 的 last_price;next_tick_best_own / next_tick_best_counterparty 会按 L1 买一卖一近似 rqalpha 的 tick 最优价语义;counterparty_offer 在存在 order_book_depth 多档盘口数据时会按真实档位逐档扫单并计算加权成交价,不存在 depth 时回退 L1 对手方报价;vwap 会在盘中执行价链路上聚合多笔成交为单条 VWAP 成交;open_auction 使用当日集合竞价开盘价 day_open 进行撮合,且不额外施加滑点,并按竞价成交量而不是盘口一档流动性限制成交;滑点支持 execution.slippage(\"none\") / execution.slippage(\"price_ratio\", 0.001) / execution.slippage(\"tick_size\", 1) / execution.slippage(\"limit_price\"),其中 limit_price 会在限价单成交时按挂单价模拟 rqalpha 的最坏成交价。".to_string(),
|
||||||
},
|
},
|
||||||
ManualSection {
|
ManualSection {
|
||||||
title: "trading.rotation / order.* / cancel.* / update_universe / subscribe".to_string(),
|
title: "trading.rotation / order.* / cancel.* / update_universe / subscribe".to_string(),
|
||||||
@@ -238,6 +238,11 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
|||||||
detail: "股票指标因子原表,可映射进 factors[...]。".to_string(),
|
detail: "股票指标因子原表,可映射进 factors[...]。".to_string(),
|
||||||
fields: vec![],
|
fields: vec![],
|
||||||
},
|
},
|
||||||
|
ManualFactorSource {
|
||||||
|
table: "order_book_depth.csv / order_book_depth/".to_string(),
|
||||||
|
detail: "可选多档盘口数据源,字段为 date,symbol,timestamp,level,bid_price,bid_volume,ask_price,ask_volume。存在该数据时,期货 counterparty_offer / next_tick_best_counterparty 可按真实多档盘口逐档扫单;不存在时不会伪造 depth。".to_string(),
|
||||||
|
fields: vec![],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
examples: vec![
|
examples: vec![
|
||||||
ManualExample {
|
ManualExample {
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use chrono::{NaiveDate, NaiveDateTime};
|
use chrono::{NaiveDate, NaiveDateTime};
|
||||||
use fidc_core::{
|
use fidc_core::{
|
||||||
BacktestConfig, BacktestEngine, BacktestProcessMod, BenchmarkSnapshot, BrokerSimulator,
|
BacktestConfig, BacktestEngine, BacktestProcessMod, BacktestProcessModLoader,
|
||||||
CandidateEligibility, ChinaAShareCostModel, ChinaEquityRuleHooks, DailyFactorSnapshot,
|
BenchmarkSnapshot, BrokerSimulator, CandidateEligibility, ChinaAShareCostModel,
|
||||||
DailyMarketSnapshot, DataSet, FuturesAccountState, FuturesCommissionType, FuturesContractSpec,
|
ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, FuturesAccountState,
|
||||||
FuturesDirection, FuturesOrderIntent, FuturesTradingParameter, Instrument,
|
FuturesCommissionType, FuturesContractSpec, FuturesDirection, FuturesOrderIntent,
|
||||||
IntradayExecutionQuote, OpenOrderView, OrderIntent, OrderSide, OrderStatus, PortfolioState,
|
FuturesTradingParameter, Instrument, IntradayExecutionQuote, IntradayOrderBookDepthLevel,
|
||||||
PriceField, ProcessEvent, ProcessEventBus, ProcessEventKind, ScheduleRule, ScheduleStage,
|
MatchingType, OpenOrderView, OrderIntent, OrderSide, OrderStatus, PortfolioState, PriceField,
|
||||||
ScheduleTimeRule, Strategy, StrategyContext, StrategyDecision,
|
ProcessEvent, ProcessEventBus, ProcessEventKind, ScheduleRule, ScheduleStage, ScheduleTimeRule,
|
||||||
|
Strategy, StrategyContext, StrategyDecision,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
||||||
@@ -407,6 +408,37 @@ impl Strategy for FuturesLimitOrderStrategy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FuturesDepthLimitOrderStrategy;
|
||||||
|
|
||||||
|
impl Strategy for FuturesDepthLimitOrderStrategy {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"futures-depth-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),
|
||||||
|
3,
|
||||||
|
3990.0,
|
||||||
|
0.0,
|
||||||
|
"sweep depth until limit",
|
||||||
|
),
|
||||||
|
}],
|
||||||
|
..StrategyDecision::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct AdvancedDataApiProbeStrategy {
|
struct AdvancedDataApiProbeStrategy {
|
||||||
observed: Rc<RefCell<Vec<String>>>,
|
observed: Rc<RefCell<Vec<String>>>,
|
||||||
}
|
}
|
||||||
@@ -1399,6 +1431,127 @@ fn engine_matches_pending_futures_limit_order_with_data_driven_costs() {
|
|||||||
assert!((position.contract_multiplier - 300.0).abs() < 1e-6);
|
assert!((position.contract_multiplier - 300.0).abs() < 1e-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn engine_sweeps_futures_order_book_depth_when_available() {
|
||||||
|
let date = d(2025, 1, 2);
|
||||||
|
let data = DataSet::from_components_with_actions_quotes_futures_and_depth(
|
||||||
|
vec![
|
||||||
|
Instrument {
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
name: "Anchor".to_string(),
|
||||||
|
board: "SZ".to_string(),
|
||||||
|
round_lot: 100,
|
||||||
|
listed_at: Some(d(2020, 1, 1)),
|
||||||
|
delisted_at: None,
|
||||||
|
status: "active".to_string(),
|
||||||
|
},
|
||||||
|
Instrument {
|
||||||
|
symbol: "IF2501".to_string(),
|
||||||
|
name: "IF".to_string(),
|
||||||
|
board: "FUTURE".to_string(),
|
||||||
|
round_lot: 1,
|
||||||
|
listed_at: Some(d(2024, 1, 1)),
|
||||||
|
delisted_at: None,
|
||||||
|
status: "active".to_string(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
market_row(date, "000001.SZ", 10.0, 10.0),
|
||||||
|
market_row(date, "IF2501", 4000.0, 4000.0),
|
||||||
|
],
|
||||||
|
vec![factor_row(date, "000001.SZ", BTreeMap::new())],
|
||||||
|
vec![candidate_row(date, "000001.SZ")],
|
||||||
|
vec![benchmark_row(date)],
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
vec![FuturesTradingParameter {
|
||||||
|
symbol: "IF2501".to_string(),
|
||||||
|
effective_date: Some(date),
|
||||||
|
contract_multiplier: 300.0,
|
||||||
|
long_margin_rate: 0.12,
|
||||||
|
short_margin_rate: 0.14,
|
||||||
|
commission_type: FuturesCommissionType::ByVolume,
|
||||||
|
open_commission_ratio: 2.5,
|
||||||
|
close_commission_ratio: 2.0,
|
||||||
|
close_today_commission_ratio: 3.0,
|
||||||
|
price_tick: 0.2,
|
||||||
|
}],
|
||||||
|
vec![
|
||||||
|
IntradayOrderBookDepthLevel {
|
||||||
|
date,
|
||||||
|
symbol: "IF2501".to_string(),
|
||||||
|
timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
|
||||||
|
level: 1,
|
||||||
|
bid_price: 3987.8,
|
||||||
|
bid_volume: 1,
|
||||||
|
ask_price: 3988.0,
|
||||||
|
ask_volume: 1,
|
||||||
|
},
|
||||||
|
IntradayOrderBookDepthLevel {
|
||||||
|
date,
|
||||||
|
symbol: "IF2501".to_string(),
|
||||||
|
timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
|
||||||
|
level: 2,
|
||||||
|
bid_price: 3987.6,
|
||||||
|
bid_volume: 1,
|
||||||
|
ask_price: 3990.0,
|
||||||
|
ask_volume: 1,
|
||||||
|
},
|
||||||
|
IntradayOrderBookDepthLevel {
|
||||||
|
date,
|
||||||
|
symbol: "IF2501".to_string(),
|
||||||
|
timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
|
||||||
|
level: 3,
|
||||||
|
bid_price: 3987.4,
|
||||||
|
bid_volume: 10,
|
||||||
|
ask_price: 3994.0,
|
||||||
|
ask_volume: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.expect("depth dataset");
|
||||||
|
let broker = BrokerSimulator::new_with_execution_price(
|
||||||
|
ChinaAShareCostModel::default(),
|
||||||
|
ChinaEquityRuleHooks::default(),
|
||||||
|
PriceField::Last,
|
||||||
|
)
|
||||||
|
.with_matching_type(MatchingType::CounterpartyOffer);
|
||||||
|
let mut engine = BacktestEngine::new(
|
||||||
|
data,
|
||||||
|
FuturesDepthLimitOrderStrategy,
|
||||||
|
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::Last,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_futures_initial_cash(1_000_000.0);
|
||||||
|
|
||||||
|
let result = engine.run().expect("backtest succeeds");
|
||||||
|
|
||||||
|
let fill = result
|
||||||
|
.fills
|
||||||
|
.iter()
|
||||||
|
.find(|fill| fill.symbol == "IF2501")
|
||||||
|
.expect("depth futures fill");
|
||||||
|
assert_eq!(fill.quantity, 2);
|
||||||
|
assert!((fill.price - 3989.0).abs() < 1e-6);
|
||||||
|
assert!(result.order_events.iter().any(|event| {
|
||||||
|
event.symbol == "IF2501"
|
||||||
|
&& event.status == OrderStatus::PartiallyFilled
|
||||||
|
&& event.filled_quantity == 2
|
||||||
|
}));
|
||||||
|
assert!(result.order_events.iter().any(|event| {
|
||||||
|
event.symbol == "IF2501"
|
||||||
|
&& event.status == OrderStatus::Pending
|
||||||
|
&& event.requested_quantity == 3
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn strategy_context_exposes_advanced_rqdata_helpers() {
|
fn strategy_context_exposes_advanced_rqdata_helpers() {
|
||||||
let observed = Rc::new(RefCell::new(Vec::new()));
|
let observed = Rc::new(RefCell::new(Vec::new()));
|
||||||
@@ -2837,6 +2990,25 @@ impl BacktestProcessMod for AnyEventCountingMod {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct NamedEventCountingMod {
|
||||||
|
name: &'static str,
|
||||||
|
sink: Rc<RefCell<Vec<String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BacktestProcessMod for NamedEventCountingMod {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install(&mut self, bus: &mut ProcessEventBus) {
|
||||||
|
let sink = self.sink.clone();
|
||||||
|
bus.add_any_listener(move |event: &ProcessEvent| {
|
||||||
|
sink.borrow_mut()
|
||||||
|
.push(format!("{:?}:{}", event.kind, event.detail));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn engine_installs_process_mods_on_event_bus() {
|
fn engine_installs_process_mods_on_event_bus() {
|
||||||
let date = d(2025, 1, 2);
|
let date = d(2025, 1, 2);
|
||||||
@@ -2874,6 +3046,58 @@ fn engine_installs_process_mods_on_event_bus() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn engine_installs_enabled_process_mods_from_loader() {
|
||||||
|
let date = d(2025, 1, 2);
|
||||||
|
let data = single_day_anchor_data(date);
|
||||||
|
let broker = BrokerSimulator::new_with_execution_price(
|
||||||
|
ChinaAShareCostModel::default(),
|
||||||
|
ChinaEquityRuleHooks::default(),
|
||||||
|
PriceField::DayOpen,
|
||||||
|
);
|
||||||
|
let mut engine = BacktestEngine::new(
|
||||||
|
data,
|
||||||
|
HookProbeStrategy {
|
||||||
|
log: Rc::new(RefCell::new(Vec::new())),
|
||||||
|
},
|
||||||
|
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::DayOpen,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let enabled_sink = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let disabled_sink = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let mut loader = BacktestProcessModLoader::new();
|
||||||
|
loader.register(NamedEventCountingMod {
|
||||||
|
name: "enabled-counter",
|
||||||
|
sink: enabled_sink.clone(),
|
||||||
|
});
|
||||||
|
loader.register(NamedEventCountingMod {
|
||||||
|
name: "disabled-counter",
|
||||||
|
sink: disabled_sink.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
loader.module_names(),
|
||||||
|
vec![
|
||||||
|
"enabled-counter".to_string(),
|
||||||
|
"disabled-counter".to_string()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
let installed =
|
||||||
|
engine.install_enabled_process_mods(&mut loader, &["enabled-counter".to_string()]);
|
||||||
|
engine.run().expect("backtest run");
|
||||||
|
|
||||||
|
assert_eq!(installed, vec!["enabled-counter".to_string()]);
|
||||||
|
assert!(!enabled_sink.borrow().is_empty());
|
||||||
|
assert!(disabled_sink.borrow().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn engine_applies_dynamic_universe_and_subscription_directives() {
|
fn engine_applies_dynamic_universe_and_subscription_directives() {
|
||||||
let dates = [d(2025, 1, 2), d(2025, 1, 3), d(2025, 1, 6)];
|
let dates = [d(2025, 1, 2), d(2025, 1, 3), d(2025, 1, 6)];
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ 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. | Closed for daily/open/close and tick-price futures fills, including limit checks and partial quantity handling. Full multi-level order-book sweeping remains data-dependent and intentionally not faked from L1 data. | Add true depth sweeping only when production futures tick depth exists. |
|
| 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, tick-price futures fills, and true multi-level order-book sweeping when optional `order_book_depth` data exists. L1-only data still uses the existing L1 matcher and is not inflated into fake depth. | Extend depth fields only if production vendors expose more levels or exchange-specific fields. |
|
||||||
| 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 | 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. | Closed. `DailyEquityPoint`, progress events, and metrics now use aggregate stock + futures initial cash and total equity. | None. |
|
| 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. | 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 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. |
|
||||||
@@ -53,7 +53,7 @@ Parity gaps found by this pass and current closure state:
|
|||||||
| P1 | Frontend risk validators for futures | RQAlpha applies cash/margin, position closable, price-limit, trading-status, and self-trade validators before order submission. | Closed for zero quantity, invalid limit price, self-trade crossing risk, paused/no executable price, price-limit, margin, and close-position rejection diagnostics. | Add exchange-specific validators only as needed. |
|
| P1 | Frontend risk validators for futures | RQAlpha applies cash/margin, position closable, price-limit, trading-status, and self-trade validators before order submission. | Closed for zero quantity, invalid limit price, 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. | Closed. These APIs are available through `DataSet` and `StrategyContext`; platform expressions also expose focused helpers such as `dividend_cash`, `factor_value`, `yield_curve`, `is_margin_stock`, `dominant_future`, and `dominant_future_price`. | Add more DSL aliases only when users need specific names. |
|
| P2 | RQData helper APIs | RQAlpha exposes `get_dividend`, `get_split`, `get_yield_curve`, `get_factor`, `get_margin_stocks`, `get_securities_margin`, `get_dominant_future`, and dominant futures price APIs. | Closed. These APIs are available through `DataSet` and `StrategyContext`; platform expressions also expose focused helpers such as `dividend_cash`, `factor_value`, `yield_curve`, `is_margin_stock`, `dominant_future`, and `dominant_future_price`. | Add more DSL aliases only when users need specific names. |
|
||||||
| P2 | Analyzer/report parity | RQAlpha analyser can export richer trades, positions, benchmark, monthly returns, risk, and summary artifacts. | Closed for normalized trades, positions, monthly returns, risk summary, equity curve, benchmark series, metrics, and JSON report bundle via `BacktestResult::analyzer_report(_json)`. | UI/service download endpoints can serialize this report directly. |
|
| P2 | Analyzer/report parity | RQAlpha analyser can export richer trades, positions, benchmark, monthly returns, risk, and summary artifacts. | Closed for normalized trades, positions, monthly returns, risk summary, equity curve, benchmark series, metrics, and JSON report bundle via `BacktestResult::analyzer_report(_json)`. | UI/service download endpoints can serialize this report directly. |
|
||||||
| P3 | Mod/config/plugin architecture | RQAlpha has pluggable mods, event bus extension points, and many config toggles. | Partially closed with a lightweight `BacktestProcessMod` interface on top of `ProcessEventBus`; this supports event-driven extensions without recreating RQAlpha's global mod loader. | Add concrete production mods/toggles as requirements appear. |
|
| P3 | Mod/config/plugin architecture | RQAlpha has pluggable mods, event bus extension points, and many config toggles. | Closed for a lightweight engine-native model: `BacktestProcessMod`, `BacktestProcessModLoader`, enabled-name installation, and event-bus lifecycle hooks. It intentionally avoids RQAlpha's Python global mod loader. | Add concrete production mods/toggles as requirements appear. |
|
||||||
|
|
||||||
## Remaining Gaps
|
## Remaining Gaps
|
||||||
|
|
||||||
@@ -149,6 +149,7 @@ Parity gaps found by this pass and current closure state:
|
|||||||
- [x] futures trading-parameter data source and automatic cost/margin resolver
|
- [x] futures trading-parameter data source and automatic cost/margin resolver
|
||||||
- [x] futures settlement/prev-settlement data integration and settlement mode
|
- [x] futures settlement/prev-settlement data integration and settlement mode
|
||||||
- [x] futures-aware submission validators and self-trade checks
|
- [x] futures-aware submission validators and self-trade checks
|
||||||
|
- [x] optional true multi-level futures order-book depth data and sweep matching
|
||||||
|
|
||||||
### Phase 10: Advanced data API parity
|
### Phase 10: Advanced data API parity
|
||||||
|
|
||||||
@@ -173,7 +174,9 @@ Parity gaps found by this pass and current closure state:
|
|||||||
|
|
||||||
- [x] event-bus process listeners
|
- [x] event-bus process listeners
|
||||||
- [x] installable `BacktestProcessMod` extension hook
|
- [x] installable `BacktestProcessMod` extension hook
|
||||||
- [ ] full RQAlpha-style global mod loader and plugin lifecycle
|
- [x] `BacktestProcessModLoader` with enabled-name installation
|
||||||
|
- [ ] Python RQAlpha-style global mod loader, intentionally out of scope unless
|
||||||
|
we need Python plugin compatibility
|
||||||
|
|
||||||
## Execution Order
|
## Execution Order
|
||||||
|
|
||||||
@@ -199,7 +202,7 @@ Parity gaps found by this pass and current closure state:
|
|||||||
## Current Step
|
## Current Step
|
||||||
|
|
||||||
Active implementation target: P0-P2 parity items are implemented in the engine
|
Active implementation target: P0-P2 parity items are implemented in the engine
|
||||||
core, and P3 now has a lightweight event-driven extension hook. Remaining
|
core, and P3 now has a lightweight event-driven extension loader. Remaining
|
||||||
future work should be driven by concrete production strategy or UI requirements,
|
future work should be driven by concrete production strategy or UI requirements,
|
||||||
especially for data-dependent futures depth matching and exchange-specific
|
especially exchange-specific validators and optional vendor-specific depth
|
||||||
validators.
|
fields.
|
||||||
|
|||||||
Reference in New Issue
Block a user