mod depth_statistics;
mod market_impact;
pub use depth_statistics::*;
pub use market_impact::*;
use super::{LimitBook, OrderBook};
use crate::{Notional, PegReference, Price, Quantity, Side};
impl LimitBook {
pub fn best_bid(&self) -> Option<(Price, Quantity)> {
self.bids
.iter()
.next_back()
.map(|(price, level_id)| (*price, self.levels[*level_id].total_quantity()))
}
pub fn best_ask(&self) -> Option<(Price, Quantity)> {
self.asks
.iter()
.next()
.map(|(price, level_id)| (*price, self.levels[*level_id].total_quantity()))
}
pub fn best_bid_price(&self) -> Option<Price> {
self.bids.keys().next_back().copied()
}
pub fn best_ask_price(&self) -> Option<Price> {
self.asks.keys().next().copied()
}
pub fn best_bid_size(&self) -> Option<Quantity> {
self.bids
.values()
.next_back()
.map(|level_id| self.levels[*level_id].total_quantity())
}
pub fn best_ask_size(&self) -> Option<Quantity> {
self.asks
.values()
.next()
.map(|level_id| self.levels[*level_id].total_quantity())
}
pub fn is_side_empty(&self, side: Side) -> bool {
match side {
Side::Buy => self.bids.is_empty(),
Side::Sell => self.asks.is_empty(),
}
}
pub fn has_crossable_order(&self, taker_side: Side, limit_price: Price) -> bool {
match taker_side {
Side::Buy => self.best_ask_price().is_some_and(|ask| limit_price >= ask),
Side::Sell => self.best_bid_price().is_some_and(|bid| limit_price <= bid),
}
}
pub fn spread(&self) -> Option<u64> {
let best_bid = self.best_bid_price()?;
let best_ask = self.best_ask_price()?;
Some(best_ask - best_bid)
}
pub fn mid_price(&self) -> Option<f64> {
let best_bid = self.best_bid_price()?;
let best_ask = self.best_ask_price()?;
Some((best_bid.as_f64() + best_ask.as_f64()) / 2.0)
}
pub fn micro_price(&self) -> Option<f64> {
let (best_bid_price, best_bid_size) = self.best_bid()?;
let (best_ask_price, best_ask_size) = self.best_ask()?;
let total_size = best_bid_size.saturating_add(best_ask_size);
if total_size.is_zero() {
return None;
}
let numerator = (best_ask_price * best_bid_size) + (best_bid_price * best_ask_size);
let denominator = total_size;
Some(numerator / denominator)
}
pub fn bid_size(&self, n_levels: usize) -> Quantity {
self.bids
.values()
.rev()
.take(n_levels)
.map(|level_id| self.levels[*level_id].total_quantity())
.sum::<Quantity>()
}
pub fn ask_size(&self, n_levels: usize) -> Quantity {
self.asks
.values()
.take(n_levels)
.map(|level_id| self.levels[*level_id].total_quantity())
.sum::<Quantity>()
}
pub fn is_thin_book(&self, threshold: Quantity, n_levels: usize) -> bool {
let bid_size = self.bid_size(n_levels);
let ask_size = self.ask_size(n_levels);
bid_size < threshold || ask_size < threshold
}
pub fn order_book_imbalance(&self, n_levels: usize) -> f64 {
let bid_size = self.bid_size(n_levels);
let ask_size = self.ask_size(n_levels);
let total_size = bid_size.saturating_add(ask_size);
if total_size.is_zero() {
return 0.0;
}
(bid_size.as_f64() - ask_size.as_f64()) / total_size.as_f64()
}
pub fn depth_statistics(&self, side: Side, n_levels: usize) -> DepthStatistics {
DepthStatistics::compute(self, side, n_levels)
}
}
impl OrderBook {
pub fn best_bid(&self) -> Option<(Price, Quantity)> {
self.limit.best_bid()
}
pub fn best_ask(&self) -> Option<(Price, Quantity)> {
self.limit.best_ask()
}
pub fn best_bid_price(&self) -> Option<Price> {
self.limit.best_bid_price()
}
pub fn best_ask_price(&self) -> Option<Price> {
self.limit.best_ask_price()
}
pub fn best_bid_size(&self) -> Option<Quantity> {
self.limit.best_bid_size()
}
pub fn best_ask_size(&self) -> Option<Quantity> {
self.limit.best_ask_size()
}
pub fn is_side_empty(&self, side: Side) -> bool {
self.limit.is_side_empty(side)
}
pub fn has_crossable_order(&self, taker_side: Side, limit_price: Price) -> bool {
self.limit.has_crossable_order(taker_side, limit_price)
}
pub fn spread(&self) -> Option<u64> {
self.limit.spread()
}
pub fn mid_price(&self) -> Option<f64> {
self.limit.mid_price()
}
pub fn micro_price(&self) -> Option<f64> {
self.limit.micro_price()
}
pub fn bid_size(&self, n_levels: usize) -> Quantity {
self.limit.bid_size(n_levels)
}
pub fn ask_size(&self, n_levels: usize) -> Quantity {
self.limit.ask_size(n_levels)
}
pub fn is_thin_book(&self, threshold: Quantity, n_levels: usize) -> bool {
self.limit.is_thin_book(threshold, n_levels)
}
pub fn order_book_imbalance(&self, n_levels: usize) -> f64 {
self.limit.order_book_imbalance(n_levels)
}
pub fn depth_statistics(&self, side: Side, n_levels: usize) -> DepthStatistics {
self.limit.depth_statistics(side, n_levels)
}
pub fn buy_sell_pressure(&self) -> (Quantity, Quantity) {
let buy_limit_pressure = self.bid_size(usize::MAX);
let sell_limit_pressure = self.ask_size(usize::MAX);
let buy_peg_pressure = self
.pegged
.bid_levels
.iter()
.map(|level| level.quantity())
.sum();
let sell_peg_pressure = self
.pegged
.ask_levels
.iter()
.map(|level| level.quantity())
.sum();
(
buy_limit_pressure.saturating_add(buy_peg_pressure),
sell_limit_pressure.saturating_add(sell_peg_pressure),
)
}
pub fn price_at_depth(&self, side: Side, depth: Quantity) -> Option<Price> {
let mid_active = self.spread().is_some_and(|spread| spread <= 1);
let mut cumulative = Quantity(0);
match side {
Side::Buy => {
if mid_active {
cumulative = cumulative.saturating_add(
self.pegged.bid_levels[PegReference::MidPrice.as_index()].quantity(),
);
}
cumulative = cumulative.saturating_add(
self.pegged.bid_levels[PegReference::Primary.as_index()].quantity(),
);
for (price, level_id) in self.limit.bids.iter().rev() {
let level = &self.limit.levels[*level_id];
cumulative = cumulative.saturating_add(level.total_quantity());
if cumulative >= depth {
return Some(*price);
}
}
None
}
Side::Sell => {
if mid_active {
cumulative = cumulative.saturating_add(
self.pegged.ask_levels[PegReference::MidPrice.as_index()].quantity(),
);
}
cumulative = cumulative.saturating_add(
self.pegged.ask_levels[PegReference::Primary.as_index()].quantity(),
);
for (price, level_id) in self.limit.asks.iter() {
let level = &self.limit.levels[*level_id];
cumulative = cumulative.saturating_add(level.total_quantity());
if cumulative >= depth {
return Some(*price);
}
}
None
}
}
}
pub fn vwap(&self, taker_side: Side, quantity: Quantity) -> Option<f64> {
if quantity.is_zero() {
return None;
}
let mid_active = self.spread().is_some_and(|spread| spread <= 1);
let mut remaining = quantity;
let mut total_cost = Notional(0);
let mut total_filled = Quantity(0);
match taker_side {
Side::Buy => {
let best_ask = self.best_ask_price()?;
if mid_active {
let available =
self.pegged.ask_levels[PegReference::MidPrice.as_index()].quantity();
let fill_qty = remaining.min(available);
total_cost = total_cost.saturating_add(best_ask * fill_qty);
total_filled = total_filled.saturating_add(fill_qty);
remaining = remaining.saturating_sub(fill_qty);
if remaining.is_zero() {
return Some(total_cost / total_filled);
}
}
let available = self.pegged.ask_levels[PegReference::Primary.as_index()].quantity();
let fill_qty = remaining.min(available);
total_cost = total_cost.saturating_add(best_ask * fill_qty);
total_filled = total_filled.saturating_add(fill_qty);
remaining = remaining.saturating_sub(fill_qty);
if remaining.is_zero() {
return Some(total_cost / total_filled);
}
for (price, level_id) in self.limit.asks.iter() {
let level = &self.limit.levels[*level_id];
let available = level.total_quantity();
let fill_qty = remaining.min(available);
total_cost = total_cost.saturating_add(*price * fill_qty);
total_filled = total_filled.saturating_add(fill_qty);
remaining = remaining.saturating_sub(fill_qty);
if remaining.is_zero() {
return Some(total_cost / total_filled);
}
}
None
}
Side::Sell => {
let best_bid = self.best_bid_price()?;
if mid_active {
let available =
self.pegged.bid_levels[PegReference::MidPrice.as_index()].quantity();
let fill_qty = remaining.min(available);
total_cost = total_cost.saturating_add(best_bid * fill_qty);
total_filled = total_filled.saturating_add(fill_qty);
remaining = remaining.saturating_sub(fill_qty);
if remaining.is_zero() {
return Some(total_cost / total_filled);
}
}
let available = self.pegged.bid_levels[PegReference::Primary.as_index()].quantity();
let fill_qty = remaining.min(available);
total_cost = total_cost.saturating_add(best_bid * fill_qty);
total_filled = total_filled.saturating_add(fill_qty);
remaining = remaining.saturating_sub(fill_qty);
if remaining.is_zero() {
return Some(total_cost / total_filled);
}
for (price, level_id) in self.limit.bids.iter().rev() {
let level = &self.limit.levels[*level_id];
let available = level.total_quantity();
let fill_qty = remaining.min(available);
total_cost = total_cost.saturating_add(*price * fill_qty);
total_filled = total_filled.saturating_add(fill_qty);
remaining = remaining.saturating_sub(fill_qty);
if remaining.is_zero() {
return Some(total_cost / total_filled);
}
}
None
}
}
}
pub fn market_impact(&self, taker_side: Side, quantity: Quantity) -> MarketImpact {
MarketImpact::compute(self, taker_side, quantity)
}
}
#[cfg(test)]
mod tests_limit_book {
use super::*;
use crate::PriceLevel;
const EPS: f64 = 1e-6;
fn make_level(visible_qty: Quantity) -> PriceLevel {
let mut level = PriceLevel::new();
level.visible_quantity = visible_qty;
level
}
fn book_with_bids_and_asks(
bids: &[(Price, Quantity)],
asks: &[(Price, Quantity)],
) -> LimitBook {
let mut book = LimitBook::new();
for &(price, qty) in bids {
let level = make_level(qty);
let level_id = book.levels.insert(level);
book.bids.insert(price, level_id);
}
for &(price, qty) in asks {
let level = make_level(qty);
let level_id = book.levels.insert(level);
book.asks.insert(price, level_id);
}
book
}
fn basic_book() -> LimitBook {
book_with_bids_and_asks(
&[(Price(100), Quantity(50)), (Price(99), Quantity(30))],
&[(Price(101), Quantity(40)), (Price(102), Quantity(60))],
)
}
#[test]
fn best_bid_empty() {
let book = LimitBook::new();
assert_eq!(book.best_bid(), None);
}
#[test]
fn best_bid_single() {
let book = book_with_bids_and_asks(&[(Price(100), Quantity(10))], &[]);
assert_eq!(book.best_bid(), Some((Price(100), Quantity(10))));
}
#[test]
fn best_bid_multiple() {
let book = book_with_bids_and_asks(
&[
(Price(90), Quantity(5)),
(Price(100), Quantity(10)),
(Price(95), Quantity(7)),
],
&[],
);
assert_eq!(book.best_bid(), Some((Price(100), Quantity(10))));
}
#[test]
fn best_ask_empty() {
let book = LimitBook::new();
assert_eq!(book.best_ask(), None);
}
#[test]
fn best_ask_single() {
let book = book_with_bids_and_asks(&[], &[(Price(200), Quantity(10))]);
assert_eq!(book.best_ask(), Some((Price(200), Quantity(10))));
}
#[test]
fn best_ask_multiple() {
let book = book_with_bids_and_asks(
&[],
&[
(Price(210), Quantity(5)),
(Price(200), Quantity(10)),
(Price(205), Quantity(7)),
],
);
assert_eq!(book.best_ask(), Some((Price(200), Quantity(10))));
}
#[test]
fn best_bid_price_empty() {
let book = LimitBook::new();
assert_eq!(book.best_bid_price(), None);
}
#[test]
fn best_bid_price_single() {
let book = book_with_bids_and_asks(&[(Price(100), Quantity(10))], &[]);
assert_eq!(book.best_bid_price(), Some(Price(100)));
}
#[test]
fn best_bid_price_returns_highest() {
let book = book_with_bids_and_asks(
&[
(Price(90), Quantity(5)),
(Price(100), Quantity(10)),
(Price(95), Quantity(7)),
],
&[],
);
assert_eq!(book.best_bid_price(), Some(Price(100)));
}
#[test]
fn best_ask_price_empty() {
let book = LimitBook::new();
assert_eq!(book.best_ask_price(), None);
}
#[test]
fn best_ask_price_single() {
let book = book_with_bids_and_asks(&[], &[(Price(200), Quantity(10))]);
assert_eq!(book.best_ask_price(), Some(Price(200)));
}
#[test]
fn best_ask_price_returns_lowest() {
let book = book_with_bids_and_asks(
&[],
&[
(Price(210), Quantity(5)),
(Price(200), Quantity(10)),
(Price(205), Quantity(7)),
],
);
assert_eq!(book.best_ask_price(), Some(Price(200)));
}
#[test]
fn best_bid_size_empty() {
let book = LimitBook::new();
assert_eq!(book.best_bid_size(), None);
}
#[test]
fn best_bid_size_single_level() {
let book = book_with_bids_and_asks(&[(Price(100), Quantity(50))], &[]);
assert_eq!(book.best_bid_size(), Some(Quantity(50)));
}
#[test]
fn best_bid_size_multiple_levels() {
let book = book_with_bids_and_asks(
&[
(Price(90), Quantity(30)),
(Price(100), Quantity(50)),
(Price(95), Quantity(20)),
],
&[],
);
assert_eq!(book.best_bid_size(), Some(Quantity(50)));
}
#[test]
fn best_ask_size_empty() {
let book = LimitBook::new();
assert_eq!(book.best_ask_size(), None);
}
#[test]
fn best_ask_size_single_level() {
let book = book_with_bids_and_asks(&[], &[(Price(200), Quantity(40))]);
assert_eq!(book.best_ask_size(), Some(Quantity(40)));
}
#[test]
fn best_ask_size_multiple_levels() {
let book = book_with_bids_and_asks(
&[],
&[
(Price(200), Quantity(40)),
(Price(210), Quantity(60)),
(Price(205), Quantity(25)),
],
);
assert_eq!(book.best_ask_size(), Some(Quantity(40)));
}
#[test]
fn is_side_empty_both_empty() {
let book = LimitBook::new();
assert!(book.is_side_empty(Side::Buy));
assert!(book.is_side_empty(Side::Sell));
}
#[test]
fn is_side_empty_with_bids() {
let book = book_with_bids_and_asks(&[(Price(100), Quantity(10))], &[]);
assert!(!book.is_side_empty(Side::Buy));
assert!(book.is_side_empty(Side::Sell));
}
#[test]
fn is_side_empty_with_asks() {
let book = book_with_bids_and_asks(&[], &[(Price(200), Quantity(10))]);
assert!(book.is_side_empty(Side::Buy));
assert!(!book.is_side_empty(Side::Sell));
}
#[test]
fn is_side_empty_both_sides() {
let book =
book_with_bids_and_asks(&[(Price(100), Quantity(10))], &[(Price(200), Quantity(10))]);
assert!(!book.is_side_empty(Side::Buy));
assert!(!book.is_side_empty(Side::Sell));
}
#[test]
fn has_crossable_order_empty_book() {
let book = LimitBook::new();
assert!(!book.has_crossable_order(Side::Buy, Price(100)));
assert!(!book.has_crossable_order(Side::Sell, Price(100)));
}
#[test]
fn has_crossable_buy_at_ask() {
let book = book_with_bids_and_asks(&[], &[(Price(200), Quantity(10))]);
assert!(book.has_crossable_order(Side::Buy, Price(200)));
}
#[test]
fn has_crossable_buy_above_ask() {
let book = book_with_bids_and_asks(&[], &[(Price(200), Quantity(10))]);
assert!(book.has_crossable_order(Side::Buy, Price(250)));
}
#[test]
fn has_crossable_buy_below_ask() {
let book = book_with_bids_and_asks(&[], &[(Price(200), Quantity(10))]);
assert!(!book.has_crossable_order(Side::Buy, Price(199)));
}
#[test]
fn has_crossable_sell_at_bid() {
let book = book_with_bids_and_asks(&[(Price(100), Quantity(10))], &[]);
assert!(book.has_crossable_order(Side::Sell, Price(100)));
}
#[test]
fn has_crossable_sell_below_bid() {
let book = book_with_bids_and_asks(&[(Price(100), Quantity(10))], &[]);
assert!(book.has_crossable_order(Side::Sell, Price(50)));
}
#[test]
fn has_crossable_sell_above_bid() {
let book = book_with_bids_and_asks(&[(Price(100), Quantity(10))], &[]);
assert!(!book.has_crossable_order(Side::Sell, Price(101)));
}
#[test]
fn has_crossable_order_no_opposite_side() {
let book = book_with_bids_and_asks(&[(Price(100), Quantity(10))], &[]);
assert!(!book.has_crossable_order(Side::Buy, Price(200)));
let book = book_with_bids_and_asks(&[], &[(Price(200), Quantity(10))]);
assert!(!book.has_crossable_order(Side::Sell, Price(100)));
}
#[test]
fn spread_empty_book() {
let book = LimitBook::new();
assert_eq!(book.spread(), None);
}
#[test]
fn spread_only_bids() {
let book = book_with_bids_and_asks(&[(Price(100), Quantity(10))], &[]);
assert_eq!(book.spread(), None);
}
#[test]
fn spread_only_asks() {
let book = book_with_bids_and_asks(&[], &[(Price(200), Quantity(10))]);
assert_eq!(book.spread(), None);
}
#[test]
fn spread_both_sides() {
let book =
book_with_bids_and_asks(&[(Price(100), Quantity(10))], &[(Price(200), Quantity(10))]);
assert_eq!(book.spread(), Some(100));
}
#[test]
fn spread_one_tick() {
let book =
book_with_bids_and_asks(&[(Price(100), Quantity(10))], &[(Price(101), Quantity(10))]);
assert_eq!(book.spread(), Some(1));
}
#[test]
fn mid_price_empty_book() {
let book = LimitBook::new();
assert_eq!(book.mid_price(), None);
}
#[test]
fn mid_price_only_bids() {
let book = book_with_bids_and_asks(&[(Price(100), Quantity(10))], &[]);
assert_eq!(book.mid_price(), None);
}
#[test]
fn mid_price_only_asks() {
let book = book_with_bids_and_asks(&[], &[(Price(200), Quantity(10))]);
assert_eq!(book.mid_price(), None);
}
#[test]
fn mid_price_both_sides() {
let book =
book_with_bids_and_asks(&[(Price(100), Quantity(10))], &[(Price(200), Quantity(10))]);
assert_eq!(book.mid_price(), Some(150.0));
}
#[test]
fn mid_price_tight_spread() {
let book =
book_with_bids_and_asks(&[(Price(99), Quantity(10))], &[(Price(101), Quantity(10))]);
assert_eq!(book.mid_price(), Some(100.0));
}
#[test]
fn mid_price_odd_spread() {
let book =
book_with_bids_and_asks(&[(Price(100), Quantity(10))], &[(Price(101), Quantity(10))]);
assert_eq!(book.mid_price(), Some(100.5));
}
#[test]
fn micro_price_empty_book() {
assert!(LimitBook::new().micro_price().is_none());
}
#[test]
fn micro_price_balanced_sizes() {
let book = book_with_bids_and_asks(
&[(Price(100), Quantity(100))],
&[(Price(102), Quantity(100))],
);
assert!((book.micro_price().unwrap() - 101.0).abs() < EPS);
}
#[test]
fn micro_price_imbalanced_toward_bid() {
let book = book_with_bids_and_asks(
&[(Price(100), Quantity(300))],
&[(Price(102), Quantity(100))],
);
assert!((book.micro_price().unwrap() - 101.5).abs() < EPS);
}
#[test]
fn micro_price_imbalanced_toward_ask() {
let book = book_with_bids_and_asks(
&[(Price(100), Quantity(100))],
&[(Price(102), Quantity(300))],
);
assert!((book.micro_price().unwrap() - 100.5).abs() < EPS);
}
#[test]
fn bid_ask_sizes_empty_book() {
let book = LimitBook::new();
assert_eq!(book.bid_size(5), Quantity(0));
assert_eq!(book.ask_size(5), Quantity(0));
}
#[test]
fn bid_ask_sizes_n_levels_zero() {
let book =
book_with_bids_and_asks(&[(Price(100), Quantity(10))], &[(Price(102), Quantity(10))]);
assert_eq!(book.bid_size(0), Quantity(0));
assert_eq!(book.ask_size(0), Quantity(0));
}
#[test]
fn bid_ask_sizes_single_level() {
let book = basic_book();
assert_eq!(book.bid_size(1), Quantity(50));
assert_eq!(book.ask_size(1), Quantity(40));
}
#[test]
fn bid_ask_sizes_multiple_levels() {
let book = basic_book();
assert_eq!(book.bid_size(2), Quantity(80));
assert_eq!(book.ask_size(2), Quantity(100));
}
#[test]
fn bid_ask_sizes_exceeding_available_levels() {
let book = basic_book();
assert_eq!(book.bid_size(100), Quantity(80));
assert_eq!(book.ask_size(100), Quantity(100));
}
#[test]
fn is_thin_book_empty() {
assert!(LimitBook::new().is_thin_book(Quantity(1), 1));
}
#[test]
fn is_thin_book_sufficient_depth() {
let book = basic_book();
assert!(!book.is_thin_book(Quantity(50), 2));
}
#[test]
fn is_thin_book_one_side_thin() {
let book = basic_book();
assert!(book.is_thin_book(Quantity(45), 1));
}
#[test]
fn is_thin_book_both_sides_sufficient() {
let book = basic_book();
assert!(!book.is_thin_book(Quantity(40), 1));
}
#[test]
fn is_thin_book_threshold_zero() {
assert!(!LimitBook::new().is_thin_book(Quantity(0), 1));
}
#[test]
fn order_book_imbalance_empty_book() {
assert_eq!(LimitBook::new().order_book_imbalance(5), 0.0);
}
#[test]
fn order_book_imbalance_balanced() {
let book = book_with_bids_and_asks(
&[(Price(100), Quantity(100))],
&[(Price(101), Quantity(100))],
);
assert!((book.order_book_imbalance(1) - 0.0).abs() < EPS);
}
#[test]
fn order_book_imbalance_all_bids() {
let book = book_with_bids_and_asks(&[(Price(100), Quantity(100))], &[]);
assert!((book.order_book_imbalance(1) - 1.0).abs() < EPS);
}
#[test]
fn order_book_imbalance_all_asks() {
let book = book_with_bids_and_asks(&[], &[(Price(101), Quantity(100))]);
assert!((book.order_book_imbalance(1) - (-1.0)).abs() < EPS);
}
#[test]
fn order_book_imbalance_multiple_levels() {
let book = basic_book();
let imb1 = book.order_book_imbalance(1);
assert!((imb1 - 10.0 / 90.0).abs() < EPS);
let imb2 = book.order_book_imbalance(2);
assert!((imb2 - (-20.0 / 180.0)).abs() < EPS);
}
#[test]
fn order_book_imbalance_n_levels_zero() {
let book = basic_book();
assert_eq!(book.order_book_imbalance(0), 0.0);
}
#[test]
fn depth_statistics_empty_book() {
let stats = LimitBook::new().depth_statistics(Side::Buy, 5);
assert!(stats.is_empty());
assert_eq!(stats.n_analyzed_levels(), 0);
assert_eq!(stats.total_size(), Quantity(0));
assert_eq!(stats.min_level_size(), Quantity(0));
assert_eq!(stats.max_level_size(), Quantity(0));
}
#[test]
fn depth_statistics_single_level() {
let book = book_with_bids_and_asks(&[(Price(50), Quantity(100))], &[]);
let stats = book.depth_statistics(Side::Buy, 1);
assert_eq!(stats.n_analyzed_levels(), 1);
assert_eq!(stats.total_value(), Notional(5000));
assert_eq!(stats.total_size(), Quantity(100));
assert_eq!(stats.min_level_size(), Quantity(100));
assert_eq!(stats.max_level_size(), Quantity(100));
assert!((stats.average_level_size() - 100.0).abs() < EPS);
assert!((stats.std_dev_level_size() - 0.0).abs() < EPS);
assert!((stats.vwap() - 50.0).abs() < EPS);
}
#[test]
fn depth_statistics_multiple_levels() {
let book = basic_book();
let stats = book.depth_statistics(Side::Buy, 2);
assert_eq!(stats.n_analyzed_levels(), 2);
assert_eq!(stats.total_value(), Notional(7970)); assert_eq!(stats.total_size(), Quantity(80)); assert_eq!(stats.min_level_size(), Quantity(30));
assert_eq!(stats.max_level_size(), Quantity(50));
assert!((stats.average_level_size() - 40.0).abs() < EPS);
}
#[test]
fn depth_statistics_sell_side() {
let book = basic_book();
let stats = book.depth_statistics(Side::Sell, 2);
assert_eq!(stats.n_analyzed_levels(), 2);
assert_eq!(stats.total_value(), Notional(10160)); assert_eq!(stats.total_size(), Quantity(100)); assert_eq!(stats.min_level_size(), Quantity(40));
assert_eq!(stats.max_level_size(), Quantity(60));
assert!((stats.average_level_size() - 50.0).abs() < EPS);
}
#[test]
fn depth_statistics_zero_levels_means_all() {
let book = basic_book();
let stats = book.depth_statistics(Side::Buy, 0);
assert_eq!(stats.n_analyzed_levels(), 2);
assert_eq!(stats.total_size(), Quantity(80));
}
}
#[cfg(test)]
mod tests_order_book {
use super::*;
use crate::{
LimitOrder, OrderFlags, OrderId, PegReference, PeggedOrder, Price, Quantity,
QuantityPolicy, SequenceNumber, Side, TimeInForce,
};
const EPS: f64 = 1e-9;
fn empty_book() -> OrderBook {
OrderBook::new("TEST")
}
fn standard(price: u64, qty: u64, side: Side) -> LimitOrder {
LimitOrder::new(
Price(price),
QuantityPolicy::Standard {
quantity: Quantity(qty),
},
OrderFlags::new(side, false, TimeInForce::Gtc),
)
}
fn iceberg(price: u64, visible: u64, hidden: u64, side: Side) -> LimitOrder {
LimitOrder::new(
Price(price),
QuantityPolicy::Iceberg {
visible_quantity: Quantity(visible),
hidden_quantity: Quantity(hidden),
replenish_quantity: Quantity(visible),
},
OrderFlags::new(side, false, TimeInForce::Gtc),
)
}
fn pegged(reference: PegReference, qty: u64, side: Side) -> PeggedOrder {
PeggedOrder::new(
reference,
Quantity(qty),
OrderFlags::new(side, false, TimeInForce::Gtc),
)
}
fn basic_book() -> OrderBook {
let mut book = OrderBook::new("TEST");
book.add_limit_order(SequenceNumber(0), OrderId(0), standard(100, 50, Side::Buy));
book.add_limit_order(SequenceNumber(1), OrderId(1), standard(99, 30, Side::Buy));
book.add_limit_order(SequenceNumber(2), OrderId(2), standard(101, 40, Side::Sell));
book.add_limit_order(SequenceNumber(3), OrderId(3), standard(102, 60, Side::Sell));
book
}
fn book_with_hidden() -> OrderBook {
let mut book = OrderBook::new("TEST");
book.add_limit_order(
SequenceNumber(0),
OrderId(0),
iceberg(100, 50, 10, Side::Buy),
);
book.add_limit_order(
SequenceNumber(1),
OrderId(1),
iceberg(99, 30, 20, Side::Buy),
);
book.add_limit_order(
SequenceNumber(2),
OrderId(2),
iceberg(101, 40, 5, Side::Sell),
);
book.add_limit_order(
SequenceNumber(3),
OrderId(3),
iceberg(102, 60, 15, Side::Sell),
);
book
}
fn book_with_pegs() -> OrderBook {
let mut book = basic_book();
book.add_pegged_order(
SequenceNumber(10),
OrderId(10),
pegged(PegReference::Primary, 20, Side::Buy),
);
book.add_pegged_order(
SequenceNumber(11),
OrderId(11),
pegged(PegReference::Primary, 25, Side::Sell),
);
book
}
fn book_with_mid_price_pegs() -> OrderBook {
let mut book = basic_book();
book.add_pegged_order(
SequenceNumber(10),
OrderId(10),
pegged(PegReference::Primary, 20, Side::Buy),
);
book.add_pegged_order(
SequenceNumber(11),
OrderId(11),
pegged(PegReference::Primary, 25, Side::Sell),
);
book.add_pegged_order(
SequenceNumber(12),
OrderId(12),
pegged(PegReference::MidPrice, 15, Side::Buy),
);
book.add_pegged_order(
SequenceNumber(13),
OrderId(13),
pegged(PegReference::MidPrice, 10, Side::Sell),
);
book
}
#[test]
fn buy_sell_pressure_empty_book() {
let (buy, sell) = empty_book().buy_sell_pressure();
assert_eq!(buy, Quantity(0));
assert_eq!(sell, Quantity(0));
}
#[test]
fn buy_sell_pressure_limit_only() {
let book = basic_book();
let (buy, sell) = book.buy_sell_pressure();
assert_eq!(buy, Quantity(80)); assert_eq!(sell, Quantity(100)); }
#[test]
fn buy_sell_pressure_with_pegs() {
let book = book_with_pegs();
let (buy, sell) = book.buy_sell_pressure();
assert_eq!(buy, Quantity(100)); assert_eq!(sell, Quantity(125)); }
#[test]
fn buy_sell_pressure_includes_hidden() {
let book = book_with_hidden();
let (buy, sell) = book.buy_sell_pressure();
assert_eq!(buy, Quantity(110));
assert_eq!(sell, Quantity(120));
}
#[test]
fn buy_sell_pressure_includes_all_peg_references() {
let book = book_with_mid_price_pegs();
let (buy, sell) = book.buy_sell_pressure();
assert_eq!(buy, Quantity(115));
assert_eq!(sell, Quantity(135));
}
#[test]
fn price_at_depth_empty_book() {
assert!(
empty_book()
.price_at_depth(Side::Buy, Quantity(1))
.is_none()
);
assert!(
empty_book()
.price_at_depth(Side::Sell, Quantity(1))
.is_none()
);
}
#[test]
fn price_at_depth_first_level_buy() {
let book = basic_book();
assert_eq!(
book.price_at_depth(Side::Buy, Quantity(50)),
Some(Price(100))
);
}
#[test]
fn price_at_depth_first_level_sell() {
let book = basic_book();
assert_eq!(
book.price_at_depth(Side::Sell, Quantity(40)),
Some(Price(101))
);
}
#[test]
fn price_at_depth_spans_multiple_levels_buy() {
let book = basic_book();
assert_eq!(
book.price_at_depth(Side::Buy, Quantity(60)),
Some(Price(99))
);
}
#[test]
fn price_at_depth_spans_multiple_levels_sell() {
let book = basic_book();
assert_eq!(
book.price_at_depth(Side::Sell, Quantity(90)),
Some(Price(102))
);
}
#[test]
fn price_at_depth_exceeds_total_returns_none() {
let book = basic_book();
assert!(book.price_at_depth(Side::Buy, Quantity(81)).is_none());
assert!(book.price_at_depth(Side::Sell, Quantity(101)).is_none());
}
#[test]
fn price_at_depth_with_primary_peg() {
let book = book_with_pegs();
assert_eq!(
book.price_at_depth(Side::Buy, Quantity(20)),
Some(Price(100))
);
assert_eq!(
book.price_at_depth(Side::Buy, Quantity(70)),
Some(Price(100))
);
assert_eq!(
book.price_at_depth(Side::Buy, Quantity(71)),
Some(Price(99))
);
}
#[test]
fn price_at_depth_with_mid_price_peg_active() {
let book = book_with_mid_price_pegs();
assert_eq!(
book.price_at_depth(Side::Buy, Quantity(35)),
Some(Price(100))
);
assert_eq!(
book.price_at_depth(Side::Sell, Quantity(75)),
Some(Price(101))
);
}
#[test]
fn price_at_depth_mid_price_peg_inactive_wide_spread() {
let mut book = OrderBook::new("TEST");
book.add_limit_order(SequenceNumber(1), OrderId(1), standard(100, 50, Side::Buy));
book.add_limit_order(SequenceNumber(2), OrderId(2), standard(102, 40, Side::Sell)); book.add_pegged_order(
SequenceNumber(10),
OrderId(10),
pegged(PegReference::MidPrice, 100, Side::Buy),
);
assert_eq!(
book.price_at_depth(Side::Buy, Quantity(50)),
Some(Price(100))
);
assert!(book.price_at_depth(Side::Buy, Quantity(51)).is_none());
}
#[test]
fn vwap_zero_quantity() {
let book = basic_book();
assert!(book.vwap(Side::Buy, Quantity(0)).is_none());
}
#[test]
fn vwap_empty_book() {
assert!(empty_book().vwap(Side::Buy, Quantity(10)).is_none());
assert!(empty_book().vwap(Side::Sell, Quantity(10)).is_none());
}
#[test]
fn vwap_single_level_buy() {
let book = basic_book();
let v = book.vwap(Side::Buy, Quantity(40)).unwrap();
assert!((v - 101.0).abs() < EPS);
}
#[test]
fn vwap_single_level_sell() {
let book = basic_book();
let v = book.vwap(Side::Sell, Quantity(50)).unwrap();
assert!((v - 100.0).abs() < EPS);
}
#[test]
fn vwap_spans_multiple_levels_buy() {
let book = basic_book();
let v = book.vwap(Side::Buy, Quantity(70)).unwrap();
assert!((v - 7100.0 / 70.0).abs() < EPS);
}
#[test]
fn vwap_spans_multiple_levels_sell() {
let book = basic_book();
let v = book.vwap(Side::Sell, Quantity(70)).unwrap();
assert!((v - 6980.0 / 70.0).abs() < EPS);
}
#[test]
fn vwap_exceeds_liquidity() {
let book = basic_book();
assert!(book.vwap(Side::Buy, Quantity(101)).is_none());
assert!(book.vwap(Side::Sell, Quantity(81)).is_none());
}
#[test]
fn vwap_with_primary_peg_buy() {
let book = book_with_pegs();
let v = book.vwap(Side::Buy, Quantity(25)).unwrap();
assert!((v - 101.0).abs() < EPS);
let v = book.vwap(Side::Buy, Quantity(65)).unwrap();
assert!((v - 101.0).abs() < EPS);
let v = book.vwap(Side::Buy, Quantity(80)).unwrap();
assert!((v - 8095.0 / 80.0).abs() < EPS);
}
#[test]
fn vwap_with_primary_peg_sell() {
let book = book_with_pegs();
let v = book.vwap(Side::Sell, Quantity(20)).unwrap();
assert!((v - 100.0).abs() < EPS);
let v = book.vwap(Side::Sell, Quantity(70)).unwrap();
assert!((v - 100.0).abs() < EPS);
}
#[test]
fn vwap_with_mid_price_peg_active() {
let book = book_with_mid_price_pegs();
let v = book.vwap(Side::Buy, Quantity(10)).unwrap();
assert!((v - 101.0).abs() < EPS);
let v = book.vwap(Side::Buy, Quantity(35)).unwrap();
assert!((v - 101.0).abs() < EPS);
}
#[test]
fn market_impact_zero_quantity() {
let book = basic_book();
let impact = book.market_impact(Side::Buy, Quantity(0));
assert_eq!(impact.requested_quantity(), Quantity(0));
assert_eq!(impact.available_quantity(), Quantity(0));
assert_eq!(impact.consumed_price_levels(), 0);
}
#[test]
fn market_impact_empty_book() {
let impact = empty_book().market_impact(Side::Buy, Quantity(100));
assert_eq!(impact.requested_quantity(), Quantity(100));
assert_eq!(impact.available_quantity(), Quantity(0));
assert_eq!(impact.best_price(), Price(0));
assert_eq!(impact.worst_price(), Price(0));
assert_eq!(impact.consumed_price_levels(), 0);
}
#[test]
fn market_impact_single_level_buy() {
let book = basic_book();
let impact = book.market_impact(Side::Buy, Quantity(40));
assert_eq!(impact.requested_quantity(), Quantity(40));
assert_eq!(impact.available_quantity(), Quantity(40));
assert_eq!(impact.best_price(), Price(101));
assert_eq!(impact.worst_price(), Price(101));
assert_eq!(impact.consumed_price_levels(), 1);
assert_eq!(impact.slippage(), 0);
assert!((impact.average_price() - 101.0).abs() < EPS);
}
#[test]
fn market_impact_single_level_sell() {
let book = basic_book();
let impact = book.market_impact(Side::Sell, Quantity(50));
assert_eq!(impact.requested_quantity(), Quantity(50));
assert_eq!(impact.available_quantity(), Quantity(50));
assert_eq!(impact.best_price(), Price(100));
assert_eq!(impact.worst_price(), Price(100));
assert_eq!(impact.consumed_price_levels(), 1);
assert_eq!(impact.slippage(), 0);
}
#[test]
fn market_impact_multi_level_buy() {
let book = basic_book();
let impact = book.market_impact(Side::Buy, Quantity(70));
assert_eq!(impact.available_quantity(), Quantity(70));
assert_eq!(impact.best_price(), Price(101));
assert_eq!(impact.worst_price(), Price(102));
assert_eq!(impact.consumed_price_levels(), 2);
assert_eq!(impact.slippage(), 1);
assert!((impact.average_price() - 7100.0 / 70.0).abs() < EPS);
}
#[test]
fn market_impact_multi_level_sell() {
let book = basic_book();
let impact = book.market_impact(Side::Sell, Quantity(70));
assert_eq!(impact.available_quantity(), Quantity(70));
assert_eq!(impact.best_price(), Price(100));
assert_eq!(impact.worst_price(), Price(99));
assert_eq!(impact.consumed_price_levels(), 2);
assert_eq!(impact.slippage(), 1);
}
#[test]
fn market_impact_partial_fill() {
let book = basic_book();
let impact = book.market_impact(Side::Buy, Quantity(150));
assert_eq!(impact.requested_quantity(), Quantity(150));
assert_eq!(impact.available_quantity(), Quantity(100));
assert_eq!(impact.consumed_price_levels(), 2);
}
#[test]
fn market_impact_with_primary_peg() {
let book = book_with_pegs();
let impact = book.market_impact(Side::Buy, Quantity(25));
assert_eq!(impact.available_quantity(), Quantity(25));
assert_eq!(impact.best_price(), Price(101));
assert_eq!(impact.worst_price(), Price(101));
assert_eq!(impact.consumed_price_levels(), 1);
let impact = book.market_impact(Side::Buy, Quantity(65));
assert_eq!(impact.available_quantity(), Quantity(65));
assert_eq!(impact.consumed_price_levels(), 1);
assert_eq!(impact.worst_price(), Price(101));
}
#[test]
fn market_impact_with_mid_price_peg_active() {
let book = book_with_mid_price_pegs();
let impact = book.market_impact(Side::Sell, Quantity(15));
assert_eq!(impact.available_quantity(), Quantity(15));
assert_eq!(impact.best_price(), Price(100));
assert_eq!(impact.worst_price(), Price(100));
assert_eq!(impact.consumed_price_levels(), 1);
let impact = book.market_impact(Side::Sell, Quantity(85));
assert_eq!(impact.available_quantity(), Quantity(85));
assert_eq!(impact.consumed_price_levels(), 1);
assert_eq!(impact.worst_price(), Price(100));
}
#[test]
fn market_impact_slippage_across_levels() {
let book = basic_book();
let impact = book.market_impact(Side::Buy, Quantity(100));
assert_eq!(impact.slippage(), 1); let impact = book.market_impact(Side::Sell, Quantity(80));
assert_eq!(impact.slippage(), 1); }
}