diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 8e5900e..6ce62f9 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hftbacktest" -version = "0.1.5" +version = "0.2.0" edition = "2021" authors = ["nkaz001 "] license = "MIT" diff --git a/rust/examples/algo.rs b/rust/examples/algo.rs index 262acd6..5a40462 100644 --- a/rust/examples/algo.rs +++ b/rust/examples/algo.rs @@ -14,8 +14,8 @@ pub fn gridtrading( ) -> Result<(), i64> where MD: MarketDepth, - I: Interface + BotTypedDepth, - ::Error: Debug, + I: Bot + BotTypedDepth, + ::Error: Debug, R: Recorder, ::Error: Debug, { diff --git a/rust/examples/gridtrading_backtest.rs b/rust/examples/gridtrading_backtest.rs index 0029bbd..b0f7263 100644 --- a/rust/examples/gridtrading_backtest.rs +++ b/rust/examples/gridtrading_backtest.rs @@ -12,7 +12,7 @@ use hftbacktest::{ ExchangeKind, MultiAssetMultiExchangeBacktest, }, - prelude::{ApplySnapshot, HashMapMarketDepth, Interface}, + prelude::{ApplySnapshot, HashMapMarketDepth, Bot}, }; mod algo; diff --git a/rust/examples/gridtrading_backtest_args.rs b/rust/examples/gridtrading_backtest_args.rs index ccbe547..9359025 100644 --- a/rust/examples/gridtrading_backtest_args.rs +++ b/rust/examples/gridtrading_backtest_args.rs @@ -12,7 +12,7 @@ use hftbacktest::{ reader::read_npz, recorder::BacktestRecorder, }, - prelude::{ApplySnapshot, HashMapMarketDepth, Interface}, + prelude::{ApplySnapshot, HashMapMarketDepth, Bot}, }; mod algo; diff --git a/rust/examples/gridtrading_live.rs b/rust/examples/gridtrading_live.rs index 64fefa7..65b480b 100644 --- a/rust/examples/gridtrading_live.rs +++ b/rust/examples/gridtrading_live.rs @@ -1,8 +1,8 @@ use algo::gridtrading; use hftbacktest::{ connector::binancefutures::{BinanceFutures, Endpoint}, - live::{Bot, LoggingRecorder}, - prelude::{HashMapMarketDepth, Interface}, + live::{LiveBot, LoggingRecorder}, + prelude::{HashMapMarketDepth, Bot}, }; mod algo; @@ -11,7 +11,7 @@ const ORDER_PREFIX: &str = "prefix"; const API_KEY: &str = "apikey"; const SECRET: &str = "secret"; -fn prepare_live() -> Bot { +fn prepare_live() -> LiveBot { let binance_futures = BinanceFutures::builder() .endpoint(Endpoint::Testnet) .api_key(API_KEY) @@ -20,7 +20,7 @@ fn prepare_live() -> Bot { .build() .unwrap(); - let mut hbt = Bot::builder() + let mut hbt = LiveBot::builder() .register("binancefutures", binance_futures) .add("binancefutures", "1000SHIBUSDT", 0.000001, 1.0) .depth(|asset| HashMapMarketDepth::new(asset.tick_size, asset.lot_size)) diff --git a/rust/examples/live_order_error_handling.rs b/rust/examples/live_order_error_handling.rs index 1be1f32..5bea03e 100644 --- a/rust/examples/live_order_error_handling.rs +++ b/rust/examples/live_order_error_handling.rs @@ -2,8 +2,8 @@ use algo::gridtrading; use chrono::Utc; use hftbacktest::{ connector::binancefutures::{BinanceFutures, BinanceFuturesError, Endpoint}, - live::{Bot, BotError, LoggingRecorder}, - prelude::{ErrorKind, HashMapMarketDepth, Interface}, + live::{LiveBot, BotError, LoggingRecorder}, + prelude::{ErrorKind, HashMapMarketDepth, Bot}, }; use tracing::{error, info}; @@ -13,7 +13,7 @@ const ORDER_PREFIX: &str = "prefix"; const API_KEY: &str = "apikey"; const SECRET: &str = "secret"; -fn prepare_live() -> Bot { +fn prepare_live() -> LiveBot { let binance_futures = BinanceFutures::builder() .endpoint(Endpoint::Testnet) .api_key(API_KEY) @@ -22,7 +22,7 @@ fn prepare_live() -> Bot { .build() .unwrap(); - let mut hbt = Bot::builder() + let mut hbt = LiveBot::builder() .register("binancefutures", binance_futures) .add("binancefutures", "SOLUSDT", 0.001, 1.0) .error_handler(|error| { diff --git a/rust/examples/logging_order_latency.rs b/rust/examples/logging_order_latency.rs index be0060d..c84f503 100644 --- a/rust/examples/logging_order_latency.rs +++ b/rust/examples/logging_order_latency.rs @@ -2,8 +2,8 @@ use algo::gridtrading; use chrono::Utc; use hftbacktest::{ connector::binancefutures::{BinanceFutures, Endpoint}, - live::{Bot, LoggingRecorder}, - prelude::{HashMapMarketDepth, Interface, Status}, + live::{LiveBot, LoggingRecorder}, + prelude::{HashMapMarketDepth, Bot, Status}, }; use tracing::info; @@ -13,7 +13,7 @@ const ORDER_PREFIX: &str = "prefix"; const API_KEY: &str = "apikey"; const SECRET: &str = "secret"; -fn prepare_live() -> Bot { +fn prepare_live() -> LiveBot { let binance_futures = BinanceFutures::builder() .endpoint(Endpoint::Public) .api_key(API_KEY) @@ -22,7 +22,7 @@ fn prepare_live() -> Bot { .build() .unwrap(); - let mut hbt = Bot::builder() + let mut hbt = LiveBot::builder() .register("binancefutures", binance_futures) .add("binancefutures", "SOLUSDT", 0.001, 1.0) .order_recv_hook(|req, resp| { diff --git a/rust/src/backtest/backtest.rs b/rust/src/backtest/backtest.rs index 6a1a2e0..9673f13 100644 --- a/rust/src/backtest/backtest.rs +++ b/rust/src/backtest/backtest.rs @@ -13,7 +13,7 @@ use crate::{ BotTypedTrade, BuildError, Event, - Interface, + Bot, OrdType, Order, Side, @@ -205,7 +205,7 @@ where } } -impl Interface for MultiAssetMultiExchangeBacktest +impl Bot for MultiAssetMultiExchangeBacktest where MD: MarketDepth, { @@ -718,7 +718,7 @@ where } } -impl Interface for MultiAssetSingleExchangeBacktest +impl Bot for MultiAssetSingleExchangeBacktest where MD: MarketDepth, Local: LocalProcessor, diff --git a/rust/src/backtest/l3backtest.rs b/rust/src/backtest/l3backtest.rs new file mode 100644 index 0000000..d40b815 --- /dev/null +++ b/rust/src/backtest/l3backtest.rs @@ -0,0 +1,491 @@ +use std::any::Any; +use std::collections::HashMap; +use std::marker::PhantomData; + +use crate::backtest::BacktestError; +use crate::backtest::evs::{EventSet, EventType}; +use crate::backtest::proc::{GenLocalProcessor, LocalProcessor, Processor}; +use crate::depth::{L3MarketDepth, MarketDepth}; +use crate::prelude::{BotTypedDepth, BotTypedTrade, Bot, Order, OrderRequest, OrdType, Side, StateValues, TimeInForce, UNTIL_END_OF_DATA, WAIT_ORDER_RESPONSE_NONE}; +use crate::types::L3Event; + +pub struct L3MultiAssetSingleExchangeBacktest + where + MD: L3MarketDepth, + Local: LocalProcessor, + Exchange: Processor +{ + cur_ts: i64, + evs: EventSet, + local: Vec, + exch: Vec, + _md_marker: PhantomData, +} + +impl L3MultiAssetSingleExchangeBacktest + where + MD: L3MarketDepth, + Local: LocalProcessor, + Exchange: Processor +{ + pub fn new(local: Vec, exch: Vec) -> Self { + let num_assets = local.len(); + if local.len() != num_assets || exch.len() != num_assets { + panic!(); + } + Self { + cur_ts: i64::MAX, + evs: EventSet::new(num_assets), + local, + exch, + _md_marker: Default::default(), + } + } + + fn initialize_evs(&mut self) -> Result<(), BacktestError> { + for (asset_no, local) in self.local.iter_mut().enumerate() { + match local.initialize_data() { + Ok(ts) => self.evs.update_local_data(asset_no, ts), + Err(BacktestError::EndOfData) => { + self.evs.invalidate_local_data(asset_no); + } + Err(e) => { + return Err(e); + } + } + } + for (asset_no, exch) in self.exch.iter_mut().enumerate() { + match exch.initialize_data() { + Ok(ts) => self.evs.update_exch_data(asset_no, ts), + Err(BacktestError::EndOfData) => { + self.evs.invalidate_exch_data(asset_no); + } + Err(e) => { + return Err(e); + } + } + } + Ok(()) + } + + pub fn goto( + &mut self, + timestamp: i64, + wait_order_response: (usize, i64), + ) -> Result { + let mut timestamp = timestamp; + loop { + match self.evs.next() { + Some(ev) => { + if ev.timestamp > timestamp { + self.cur_ts = timestamp; + return Ok(true); + } + match ev.ty { + EventType::LocalData => { + let local = unsafe { self.local.get_unchecked_mut(ev.asset_no) }; + match local.process_data() { + Ok((next_ts, _)) => { + self.evs.update_local_data(ev.asset_no, next_ts); + } + Err(BacktestError::EndOfData) => { + self.evs.invalidate_local_data(ev.asset_no); + } + Err(e) => { + return Err(e); + } + } + } + EventType::LocalOrder => { + let local = unsafe { self.local.get_unchecked_mut(ev.asset_no) }; + let wait_order_resp_id = if ev.asset_no == wait_order_response.0 { + wait_order_response.1 + } else { + WAIT_ORDER_RESPONSE_NONE + }; + if local.process_recv_order(ev.timestamp, wait_order_resp_id)? { + timestamp = ev.timestamp; + } + self.evs.update_local_order( + ev.asset_no, + local.earliest_recv_order_timestamp(), + ); + } + EventType::ExchData => { + let exch = unsafe { self.exch.get_unchecked_mut(ev.asset_no) }; + match exch.process_data() { + Ok((next_ts, _)) => { + self.evs.update_exch_data(ev.asset_no, next_ts); + } + Err(BacktestError::EndOfData) => { + self.evs.invalidate_exch_data(ev.asset_no); + } + Err(e) => { + return Err(e); + } + } + self.evs.update_local_order( + ev.asset_no, + exch.earliest_send_order_timestamp(), + ); + } + EventType::ExchOrder => { + let exch = unsafe { self.exch.get_unchecked_mut(ev.asset_no) }; + let _ = + exch.process_recv_order(ev.timestamp, WAIT_ORDER_RESPONSE_NONE)?; + self.evs.update_exch_order( + ev.asset_no, + exch.earliest_recv_order_timestamp(), + ); + } + } + } + None => { + return Ok(false); + } + } + } + } +} + +impl Bot for L3MultiAssetSingleExchangeBacktest + where + MD: L3MarketDepth, + Local: LocalProcessor, + Exchange: Processor +{ + type Error = BacktestError; + + #[inline] + fn current_timestamp(&self) -> i64 { + self.cur_ts + } + + #[inline] + fn num_assets(&self) -> usize { + self.local.len() + } + + #[inline] + fn position(&self, asset_no: usize) -> f64 { + self.local.get(asset_no).unwrap().position() + } + + #[inline] + fn state_values(&self, asset_no: usize) -> StateValues { + self.local.get(asset_no).unwrap().state_values() + } + + fn depth(&self, asset_no: usize) -> &dyn MarketDepth { + self.local.get(asset_no).unwrap().depth() + } + + fn trade(&self, asset_no: usize) -> Vec<&dyn Any> { + self.local + .get(asset_no) + .unwrap() + .trade() + .iter() + .map(|ev| ev as &dyn Any) + .collect() + } + + #[inline] + fn clear_last_trades(&mut self, asset_no: Option) { + match asset_no { + Some(an) => { + let local = self.local.get_mut(an).unwrap(); + local.clear_last_trades(); + } + None => { + for local in self.local.iter_mut() { + local.clear_last_trades(); + } + } + } + } + + #[inline] + fn orders(&self, asset_no: usize) -> &HashMap { + &self.local.get(asset_no).unwrap().orders() + } + + #[inline] + fn submit_buy_order( + &mut self, + asset_no: usize, + order_id: i64, + price: f32, + qty: f32, + time_in_force: TimeInForce, + order_type: OrdType, + wait: bool, + ) -> Result { + let local = self.local.get_mut(asset_no).unwrap(); + local.submit_order( + order_id, + Side::Buy, + price, + qty, + order_type, + time_in_force, + self.cur_ts, + )?; + self.evs + .update_exch_order(asset_no, local.earliest_send_order_timestamp()); + + if wait { + return self.goto(UNTIL_END_OF_DATA, (asset_no, order_id)); + } + Ok(true) + } + + #[inline] + fn submit_sell_order( + &mut self, + asset_no: usize, + order_id: i64, + price: f32, + qty: f32, + time_in_force: TimeInForce, + order_type: OrdType, + wait: bool, + ) -> Result { + let local = self.local.get_mut(asset_no).unwrap(); + local.submit_order( + order_id, + Side::Sell, + price, + qty, + order_type, + time_in_force, + self.cur_ts, + )?; + self.evs + .update_exch_order(asset_no, local.earliest_send_order_timestamp()); + + if wait { + return self.goto(UNTIL_END_OF_DATA, (asset_no, order_id)); + } + Ok(true) + } + + fn submit_order( + &mut self, + asset_no: usize, + order: OrderRequest, + wait: bool, + ) -> Result { + let local = self.local.get_mut(asset_no).unwrap(); + local.submit_order( + order.order_id, + Side::Sell, + order.price, + order.qty, + order.order_type, + order.time_in_force, + self.cur_ts, + )?; + self.evs + .update_exch_order(asset_no, local.earliest_send_order_timestamp()); + + if wait { + return self.goto(UNTIL_END_OF_DATA, (asset_no, order.order_id)); + } + Ok(true) + } + + #[inline] + fn cancel(&mut self, asset_no: usize, order_id: i64, wait: bool) -> Result { + let local = self.local.get_mut(asset_no).unwrap(); + local.cancel(order_id, self.cur_ts)?; + self.evs + .update_exch_order(asset_no, local.earliest_send_order_timestamp()); + + if wait { + return self.goto(UNTIL_END_OF_DATA, (asset_no, order_id)); + } + Ok(true) + } + + #[inline] + fn clear_inactive_orders(&mut self, asset_no: Option) { + match asset_no { + Some(asset_no) => { + self.local + .get_mut(asset_no) + .unwrap() + .clear_inactive_orders(); + } + None => { + for local in self.local.iter_mut() { + local.clear_inactive_orders(); + } + } + } + } + + #[inline] + fn wait_order_response( + &mut self, + asset_no: usize, + order_id: i64, + timeout: i64, + ) -> Result { + self.goto(self.cur_ts + timeout, (asset_no, order_id)) + } + + fn wait_next_feed( + &mut self, + include_order_resp: bool, + timeout: i64, + ) -> Result { + if self.cur_ts == i64::MAX { + self.initialize_evs()?; + match self.evs.next() { + Some(ev) => { + self.cur_ts = ev.timestamp; + } + None => { + return Ok(false); + } + } + } + let mut timestamp = self.cur_ts + timeout; + loop { + match self.evs.next() { + Some(ev) => { + if ev.timestamp > timestamp { + self.cur_ts = timestamp; + return Ok(true); + } + match ev.ty { + EventType::LocalData => { + let local = unsafe { self.local.get_unchecked_mut(ev.asset_no) }; + match local.process_data() { + Ok((next_ts, _)) => { + self.evs.update_local_data(ev.asset_no, next_ts); + } + Err(BacktestError::EndOfData) => { + self.evs.invalidate_local_data(ev.asset_no); + } + Err(e) => { + return Err(e); + } + } + timestamp = ev.timestamp; + } + EventType::LocalOrder => { + let local = unsafe { self.local.get_unchecked_mut(ev.asset_no) }; + let _ = + local.process_recv_order(ev.timestamp, WAIT_ORDER_RESPONSE_NONE)?; + self.evs.update_local_order( + ev.asset_no, + local.earliest_recv_order_timestamp(), + ); + if include_order_resp { + timestamp = ev.timestamp; + } + } + EventType::ExchData => { + let exch = unsafe { self.exch.get_unchecked_mut(ev.asset_no) }; + match exch.process_data() { + Ok((next_ts, _)) => { + self.evs.update_exch_data(ev.asset_no, next_ts); + } + Err(BacktestError::EndOfData) => { + self.evs.invalidate_exch_data(ev.asset_no); + } + Err(e) => { + return Err(e); + } + } + self.evs.update_local_order( + ev.asset_no, + exch.earliest_send_order_timestamp(), + ); + } + EventType::ExchOrder => { + let exch = unsafe { self.exch.get_unchecked_mut(ev.asset_no) }; + let _ = + exch.process_recv_order(ev.timestamp, WAIT_ORDER_RESPONSE_NONE)?; + self.evs.update_exch_order( + ev.asset_no, + exch.earliest_recv_order_timestamp(), + ); + } + } + } + None => { + return Ok(false); + } + } + } + } + + #[inline] + fn elapse(&mut self, duration: i64) -> Result { + if self.cur_ts == i64::MAX { + self.initialize_evs()?; + match self.evs.next() { + Some(ev) => { + self.cur_ts = ev.timestamp; + } + None => { + return Ok(false); + } + } + } + self.goto(self.cur_ts + duration, (0, WAIT_ORDER_RESPONSE_NONE)) + } + + #[inline] + fn elapse_bt(&mut self, duration: i64) -> Result { + self.elapse(duration) + } + + #[inline] + fn close(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + + #[inline] + fn feed_latency(&self, asset_no: usize) -> Option<(i64, i64)> { + self.local.get(asset_no).unwrap().feed_latency() + } + + #[inline] + fn order_latency(&self, asset_no: usize) -> Option<(i64, i64, i64)> { + self.local.get(asset_no).unwrap().order_latency() + } +} + +impl BotTypedDepth for L3MultiAssetSingleExchangeBacktest + where + MD: L3MarketDepth, + Local: LocalProcessor, + Exchange: Processor, +{ + #[inline] + fn depth_typed(&self, asset_no: usize) -> &MD { + &self.local.get(asset_no).unwrap().depth() + } +} + +impl BotTypedTrade for L3MultiAssetSingleExchangeBacktest + where + MD: L3MarketDepth, + Local: LocalProcessor, + Exchange: Processor, +{ + #[inline] + fn trade_typed(&self, asset_no: usize) -> &Vec { + let local = self.local.get(asset_no).unwrap(); + local.trade() + } +} + +pub struct Backtest { + local: Vec>, + exch: Vec>, +} \ No newline at end of file diff --git a/rust/src/backtest/mod.rs b/rust/src/backtest/mod.rs index 0213d1a..983725e 100644 --- a/rust/src/backtest/mod.rs +++ b/rust/src/backtest/mod.rs @@ -15,6 +15,7 @@ use crate::{ depth::MarketDepth, types::{BuildError, Event}, }; +use crate::depth::L2MarketDepth; /// Provides asset types. pub mod assettype; @@ -85,7 +86,7 @@ impl Asset { pub fn builder() -> AssetBuilder where AT: AssetType + Clone + 'static, - MD: MarketDepth + 'static, + MD: MarketDepth + L2MarketDepth + 'static, QM: QueueModel + 'static, LM: LatencyModel + Clone + 'static, { @@ -115,7 +116,7 @@ pub struct AssetBuilder { impl AssetBuilder where AT: AssetType + Clone + 'static, - MD: MarketDepth + 'static, + MD: MarketDepth + L2MarketDepth + 'static, QM: QueueModel + 'static, LM: LatencyModel + Clone + 'static, { diff --git a/rust/src/backtest/models/latencies.rs b/rust/src/backtest/models/latencies.rs index f5bf112..6d4f35a 100644 --- a/rust/src/backtest/models/latencies.rs +++ b/rust/src/backtest/models/latencies.rs @@ -34,7 +34,7 @@ impl ConstantLatency { /// /// `entry_latency` and `response_latency` should match the time unit of the data's timestamps. /// Using nanoseconds across all datasets is recommended, since the live - /// [Bot](crate::live::Bot) uses nanoseconds. + /// [Bot](crate::live::LiveBot) uses nanoseconds. pub fn new(entry_latency: i64, response_latency: i64) -> Self { Self { entry_latency, diff --git a/rust/src/backtest/proc/l3_local.rs b/rust/src/backtest/proc/l3_local.rs index f331fa0..700bb2f 100644 --- a/rust/src/backtest/proc/l3_local.rs +++ b/rust/src/backtest/proc/l3_local.rs @@ -255,9 +255,9 @@ where let ev = &self.data[self.row_num]; // Processes a depth event if ev.is(LOCAL_BID_DEPTH_CLEAR_EVENT) { - self.depth.clear_depth(BUY, f32::NEG_INFINITY); + self.depth.clear_depth(BUY); } else if ev.is(LOCAL_ASK_DEPTH_CLEAR_EVENT) { - self.depth.clear_depth(SELL, f32::INFINITY); + self.depth.clear_depth(SELL); } else if ev.is(LOCAL_BID_ADD_ORDER_EVENT) { self.depth .add_buy_order(ev.order_id, ev.px, ev.qty, ev.local_ts)?; diff --git a/rust/src/backtest/proc/l3_nopartialfillexchange.rs b/rust/src/backtest/proc/l3_nopartialfillexchange.rs index faa65bd..158ec12 100644 --- a/rust/src/backtest/proc/l3_nopartialfillexchange.rs +++ b/rust/src/backtest/proc/l3_nopartialfillexchange.rs @@ -373,9 +373,9 @@ where fn process_data(&mut self) -> Result<(i64, i64), BacktestError> { let row_num = self.row_num; if self.data[row_num].is(EXCH_BID_DEPTH_CLEAR_EVENT) { - self.depth.clear_depth(BUY, f32::NEG_INFINITY); + self.depth.clear_depth(BUY); } else if self.data[row_num].is(EXCH_ASK_DEPTH_CLEAR_EVENT) { - self.depth.clear_depth(SELL, f32::INFINITY); + self.depth.clear_depth(SELL); } else if self.data[row_num].is(EXCH_BID_ADD_ORDER_EVENT) { let (prev_best_bid_tick, best_bid_tick) = self.depth.add_buy_order( self.data[row_num].order_id, @@ -421,7 +421,7 @@ where } } } else if self.data[row_num].is(EXCH_CANCEL_ORDER_EVENT) { - self.depth + let _ = self.depth .delete_order(self.data[row_num].order_id, self.data[row_num].exch_ts)?; self.queue_model .cancel_order(L3OrderId::Market(self.data[row_num].order_id))?; diff --git a/rust/src/backtest/proc/local.rs b/rust/src/backtest/proc/local.rs index e572a02..e2f37fe 100644 --- a/rust/src/backtest/proc/local.rs +++ b/rust/src/backtest/proc/local.rs @@ -36,6 +36,7 @@ use crate::{ WAIT_ORDER_RESPONSE_ANY, }, }; +use crate::depth::L2MarketDepth; /// The local model. pub struct Local @@ -111,7 +112,7 @@ impl LocalProcessor for Local where AT: AssetType, LM: LatencyModel, - MD: MarketDepth, + MD: MarketDepth + L2MarketDepth, { fn submit_order( &mut self, @@ -235,7 +236,7 @@ impl Processor for Local where AT: AssetType, LM: LatencyModel, - MD: MarketDepth, + MD: MarketDepth + L2MarketDepth, { fn initialize_data(&mut self) -> Result { self.data = self.reader.next()?; diff --git a/rust/src/backtest/proc/nopartialfillexchange.rs b/rust/src/backtest/proc/nopartialfillexchange.rs index 2de38c1..1580c14 100644 --- a/rust/src/backtest/proc/nopartialfillexchange.rs +++ b/rust/src/backtest/proc/nopartialfillexchange.rs @@ -35,6 +35,7 @@ use crate::{ SELL, }, }; +use crate::depth::L2MarketDepth; /// The exchange model without partial fills. /// @@ -593,7 +594,7 @@ where AT: AssetType, LM: LatencyModel, QM: QueueModel, - MD: MarketDepth, + MD: MarketDepth + L2MarketDepth, { fn initialize_data(&mut self) -> Result { self.data = self.reader.next()?; diff --git a/rust/src/backtest/proc/partialfillexchange.rs b/rust/src/backtest/proc/partialfillexchange.rs index 78c94b1..41c988b 100644 --- a/rust/src/backtest/proc/partialfillexchange.rs +++ b/rust/src/backtest/proc/partialfillexchange.rs @@ -35,6 +35,7 @@ use crate::{ SELL, }, }; +use crate::depth::L2MarketDepth; /// The exchange model with partial fills. /// @@ -777,7 +778,7 @@ where AT: AssetType, LM: LatencyModel, QM: QueueModel, - MD: MarketDepth, + MD: MarketDepth + L2MarketDepth, { fn initialize_data(&mut self) -> Result { self.data = self.reader.next()?; diff --git a/rust/src/backtest/recorder.rs b/rust/src/backtest/recorder.rs index 1163de8..841f130 100644 --- a/rust/src/backtest/recorder.rs +++ b/rust/src/backtest/recorder.rs @@ -6,7 +6,7 @@ use std::{ use crate::{ depth::MarketDepth, - types::{BotTypedDepth, Interface, Recorder}, + types::{BotTypedDepth, Bot, Recorder}, }; /// Provides recording of the backtesting strategy's state values, which are needed to compute @@ -20,7 +20,7 @@ impl Recorder for BacktestRecorder { fn record(&mut self, hbt: &mut I) -> Result<(), Self::Error> where - I: Interface + BotTypedDepth, + I: Bot + BotTypedDepth, MD: MarketDepth, { let timestamp = hbt.current_timestamp(); @@ -48,7 +48,7 @@ impl BacktestRecorder { /// Constructs an instance of `BacktestRecorder`. pub fn new(hbt: &I) -> Self where - I: Interface, + I: Bot, { Self { values: { diff --git a/rust/src/depth/btreemarketdepth.rs b/rust/src/depth/btreemarketdepth.rs index c69442d..8d2f74f 100644 --- a/rust/src/depth/btreemarketdepth.rs +++ b/rust/src/depth/btreemarketdepth.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use super::{ApplySnapshot, MarketDepth, INVALID_MAX, INVALID_MIN}; +use super::{ApplySnapshot, MarketDepth, INVALID_MAX, INVALID_MIN, L2MarketDepth}; use crate::{ backtest::reader::Data, types::{Event, BUY, SELL}, @@ -33,7 +33,7 @@ impl BTreeMarketDepth { } } -impl MarketDepth for BTreeMarketDepth { +impl L2MarketDepth for BTreeMarketDepth { fn update_bid_depth( &mut self, price: f32, @@ -111,7 +111,9 @@ impl MarketDepth for BTreeMarketDepth { self.ask_depth.clear(); } } +} +impl MarketDepth for BTreeMarketDepth { #[inline(always)] fn best_bid(&self) -> f32 { self.best_bid_tick() as f32 * self.tick_size @@ -153,7 +155,7 @@ impl MarketDepth for BTreeMarketDepth { } } -impl ApplySnapshot for BTreeMarketDepth { +impl ApplySnapshot for BTreeMarketDepth { fn apply_snapshot(&mut self, data: &Data) { self.bid_depth.clear(); self.ask_depth.clear(); diff --git a/rust/src/depth/hashmapmarketdepth.rs b/rust/src/depth/hashmapmarketdepth.rs index b33c994..d7bd4af 100644 --- a/rust/src/depth/hashmapmarketdepth.rs +++ b/rust/src/depth/hashmapmarketdepth.rs @@ -5,6 +5,7 @@ use crate::{ backtest::reader::Data, types::{Event, BUY, SELL}, }; +use crate::prelude::L2MarketDepth; /// L2 Market depth implementation based on a hash map. /// @@ -65,7 +66,7 @@ impl HashMapMarketDepth { } } -impl MarketDepth for HashMapMarketDepth { +impl L2MarketDepth for HashMapMarketDepth { fn update_bid_depth( &mut self, price: f32, @@ -211,7 +212,9 @@ impl MarketDepth for HashMapMarketDepth { self.high_ask_tick = INVALID_MIN; } } +} +impl MarketDepth for HashMapMarketDepth { #[inline(always)] fn best_bid(&self) -> f32 { self.best_bid_tick as f32 * self.tick_size @@ -253,7 +256,7 @@ impl MarketDepth for HashMapMarketDepth { } } -impl ApplySnapshot for HashMapMarketDepth { +impl ApplySnapshot for HashMapMarketDepth { fn apply_snapshot(&mut self, data: &Data) { self.best_bid_tick = INVALID_MIN; self.best_ask_tick = INVALID_MAX; diff --git a/rust/src/depth/l3mbomarketdepth.rs b/rust/src/depth/l3mbomarketdepth.rs index 5ade596..4011b0f 100644 --- a/rust/src/depth/l3mbomarketdepth.rs +++ b/rust/src/depth/l3mbomarketdepth.rs @@ -2,30 +2,23 @@ use std::collections::{hash_map::Entry, BTreeMap, HashMap}; use crate::{ backtest::BacktestError, - depth::{L3MarketDepth, MarketDepth, INVALID_MAX, INVALID_MIN}, + depth::{L3MarketDepth, MarketDepth, L3Order, INVALID_MAX, INVALID_MIN}, types::{Side, BUY, SELL}, }; -pub struct MarketOrder { - order_id: i64, - side: Side, - price_tick: i32, - qty: f32, -} - pub struct L3MBOMarketDepth { pub tick_size: f32, pub lot_size: f32, pub timestamp: i64, pub bid_depth: BTreeMap, pub ask_depth: BTreeMap, - pub orders: HashMap, + pub orders: HashMap, pub best_bid_tick: i32, pub best_ask_tick: i32, } impl L3MBOMarketDepth { - pub fn add(&mut self, order: MarketOrder) -> Result<(), BacktestError> { + pub fn add(&mut self, order: L3Order) -> Result<(), BacktestError> { if order.side == Side::Buy { *self.bid_depth.entry(order.price_tick).or_insert(0.0) += order.qty; } else { @@ -52,7 +45,7 @@ impl L3MarketDepth for L3MBOMarketDepth { timestamp: i64, ) -> Result<(i32, i32), Self::Error> { let price_tick = (px / self.tick_size).round() as i32; - self.add(MarketOrder { + self.add(L3Order { order_id, side: Side::Buy, price_tick, @@ -73,7 +66,7 @@ impl L3MarketDepth for L3MBOMarketDepth { timestamp: i64, ) -> Result<(i32, i32), Self::Error> { let price_tick = (px / self.tick_size).round() as i32; - self.add(MarketOrder { + self.add(L3Order { order_id, side: Side::Sell, price_tick, @@ -86,25 +79,36 @@ impl L3MarketDepth for L3MBOMarketDepth { Ok((prev_best_tick, self.best_ask_tick)) } - fn delete_order(&mut self, order_id: i64, timestamp: i64) -> Result<(), Self::Error> { + fn delete_order(&mut self, order_id: i64, timestamp: i64) -> Result<(i64, i32, i32), Self::Error> { let order = self .orders .remove(&order_id) .ok_or(BacktestError::OrderNotFound)?; if order.side == Side::Buy { + let prev_best_tick = self.best_bid_tick; + let depth_qty = self.bid_depth.get_mut(&order.price_tick).unwrap(); *depth_qty -= order.qty; - if (*depth_qty / self.lot_size as f32).round() as i32 == 0 { + if (*depth_qty / self.lot_size).round() as i32 == 0 { self.bid_depth.remove(&order.price_tick).unwrap(); + if order.price_tick == self.best_bid_tick { + self.best_bid_tick = *self.bid_depth.keys().next().unwrap_or(&INVALID_MIN); + } } + Ok((SELL, prev_best_tick, self.best_bid_tick)) } else { + let prev_best_tick = self.best_ask_tick; + let depth_qty = self.ask_depth.get_mut(&order.price_tick).unwrap(); *depth_qty -= order.qty; - if (*depth_qty / self.lot_size as f32).round() as i32 == 0 { + if (*depth_qty / self.lot_size).round() as i32 == 0 { self.ask_depth.remove(&order.price_tick).unwrap(); + if order.price_tick == self.best_ask_tick { + self.best_ask_tick = *self.ask_depth.keys().next().unwrap_or(&INVALID_MAX); + } } + Ok((SELL, prev_best_tick, self.best_ask_tick)) } - Ok(()) } fn modify_order( @@ -170,35 +174,24 @@ impl L3MarketDepth for L3MBOMarketDepth { } } } -} -impl MarketDepth for L3MBOMarketDepth { - fn update_bid_depth( - &mut self, - price: f32, - qty: f32, - timestamp: i64, - ) -> (i32, i32, i32, f32, f32, i64) { - todo!() - } - - fn update_ask_depth( - &mut self, - price: f32, - qty: f32, - timestamp: i64, - ) -> (i32, i32, i32, f32, f32, i64) { - todo!() - } - - fn clear_depth(&mut self, side: i64, clear_upto_price: f32) { + fn clear_depth(&mut self, side: i64) { if side == BUY { self.bid_depth.clear(); + } else if side == SELL { + self.ask_depth.clear(); } else { + self.bid_depth.clear(); self.ask_depth.clear(); } } + fn orders(&self) -> &HashMap { + &self.orders + } +} + +impl MarketDepth for L3MBOMarketDepth { #[inline(always)] fn best_bid(&self) -> f32 { self.best_bid_tick() as f32 * self.tick_size diff --git a/rust/src/depth/mod.rs b/rust/src/depth/mod.rs index 757e3c1..bfa15e8 100644 --- a/rust/src/depth/mod.rs +++ b/rust/src/depth/mod.rs @@ -1,7 +1,10 @@ +use std::collections::HashMap; + pub use btreemarketdepth::BTreeMarketDepth; pub use hashmapmarketdepth::HashMapMarketDepth; -use crate::{backtest::reader::Data, types::Event}; +use crate::backtest::reader::Data; +use crate::prelude::Side; mod btreemarketdepth; mod hashmapmarketdepth; @@ -17,6 +20,33 @@ pub const INVALID_MAX: i32 = i32::MAX; /// Provides MarketDepth interface. pub trait MarketDepth { + /// Returns the best bid price. + fn best_bid(&self) -> f32; + + /// Returns the best ask price. + fn best_ask(&self) -> f32; + + /// Returns the best bid price in ticks. + fn best_bid_tick(&self) -> i32; + + /// Returns the best ask price in ticks. + fn best_ask_tick(&self) -> i32; + + /// Returns the tick size. + fn tick_size(&self) -> f32; + + /// Returns the lot size. + fn lot_size(&self) -> f32; + + /// Returns the quantity at the bid market depth for a given price in ticks. + fn bid_qty_at_tick(&self, price_tick: i32) -> f32; + + /// Returns the quantity at the ask market depth for a given price in ticks. + fn ask_qty_at_tick(&self, price_tick: i32) -> f32; +} + +/// Provides Level2-specific market depth functions. +pub trait L2MarketDepth { /// Updates the bid-side market depth and returns a tuple containing (the price in ticks, /// the previous best bid price in ticks, the current best bid price in ticks, the previous /// quantity at the price, the current quantity at the price, and the timestamp). @@ -46,43 +76,31 @@ pub trait MarketDepth { /// Clears the market depth. If the `side` is neither [crate::types::BUY] nor [crate::types::SELL], /// both sides are cleared. In this case, `clear_upto_price` is ignored. fn clear_depth(&mut self, side: i64, clear_upto_price: f32); - - /// Returns the best bid price. - fn best_bid(&self) -> f32; - - /// Returns the best ask price. - fn best_ask(&self) -> f32; - - /// Returns the best bid price in ticks. - fn best_bid_tick(&self) -> i32; - - /// Returns the best ask price in ticks. - fn best_ask_tick(&self) -> i32; - - /// Returns the tick size. - fn tick_size(&self) -> f32; - - /// Returns the lot size. - fn lot_size(&self) -> f32; - - /// Returns the quantity at the bid market depth for a given price in ticks. - fn bid_qty_at_tick(&self, price_tick: i32) -> f32; - - /// Returns the quantity at the ask market depth for a given price in ticks. - fn ask_qty_at_tick(&self, price_tick: i32) -> f32; } -/// Provides a method to initialize the [`MarketDepth`] from the given snapshot data, such as +/// Provides a method to initialize the `MarketDepth` from the given snapshot data, such as /// Start-Of-Day snapshot or End-Of-Day snapshot, for backtesting purpose. -pub trait ApplySnapshot { +pub trait ApplySnapshot { /// Applies the snapshot from the given data to this market depth. - fn apply_snapshot(&mut self, data: &Data); + fn apply_snapshot(&mut self, data: &Data); +} + +/// Level3 order from the market feed. +#[cfg(feature = "unstable_l3")] +pub struct L3Order { + pub order_id: i64, + pub side: Side, + pub price_tick: i32, + pub qty: f32, } +/// Provides Level3-specific market depth functions. #[cfg(feature = "unstable_l3")] pub trait L3MarketDepth : MarketDepth { type Error; + /// Adds a buy order to the order book and returns a tuple containing (the previous best bid + /// in ticks, the current best bid in ticks). fn add_buy_order( &mut self, order_id: i64, @@ -91,6 +109,8 @@ pub trait L3MarketDepth : MarketDepth { timestamp: i64, ) -> Result<(i32, i32), Self::Error>; + /// Adds a sell order to the order book and returns a tuple containing (the previous best ask + /// in ticks, the current best ask in ticks). fn add_sell_order( &mut self, order_id: i64, @@ -99,8 +119,15 @@ pub trait L3MarketDepth : MarketDepth { timestamp: i64, ) -> Result<(i32, i32), Self::Error>; - fn delete_order(&mut self, order_id: i64, timestamp: i64) -> Result<(), Self::Error>; + /// Deletes the order in the order book. + fn delete_order( + &mut self, + order_id: i64, + timestamp: i64 + ) -> Result<(i64, i32, i32), Self::Error>; + /// Modifies the order in the order book and returns a tuple containing (side, the previous best + /// in ticks, the current best in ticks). fn modify_order( &mut self, order_id: i64, @@ -108,4 +135,11 @@ pub trait L3MarketDepth : MarketDepth { qty: f32, timestamp: i64, ) -> Result<(i64, i32, i32), Self::Error>; + + /// Clears the market depth. If the `side` is neither [crate::types::BUY] nor + /// [crate::types::SELL], both sides are cleared. + fn clear_depth(&mut self, side: i64); + + /// Returns the orders held in the order book. + fn orders(&self) -> &HashMap; } diff --git a/rust/src/live/bot.rs b/rust/src/live/bot.rs index f341e74..69d19cd 100644 --- a/rust/src/live/bot.rs +++ b/rust/src/live/bot.rs @@ -26,7 +26,7 @@ use crate::{ Error as ErrorEvent, Error, Event, - Interface, + Bot, LiveEvent, OrdType, Order, @@ -41,6 +41,7 @@ use crate::{ WAIT_ORDER_RESPONSE_NONE, }, }; +use crate::depth::L2MarketDepth; #[derive(Error, Eq, PartialEq, Clone, Debug)] pub enum BotError { @@ -112,7 +113,7 @@ async fn thread_main( pub type ErrorHandler = Box Result<(), BotError>>; pub type OrderRecvHook = Box Result<(), BotError>>; -/// Live [`Bot`] builder. +/// Live [`LiveBot`] builder. pub struct BotBuilder { conns: HashMap>, assets: Vec<(String, Asset)>, @@ -201,8 +202,8 @@ impl BotBuilder { } } - /// Builds a live [`Bot`] based on the registered connectors and assets. - pub fn build(self) -> Result, BuildError> { + /// Builds a live [`LiveBot`] based on the registered connectors and assets. + pub fn build(self) -> Result, BuildError> { let mut dup = HashSet::new(); let mut conns = self.conns; for (asset_no, (name, asset_info)) in self.assets.iter().enumerate() { @@ -241,7 +242,7 @@ impl BotBuilder { let last_feed_latency = self.assets.iter().map(|_| None).collect(); let last_order_latency = self.assets.iter().map(|_| None).collect(); - Ok(Bot { + Ok(LiveBot { ev_tx: Some(ev_tx), ev_rx, req_rx: Some(req_rx), @@ -265,16 +266,16 @@ impl BotBuilder { /// Provides the same interface as the backtesters in [`backtest`](`crate::backtest`). /// /// ``` -/// use hftbacktest::{live::Bot, prelude::HashMapMarketDepth}; +/// use hftbacktest::{live::LiveBot, prelude::HashMapMarketDepth}; /// -/// let mut hbt = Bot::builder() +/// let mut hbt = LiveBot::builder() /// .register("connector_name", connector) /// .add("connector_name", "symbol", tick_size, lot_size) /// .depth(|asset| HashMapMarketDepth::new(asset.tick_size, asset.lot_size)) /// .build() /// .unwrap(); /// ``` -pub struct Bot { +pub struct LiveBot { req_tx: UnboundedSender, req_rx: Option>, ev_tx: Option>, @@ -291,11 +292,11 @@ pub struct Bot { last_order_latency: Vec>, } -impl Bot +impl LiveBot where - MD: MarketDepth, + MD: MarketDepth + L2MarketDepth, { - /// Builder to construct [`Bot`] instances. + /// Builder to construct [`LiveBot`] instances. pub fn builder() -> BotBuilder { BotBuilder { conns: HashMap::new(), @@ -306,7 +307,7 @@ where } } - /// Runs the [`Bot`]. Spawns a thread to run [`Connector`]s and to handle sending [`Request`] + /// Runs the [`LiveBot`]. Spawns a thread to run [`Connector`]s and to handle sending [`Request`] /// to [`Connector`]s without blocking. pub fn run(&mut self) -> Result<(), BotError> { let ev_tx = self.ev_tx.take().unwrap(); @@ -491,9 +492,9 @@ where } } -impl Interface for Bot +impl Bot for LiveBot where - MD: MarketDepth, + MD: MarketDepth + L2MarketDepth, { type Error = BotError; @@ -710,7 +711,7 @@ where } } -impl BotTypedDepth for Bot +impl BotTypedDepth for LiveBot where MD: MarketDepth, { @@ -719,7 +720,7 @@ where } } -impl BotTypedTrade for Bot +impl BotTypedTrade for LiveBot where MD: MarketDepth, { diff --git a/rust/src/live/mod.rs b/rust/src/live/mod.rs index 89e8a1e..94e1801 100644 --- a/rust/src/live/mod.rs +++ b/rust/src/live/mod.rs @@ -1,7 +1,7 @@ mod bot; mod recorder; -pub use bot::{Bot, BotBuilder, BotError}; +pub use bot::{LiveBot, BotBuilder, BotError}; pub use recorder::LoggingRecorder; /// Provides asset information for internal use. diff --git a/rust/src/live/recorder.rs b/rust/src/live/recorder.rs index 4d53bd9..5585c4c 100644 --- a/rust/src/live/recorder.rs +++ b/rust/src/live/recorder.rs @@ -4,7 +4,7 @@ use tracing::info; use crate::{ depth::MarketDepth, - prelude::{get_precision, Interface}, + prelude::{get_precision, Bot}, types::{Recorder, StateValues}, }; @@ -18,7 +18,7 @@ impl Recorder for LoggingRecorder { fn record(&mut self, hbt: &mut I) -> Result<(), Self::Error> where - I: Interface, + I: Bot, MD: MarketDepth, { for asset_no in 0..hbt.num_assets() { diff --git a/rust/src/types.rs b/rust/src/types.rs index d899e07..2156151 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -659,7 +659,7 @@ pub struct OrderRequest { } /// Provides an interface for a backtester or a bot. -pub trait Interface { +pub trait Bot { type Error; /// In backtesting, this timestamp reflects the time at which the backtesting is conducted @@ -839,7 +839,7 @@ pub trait Recorder { type Error; fn record(&mut self, hbt: &mut I) -> Result<(), Self::Error> where - I: Interface + BotTypedDepth, + I: Bot + BotTypedDepth, MD: MarketDepth; }