use crate::{OrderBook, Price, Quantity, Timestamp};
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BookSnapshot {
pub bids: Vec<LevelSnapshot>,
pub asks: Vec<LevelSnapshot>,
pub timestamp: Timestamp,
}
impl BookSnapshot {
pub fn best_bid(&self) -> Option<Price> {
self.bids.first().map(|l| l.price)
}
pub fn best_ask(&self) -> Option<Price> {
self.asks.first().map(|l| l.price)
}
pub fn spread(&self) -> Option<i64> {
match (self.best_bid(), self.best_ask()) {
(Some(bid), Some(ask)) => Some(ask.0 - bid.0),
_ => None,
}
}
pub fn mid_price(&self) -> Option<f64> {
match (self.best_bid(), self.best_ask()) {
(Some(bid), Some(ask)) => Some((bid.0 + ask.0) as f64 / 2.0),
_ => None,
}
}
pub fn total_bid_quantity(&self) -> Quantity {
self.bids.iter().map(|l| l.quantity).sum()
}
pub fn total_ask_quantity(&self) -> Quantity {
self.asks.iter().map(|l| l.quantity).sum()
}
pub fn imbalance(&self) -> Option<f64> {
let bid_qty = self.total_bid_quantity();
let ask_qty = self.total_ask_quantity();
let total = bid_qty + ask_qty;
if total == 0 {
return None;
}
Some((bid_qty as f64 - ask_qty as f64) / total as f64)
}
pub fn weighted_mid(&self) -> Option<f64> {
let bid = self.bids.first()?;
let ask = self.asks.first()?;
let total = bid.quantity + ask.quantity;
if total == 0 {
return None;
}
Some(
(ask.quantity as f64 * bid.price.0 as f64 + bid.quantity as f64 * ask.price.0 as f64)
/ total as f64,
)
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LevelSnapshot {
pub price: Price,
pub quantity: Quantity,
pub order_count: usize,
}
impl OrderBook {
pub fn snapshot(&self, depth: usize) -> BookSnapshot {
fn snapshot_levels(levels: &crate::PriceLevels, depth: usize) -> Vec<LevelSnapshot> {
levels
.iter_best_to_worst()
.take(depth)
.map(|(price, level)| LevelSnapshot {
price: *price,
quantity: level.total_quantity(),
order_count: level.order_count(),
})
.collect()
}
BookSnapshot {
bids: snapshot_levels(self.bids(), depth),
asks: snapshot_levels(self.asks(), depth),
timestamp: self.peek_next_order_id().0,
}
}
pub fn full_snapshot(&self) -> BookSnapshot {
self.snapshot(usize::MAX)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Side, TimeInForce};
#[test]
fn empty_snapshot() {
let book = OrderBook::new();
let snap = book.snapshot(10);
assert!(snap.bids.is_empty());
assert!(snap.asks.is_empty());
assert_eq!(snap.best_bid(), None);
assert_eq!(snap.best_ask(), None);
assert_eq!(snap.spread(), None);
assert_eq!(snap.mid_price(), None);
}
#[test]
fn snapshot_with_orders() {
let mut book = OrderBook::new();
let b1 = book.create_order(Side::Buy, Price(100_00), 100, TimeInForce::GTC);
let b2 = book.create_order(Side::Buy, Price(100_00), 50, TimeInForce::GTC);
let b3 = book.create_order(Side::Buy, Price(99_00), 200, TimeInForce::GTC);
book.add_order(b1);
book.add_order(b2);
book.add_order(b3);
let a1 = book.create_order(Side::Sell, Price(101_00), 75, TimeInForce::GTC);
let a2 = book.create_order(Side::Sell, Price(102_00), 150, TimeInForce::GTC);
book.add_order(a1);
book.add_order(a2);
let snap = book.snapshot(10);
assert_eq!(snap.bids.len(), 2);
assert_eq!(snap.bids[0].price, Price(100_00));
assert_eq!(snap.bids[0].quantity, 150); assert_eq!(snap.bids[0].order_count, 2);
assert_eq!(snap.bids[1].price, Price(99_00));
assert_eq!(snap.bids[1].quantity, 200);
assert_eq!(snap.asks.len(), 2);
assert_eq!(snap.asks[0].price, Price(101_00));
assert_eq!(snap.asks[0].quantity, 75);
assert_eq!(snap.asks[1].price, Price(102_00));
assert_eq!(snap.best_bid(), Some(Price(100_00)));
assert_eq!(snap.best_ask(), Some(Price(101_00)));
assert_eq!(snap.spread(), Some(100)); assert_eq!(snap.mid_price(), Some(100_50.0));
assert_eq!(snap.total_bid_quantity(), 350);
assert_eq!(snap.total_ask_quantity(), 225);
}
#[test]
fn snapshot_depth_limit() {
let mut book = OrderBook::new();
for i in 0..5 {
let order =
book.create_order(Side::Buy, Price(100_00 - i * 100), 100, TimeInForce::GTC);
book.add_order(order);
}
let snap = book.snapshot(3);
assert_eq!(snap.bids.len(), 3);
assert_eq!(snap.bids[0].price, Price(100_00));
assert_eq!(snap.bids[1].price, Price(99_00));
assert_eq!(snap.bids[2].price, Price(98_00));
}
#[test]
fn full_snapshot() {
let mut book = OrderBook::new();
for i in 0..10 {
let order =
book.create_order(Side::Buy, Price(100_00 - i * 100), 100, TimeInForce::GTC);
book.add_order(order);
}
let snap = book.full_snapshot();
assert_eq!(snap.bids.len(), 10);
}
#[test]
fn imbalance_balanced() {
let mut book = OrderBook::new();
let b = book.create_order(Side::Buy, Price(100_00), 100, TimeInForce::GTC);
let a = book.create_order(Side::Sell, Price(101_00), 100, TimeInForce::GTC);
book.add_order(b);
book.add_order(a);
let snap = book.snapshot(10);
let imb = snap.imbalance().unwrap();
assert!((imb).abs() < 1e-10); }
#[test]
fn imbalance_bid_heavy() {
let mut book = OrderBook::new();
let b = book.create_order(Side::Buy, Price(100_00), 300, TimeInForce::GTC);
let a = book.create_order(Side::Sell, Price(101_00), 100, TimeInForce::GTC);
book.add_order(b);
book.add_order(a);
let snap = book.snapshot(10);
let imb = snap.imbalance().unwrap();
assert!((imb - 0.5).abs() < 1e-10);
}
#[test]
fn imbalance_empty() {
let book = OrderBook::new();
let snap = book.snapshot(10);
assert!(snap.imbalance().is_none());
}
#[test]
fn weighted_mid_equal_qty() {
let mut book = OrderBook::new();
let b = book.create_order(Side::Buy, Price(100_00), 100, TimeInForce::GTC);
let a = book.create_order(Side::Sell, Price(102_00), 100, TimeInForce::GTC);
book.add_order(b);
book.add_order(a);
let snap = book.snapshot(10);
let wmid = snap.weighted_mid().unwrap();
assert!((wmid - 101_00.0).abs() < 1e-10);
}
#[test]
fn weighted_mid_skewed() {
let mut book = OrderBook::new();
let b = book.create_order(Side::Buy, Price(100_00), 300, TimeInForce::GTC);
let a = book.create_order(Side::Sell, Price(102_00), 100, TimeInForce::GTC);
book.add_order(b);
book.add_order(a);
let snap = book.snapshot(10);
let wmid = snap.weighted_mid().unwrap();
assert!((wmid - 101_50.0).abs() < 1e-10);
}
#[test]
fn weighted_mid_empty() {
let book = OrderBook::new();
let snap = book.snapshot(10);
assert!(snap.weighted_mid().is_none());
}
}