Add futures depth matching and mod loader
This commit is contained in:
@@ -261,6 +261,43 @@ pub struct IntradayExecutionQuote {
|
||||
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 {
|
||||
pub fn buy_price(&self) -> Option<f64> {
|
||||
if self.ask1.is_finite() && self.ask1 > 0.0 {
|
||||
@@ -661,6 +698,7 @@ pub struct DataSet {
|
||||
candidate_index: HashMap<(NaiveDate, String), CandidateEligibility>,
|
||||
corporate_actions_by_date: BTreeMap<NaiveDate, Vec<CorporateAction>>,
|
||||
execution_quotes_index: HashMap<(NaiveDate, String), Vec<IntradayExecutionQuote>>,
|
||||
order_book_depth_index: HashMap<(NaiveDate, String), Vec<IntradayOrderBookDepthLevel>>,
|
||||
benchmark_by_date: BTreeMap<NaiveDate, BenchmarkSnapshot>,
|
||||
market_series_by_symbol: HashMap<String, SymbolPriceSeries>,
|
||||
benchmark_series_cache: BenchmarkPriceSeries,
|
||||
@@ -694,7 +732,13 @@ impl DataSet {
|
||||
} else {
|
||||
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,
|
||||
market,
|
||||
factors,
|
||||
@@ -703,6 +747,7 @@ impl DataSet {
|
||||
corporate_actions,
|
||||
execution_quotes,
|
||||
futures_params,
|
||||
order_book_depth,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -730,7 +775,13 @@ impl DataSet {
|
||||
} else {
|
||||
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,
|
||||
market,
|
||||
factors,
|
||||
@@ -739,6 +790,7 @@ impl DataSet {
|
||||
corporate_actions,
|
||||
execution_quotes,
|
||||
futures_params,
|
||||
order_book_depth,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -809,6 +861,30 @@ impl DataSet {
|
||||
corporate_actions: Vec<CorporateAction>,
|
||||
execution_quotes: Vec<IntradayExecutionQuote>,
|
||||
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> {
|
||||
let benchmark_code = collect_benchmark_code(&benchmarks)?;
|
||||
let calendar = TradingCalendar::new(benchmarks.iter().map(|item| item.date).collect());
|
||||
@@ -837,6 +913,7 @@ impl DataSet {
|
||||
.collect::<HashMap<_, _>>();
|
||||
let corporate_actions_by_date = group_by_date(corporate_actions, |item| item.date);
|
||||
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
|
||||
.into_iter()
|
||||
@@ -860,6 +937,7 @@ impl DataSet {
|
||||
candidate_index,
|
||||
corporate_actions_by_date,
|
||||
execution_quotes_index,
|
||||
order_book_depth_index,
|
||||
benchmark_by_date,
|
||||
market_series_by_symbol,
|
||||
benchmark_series_cache,
|
||||
@@ -936,6 +1014,17 @@ impl DataSet {
|
||||
.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> {
|
||||
let mut quotes = self
|
||||
.execution_quotes_index
|
||||
@@ -1978,6 +2067,27 @@ fn read_execution_quotes(path: &Path) -> Result<Vec<IntradayExecutionQuote>, Dat
|
||||
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(
|
||||
path: &Path,
|
||||
) -> Result<Vec<FuturesTradingParameter>, DataSetError> {
|
||||
@@ -2329,6 +2439,28 @@ fn build_execution_quote_index(
|
||||
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(
|
||||
factor_by_date: &BTreeMap<NaiveDate, Vec<DailyFactorSnapshot>>,
|
||||
candidate_index: &HashMap<(NaiveDate, String), CandidateEligibility>,
|
||||
|
||||
@@ -7,7 +7,7 @@ use thiserror::Error;
|
||||
use crate::broker::{BrokerExecutionReport, BrokerSimulator, MatchingType};
|
||||
use crate::cost::CostModel;
|
||||
use crate::data::{BenchmarkSnapshot, DataSet, DataSetError, PriceField};
|
||||
use crate::event_bus::{BacktestProcessMod, ProcessEventBus};
|
||||
use crate::event_bus::{BacktestProcessMod, BacktestProcessModLoader, ProcessEventBus};
|
||||
use crate::events::{
|
||||
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
||||
ProcessEventKind,
|
||||
@@ -401,6 +401,22 @@ impl<S, C, R> BacktestEngine<S, C, R> {
|
||||
{
|
||||
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>
|
||||
@@ -1119,6 +1135,15 @@ where
|
||||
intent: &FuturesOrderIntent,
|
||||
) -> Option<(f64, u32)> {
|
||||
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);
|
||||
for quote in quotes {
|
||||
let price = match self.broker.matching_type() {
|
||||
@@ -1172,6 +1197,63 @@ where
|
||||
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(
|
||||
&self,
|
||||
snapshot: &crate::data::DailyMarketSnapshot,
|
||||
|
||||
@@ -9,6 +9,65 @@ pub trait BacktestProcessMod {
|
||||
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)]
|
||||
pub struct ProcessEventBus {
|
||||
listeners: BTreeMap<ProcessEventKind, Vec<ProcessEventListener>>,
|
||||
@@ -54,6 +113,18 @@ impl ProcessEventBus {
|
||||
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) {
|
||||
if let Some(listeners) = self.listeners.get_mut(&event.kind) {
|
||||
for listener in listeners {
|
||||
|
||||
@@ -22,15 +22,15 @@ pub use cost::{ChinaAShareCostModel, CostModel, TradingCost};
|
||||
pub use data::{
|
||||
BenchmarkSnapshot, CandidateEligibility, CorporateAction, DailyFactorSnapshot,
|
||||
DailyMarketSnapshot, DailySnapshotBundle, DataSet, DataSetError, DividendRecord,
|
||||
EligibleUniverseSnapshot, FactorValue, IntradayExecutionQuote, PriceBar, PriceField,
|
||||
SecuritiesMarginRecord, SplitRecord, YieldCurvePoint,
|
||||
EligibleUniverseSnapshot, FactorValue, IntradayExecutionQuote, IntradayOrderBookDepthLevel,
|
||||
PriceBar, PriceField, SecuritiesMarginRecord, SplitRecord, YieldCurvePoint,
|
||||
};
|
||||
pub use engine::{
|
||||
AnalyzerMonthlyReturnRow, AnalyzerPositionRow, AnalyzerReport, AnalyzerRiskSummary,
|
||||
AnalyzerTradeRow, BacktestConfig, BacktestDayProgress, BacktestEngine, BacktestError,
|
||||
BacktestResult, DailyEquityPoint,
|
||||
};
|
||||
pub use event_bus::{BacktestProcessMod, ProcessEventBus};
|
||||
pub use event_bus::{BacktestProcessMod, BacktestProcessModLoader, ProcessEventBus};
|
||||
pub use events::{
|
||||
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
||||
ProcessEventKind,
|
||||
|
||||
@@ -120,7 +120,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
},
|
||||
ManualSection {
|
||||
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 {
|
||||
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(),
|
||||
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![
|
||||
ManualExample {
|
||||
|
||||
Reference in New Issue
Block a user