Skip to content

Commit

Permalink
refactor(rust): refactor L3 implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
nkaz001 committed Aug 11, 2024
1 parent 1b33a78 commit 05e66e4
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 50 deletions.
14 changes: 14 additions & 0 deletions hftbacktest/src/backtest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,20 @@ pub struct L3AssetBuilder<LM, AT, QM, MD, FM> {
last_trades_cap: usize,
}

impl<LM, AT, QM, MD, FM> Default for L3AssetBuilder<LM, AT, QM, MD, FM>
where
AT: AssetType + Clone + 'static,
MD: MarketDepth + L3MarketDepth + 'static,
QM: L3QueueModel<MD> + 'static,
LM: LatencyModel + Clone + 'static,
FM: FeeModel + Clone + 'static,
BacktestError: From<<MD as L3MarketDepth>::Error>,
{
fn default() -> Self {
Self::new()
}
}

impl<LM, AT, QM, MD, FM> L3AssetBuilder<LM, AT, QM, MD, FM>
where
AT: AssetType + Clone + 'static,
Expand Down
2 changes: 1 addition & 1 deletion hftbacktest/src/backtest/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub use fee::{
};
pub use latency::{ConstantLatency, IntpOrderLatency, LatencyModel, OrderLatencyRow};
#[cfg(any(feature = "unstable_l3", doc))]
pub use queue::{L3FIFOQueueModel, L3OrderSource, L3QueueModel};
pub use queue::{L3FIFOQueueModel, L3QueueModel};
pub use queue::{
LogProbQueueFunc,
LogProbQueueFunc2,
Expand Down
70 changes: 46 additions & 24 deletions hftbacktest/src/backtest/models/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ impl Probability for PowerProbQueueFunc3 {
/// Represents the order source for the Level 3 Market-By-Order queue model, which is stored in
/// [`order.q`](crate::types::Order::q)
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum L3OrderSource {
enum L3OrderSource {
/// Represents an order originating from the market feed.
MarketFeed,
/// Represents an order originating from the backtest.
Expand All @@ -345,100 +345,123 @@ impl AnyClone for L3OrderSource {
}
}

trait L3OrderId {
trait L3Order {
fn order_source(&self) -> L3OrderSource;

fn is_backtest_order(&self) -> bool;

fn is_market_feed_order(&self) -> bool;
}

impl L3OrderId for Order {
impl L3Order for Order {
fn order_source(&self) -> L3OrderSource {
*self.q.as_any().downcast_ref::<L3OrderSource>().unwrap()
}

fn is_backtest_order(&self) -> bool {
let order_source = self.q.as_any().downcast_ref::<L3OrderSource>().unwrap();
match order_source {
match self.order_source() {
L3OrderSource::MarketFeed => false,
L3OrderSource::Backtest => true,
}
}

fn is_market_feed_order(&self) -> bool {
let order_source = self.q.as_any().downcast_ref::<L3OrderSource>().unwrap();
match order_source {
match self.order_source() {
L3OrderSource::MarketFeed => true,
L3OrderSource::Backtest => false,
}
}
}

/// Provides an estimation of the order's queue position for Level 3 Market-By-Order feed.
/// Provides a model to determine whether the backtest order is filled, accounting for the queue
/// position based on L3 Market-By-Order data.
pub trait L3QueueModel<MD> {
/// Returns `true` if the queue contains a backtest order for the order ID.
fn contains_backtest_order(&self, order_id: OrderId) -> bool;

/// Invoked when the best bid is updated.
/// Returns the ask backtest orders that are filled by crossing the best bid.
fn on_best_bid_update(
&mut self,
prev_best_tick: i64,
new_best_tick: i64,
) -> Result<Vec<Order>, BacktestError>;

/// Invoked when the best ask is updated.
/// Returns the bid backtest orders that are filled by crossing the best ask.
fn on_best_ask_update(
&mut self,
prev_best_tick: i64,
new_best_tick: i64,
) -> Result<Vec<Order>, BacktestError>;

/// This function is called when an order is added.
/// Invoked when a backtest order is added.
fn add_backtest_order(&mut self, order: Order, depth: &MD) -> Result<(), BacktestError>;

/// Invoked when an order is added from the market feed.
fn add_market_feed_order(&mut self, order: &Event, depth: &MD) -> Result<(), BacktestError>;

/// This function is called when an order is canceled.
/// It does not necessarily mean that the order is canceled by the person who submitted it. It
/// may simply mean that the order has been deleted in the market.
/// Invoked when a backtest order is canceled.
fn cancel_backtest_order(
&mut self,
order_id: OrderId,
depth: &MD,
) -> Result<Order, BacktestError>;

/// Invoked when an order is canceled from the market feed.
///
/// It does not necessarily mean that the order is canceled by the one who submitted it. It may
/// simply mean that the order has been deleted in the market.
fn cancel_market_feed_order(
&mut self,
order_id: OrderId,
depth: &MD,
) -> Result<(), BacktestError>;

/// This function is called when an order is modified.
/// Invoked when a backtest order is modified.
fn modify_backtest_order(
&mut self,
order_id: OrderId,
order: Order,
depth: &MD,
) -> Result<(), BacktestError>;

/// Invoked when an order is modified from the market feed.
fn modify_market_feed_order(
&mut self,
order_id: OrderId,
order: &Event,
depth: &MD,
) -> Result<(), BacktestError>;

/// This function is called when an order is filled.
/// Invoked when an order is filled from the market feed.
///
/// According to the exchange, the market feed may send fill and delete order events separately.
/// This means that after a fill event is received, a delete order event can be received
/// subsequently. The `delete` argument is used to indicate whether the order should be deleted
/// immediately or if it should be deleted upon receiving a delete order event, which is handled
/// by [`cancel_order`](L3QueueModel::cancel_backtest_order).
fn fill_market_feed_order(
/// subsequently. The `DELETE` constant generic is used to indicate whether the order should be
/// deleted immediately or if it should be deleted upon receiving a delete order event, which is
/// handled by [`cancel_market_feed_order`](L3QueueModel::cancel_market_feed_order).
fn fill_market_feed_order<const DELETE: bool>(
&mut self,
order_id: OrderId,
delete: bool,
depth: &MD,
) -> Result<Vec<Order>, BacktestError>;

/// Invoked when a clear order message is received. Returns the expired orders due to the clear
/// message.
///
/// Such messages can occur in two scenarios:
///
/// 1. The exchange clears orders for reasons such as session close.
/// In this case, backtest orders must also be cleared.
/// 2. The exchange sends a clear message before sending a snapshot to properly maintain market
/// depth. Here, clearing backtest orders is not always necessary. However, estimating the
/// order queue position becomes difficult because clearing the market feed orders leads to
/// the loss of queue position information. Additionally, there's no guarantee that all
/// orders preceding the clear message will persist.
///
/// Due to these challenges, HftBacktest opts to clear all backtest orders upon receiving a
/// clear message, even though this may differ from the exchange's actual behavior.
fn clear_orders(&mut self, side: Side) -> Vec<Order>;
}

Expand Down Expand Up @@ -934,13 +957,12 @@ where
Ok(())
}

fn fill_market_feed_order(
fn fill_market_feed_order<const DELETE: bool>(
&mut self,
order_id: OrderId,
delete: bool,
_depth: &MD,
) -> Result<Vec<Order>, BacktestError> {
let (side, order_price_tick) = if delete {
let (side, order_price_tick) = if DELETE {
self.mkt_feed_orders
.remove(&order_id)
.ok_or(BacktestError::OrderNotFound)?
Expand Down Expand Up @@ -975,7 +997,7 @@ where
}
}
let pos = pos.ok_or(BacktestError::OrderNotFound)?;
if delete {
if DELETE {
queue.remove(pos);
// if queue.len() == 0 {
// self.ask_queue.remove(&order_price_tick);
Expand Down Expand Up @@ -1009,7 +1031,7 @@ where
}
}
let pos = pos.ok_or(BacktestError::OrderNotFound)?;
if delete {
if DELETE {
queue.remove(pos);
// if queue.len() == 0 {
// self.ask_queue.remove(&order_price_tick);
Expand Down
6 changes: 0 additions & 6 deletions hftbacktest/src/backtest/proc/l3_local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ use crate::{
LOCAL_CANCEL_ORDER_EVENT,
LOCAL_DEPTH_CLEAR_EVENT,
LOCAL_EVENT,
LOCAL_FILL_EVENT,
LOCAL_MODIFY_ORDER_EVENT,
LOCAL_TRADE_EVENT,
},
Expand Down Expand Up @@ -283,11 +282,6 @@ where
.modify_order(ev.order_id, ev.px, ev.qty, ev.local_ts)?;
} else if ev.is(LOCAL_CANCEL_ORDER_EVENT) {
self.depth.delete_order(ev.order_id, ev.local_ts)?;
} else if ev.is(LOCAL_FILL_EVENT) {
// todo: based on Databento's data, CME sends a separate cancel message for filled
// orders, so the fill event doesn't need to remove the order. However, it needs
// to be checked if the same applies to other exchanges.
// self.depth.delete_order(ev.order_id, ev.local_ts)?;
}
// Processes a trade event
else if ev.is(LOCAL_TRADE_EVENT) && self.trades.capacity() > 0 {
Expand Down
36 changes: 17 additions & 19 deletions hftbacktest/src/backtest/proc/l3_nopartialfillexchange.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,10 @@ where
new_best_tick: i64,
timestamp: i64,
) -> Result<(), BacktestError> {
let filled_orders = self
let filled = self
.queue_model
.on_best_bid_update(prev_best_tick, new_best_tick)?;
for mut order in filled_orders {
for mut order in filled {
let price_tick = order.price_tick;
self.fill(&mut order, timestamp, true, price_tick)?;
}
Expand All @@ -194,10 +194,10 @@ where
new_best_tick: i64,
timestamp: i64,
) -> Result<(), BacktestError> {
let filled_orders = self
let filled = self
.queue_model
.on_best_ask_update(prev_best_tick, new_best_tick)?;
for mut order in filled_orders {
for mut order in filled {
let price_tick = order.price_tick;
self.fill(&mut order, timestamp, true, price_tick)?;
}
Expand Down Expand Up @@ -324,21 +324,21 @@ where
let row_num = self.row_num;
if self.data[row_num].is(EXCH_BID_DEPTH_CLEAR_EVENT) {
self.depth.clear_orders(Side::Buy);
let expired_orders = self.queue_model.clear_orders(Side::Buy);
for expired_order in expired_orders {
self.expired(expired_order, self.data[row_num].exch_ts)?;
let expired = self.queue_model.clear_orders(Side::Buy);
for order in expired {
self.expired(order, self.data[row_num].exch_ts)?;
}
} else if self.data[row_num].is(EXCH_ASK_DEPTH_CLEAR_EVENT) {
self.depth.clear_orders(Side::Sell);
let expired_orders = self.queue_model.clear_orders(Side::Sell);
for expired_order in expired_orders {
self.expired(expired_order, self.data[row_num].exch_ts)?;
let expired = self.queue_model.clear_orders(Side::Sell);
for order in expired {
self.expired(order, self.data[row_num].exch_ts)?;
}
} else if self.data[row_num].is(EXCH_DEPTH_CLEAR_EVENT) {
self.depth.clear_orders(Side::None);
let expired_orders = self.queue_model.clear_orders(Side::None);
for expired_order in expired_orders {
self.expired(expired_order, self.data[row_num].exch_ts)?;
let expired = self.queue_model.clear_orders(Side::None);
for order in expired {
self.expired(order, self.data[row_num].exch_ts)?;
}
} 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(
Expand Down Expand Up @@ -398,13 +398,11 @@ where
self.queue_model
.cancel_market_feed_order(self.data[row_num].order_id, &self.depth)?;
} else if self.data[row_num].is(EXCH_FILL_EVENT) {
let filled_orders = self.queue_model.fill_market_feed_order(
self.data[row_num].order_id,
false,
&self.depth,
)?;
let filled = self
.queue_model
.fill_market_feed_order::<false>(self.data[row_num].order_id, &self.depth)?;
let timestamp = self.data[row_num].exch_ts;
for mut order in filled_orders {
for mut order in filled {
let price_tick = order.price_tick;
self.fill(&mut order, timestamp, true, price_tick)?;
}
Expand Down

0 comments on commit 05e66e4

Please sign in to comment.