use crate::error::FinError;
use crate::types::{Price, Quantity, Side, Symbol};
use rust_decimal::Decimal;
use std::collections::BTreeMap;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PriceLevel {
pub price: Price,
pub quantity: Quantity,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum DeltaAction {
Set,
Remove,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BookDelta {
pub side: Side,
pub price: Price,
pub quantity: Quantity,
pub action: DeltaAction,
pub sequence: u64,
}
#[derive(Debug, Clone)]
pub struct OrderBook {
pub symbol: Symbol,
bids: BTreeMap<Decimal, Decimal>,
asks: BTreeMap<Decimal, Decimal>,
sequence: u64,
}
impl OrderBook {
pub fn new(symbol: Symbol) -> Self {
Self {
symbol,
bids: BTreeMap::new(),
asks: BTreeMap::new(),
sequence: 0,
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn apply_delta(&mut self, delta: BookDelta) -> Result<(), FinError> {
let expected = self.sequence + 1;
if delta.sequence != expected {
return Err(FinError::SequenceMismatch {
expected,
got: delta.sequence,
});
}
let prev_val = match delta.side {
Side::Bid => self.bids.get(&delta.price.value()).copied(),
Side::Ask => self.asks.get(&delta.price.value()).copied(),
};
let book_side = match delta.side {
Side::Bid => &mut self.bids,
Side::Ask => &mut self.asks,
};
match delta.action {
DeltaAction::Set => {
book_side.insert(delta.price.value(), delta.quantity.value());
}
DeltaAction::Remove => {
book_side.remove(&delta.price.value());
}
}
self.sequence = delta.sequence;
let maybe_inversion = {
let best_bid_p = self.bids.keys().next_back().copied();
let best_ask_p = self.asks.keys().next().copied();
match (best_bid_p, best_ask_p) {
(Some(b), Some(a)) if b >= a => Some((b, a)),
_ => None,
}
};
if let Some((best_bid_p, best_ask_p)) = maybe_inversion {
match delta.action {
DeltaAction::Set => match delta.side {
Side::Bid => {
self.bids.remove(&delta.price.value());
}
Side::Ask => {
self.asks.remove(&delta.price.value());
}
},
DeltaAction::Remove => match delta.side {
Side::Bid => {
if let Some(qty) = prev_val {
self.bids.insert(delta.price.value(), qty);
}
}
Side::Ask => {
if let Some(qty) = prev_val {
self.asks.insert(delta.price.value(), qty);
}
}
},
}
self.sequence = expected - 1;
return Err(FinError::InvertedSpread {
best_bid: best_bid_p,
best_ask: best_ask_p,
});
}
Ok(())
}
pub fn best_bid(&self) -> Option<PriceLevel> {
self.bids.iter().next_back().and_then(|(p, q)| {
Some(PriceLevel {
price: Price::new(*p).ok()?,
quantity: Quantity::new(*q).unwrap_or_else(|_| Quantity::zero()),
})
})
}
pub fn best_quote(&self) -> Option<(PriceLevel, PriceLevel)> {
Some((self.best_bid()?, self.best_ask()?))
}
pub fn best_ask(&self) -> Option<PriceLevel> {
self.asks.iter().next().and_then(|(p, q)| {
Some(PriceLevel {
price: Price::new(*p).ok()?,
quantity: Quantity::new(*q).unwrap_or_else(|_| Quantity::zero()),
})
})
}
pub fn mid_price(&self) -> Option<Decimal> {
let bid = self.best_bid()?.price.value();
let ask = self.best_ask()?.price.value();
Some((bid + ask) / Decimal::TWO)
}
pub fn spread(&self) -> Option<Decimal> {
let bid = self.best_bid()?.price.value();
let ask = self.best_ask()?.price.value();
Some(ask - bid)
}
pub fn spread_pct(&self) -> Option<Decimal> {
let mid = self.mid_price()?;
if mid.is_zero() {
return None;
}
let spread = self.spread()?;
Some(spread / mid * Decimal::ONE_HUNDRED)
}
pub fn depth_at(&self, side: Side, price: Price) -> Option<Decimal> {
let key = price.value();
match side {
Side::Bid => self.bids.get(&key).copied(),
Side::Ask => self.asks.get(&key).copied(),
}
}
pub fn top_bids(&self, n: usize) -> Vec<PriceLevel> {
self.bids
.iter()
.rev()
.take(n)
.filter_map(|(p, q)| {
let price = Price::new(*p).ok()?;
let quantity = Quantity::new(*q).ok()?;
Some(PriceLevel { price, quantity })
})
.collect()
}
pub fn top_asks(&self, n: usize) -> Vec<PriceLevel> {
self.asks
.iter()
.take(n)
.filter_map(|(p, q)| {
let price = Price::new(*p).ok()?;
let quantity = Quantity::new(*q).ok()?;
Some(PriceLevel { price, quantity })
})
.collect()
}
pub fn vwap_for_qty(&self, side: Side, qty: Quantity) -> Result<Decimal, FinError> {
let target = qty.value();
if target <= Decimal::ZERO {
return Ok(Decimal::ZERO);
}
match side {
Side::Bid => Self::vwap_fill(self.bids.iter().rev(), target),
Side::Ask => Self::vwap_fill(self.asks.iter(), target),
}
}
fn vwap_fill<'a>(
levels: impl Iterator<Item = (&'a Decimal, &'a Decimal)>,
target: Decimal,
) -> Result<Decimal, FinError> {
let mut remaining = target;
let mut total_cost = Decimal::ZERO;
for (price, avail_qty) in levels {
let fill = remaining.min(*avail_qty);
total_cost += fill * price;
remaining -= fill;
if remaining <= Decimal::ZERO {
break;
}
}
if remaining > Decimal::ZERO {
return Err(FinError::InsufficientLiquidity(target));
}
Ok(total_cost / target)
}
pub fn sequence(&self) -> u64 {
self.sequence
}
pub fn snapshot(&self, n: usize) -> (Vec<PriceLevel>, Vec<PriceLevel>) {
(self.top_bids(n), self.top_asks(n))
}
pub fn bid_count(&self) -> usize {
self.bids.len()
}
pub fn ask_count(&self) -> usize {
self.asks.len()
}
pub fn level_count(&self, side: Side) -> usize {
match side {
Side::Bid => self.bids.len(),
Side::Ask => self.asks.len(),
}
}
pub fn clear(&mut self) {
self.bids.clear();
self.asks.clear();
self.sequence = 0;
}
pub fn remove_all(&mut self, side: crate::types::Side) {
use crate::types::Side;
match side {
Side::Bid => self.bids.clear(),
Side::Ask => self.asks.clear(),
}
}
pub fn is_crossed(&self) -> bool {
match (self.best_bid(), self.best_ask()) {
(Some(bid), Some(ask)) => bid.price >= ask.price,
_ => false,
}
}
pub fn is_empty(&self) -> bool {
self.bids.is_empty() && self.asks.is_empty()
}
pub fn total_levels(&self) -> usize {
self.bids.len() + self.asks.len()
}
pub fn cumulative_depth(&self, side: Side, price: Price) -> Decimal {
let p = price.value();
match side {
Side::Bid => self
.bids
.range(p..)
.map(|(_, qty)| *qty)
.sum(),
Side::Ask => self
.asks
.range(..=p)
.map(|(_, qty)| *qty)
.sum(),
}
}
pub fn total_bid_volume(&self) -> Decimal {
self.bids.values().copied().sum()
}
pub fn total_ask_volume(&self) -> Decimal {
self.asks.values().copied().sum()
}
pub fn best_bid_price(&self) -> Option<Price> {
self.bids.keys().next_back().and_then(|p| Price::new(*p).ok())
}
pub fn best_ask_price(&self) -> Option<Price> {
self.asks.keys().next().and_then(|p| Price::new(*p).ok())
}
pub fn best_bid_qty(&self) -> Option<Quantity> {
self.bids
.values()
.next_back()
.and_then(|q| Quantity::new(*q).ok())
}
pub fn best_ask_qty(&self) -> Option<Quantity> {
self.asks
.values()
.next()
.and_then(|q| Quantity::new(*q).ok())
}
pub fn liquidity_at_pct(&self, side: Side, pct_from_mid: Decimal) -> Option<Decimal> {
let mid = self.mid_price()?;
let band = mid * pct_from_mid / Decimal::ONE_HUNDRED;
let (lo, hi) = match side {
Side::Bid => (mid - band, mid),
Side::Ask => (mid, mid + band),
};
let qty: Decimal = match side {
Side::Bid => self
.bids
.range(lo..=hi)
.map(|(_, q)| *q)
.sum(),
Side::Ask => self
.asks
.range(lo..=hi)
.map(|(_, q)| *q)
.sum(),
};
Some(qty)
}
pub fn has_price(&self, side: Side, price: Price) -> bool {
let key = price.value();
match side {
Side::Bid => self.bids.contains_key(&key),
Side::Ask => self.asks.contains_key(&key),
}
}
pub fn weighted_mid(&self) -> Option<Decimal> {
let bid = self.best_bid()?;
let ask = self.best_ask()?;
let bid_qty = bid.quantity.value();
let ask_qty = ask.quantity.value();
let total = bid_qty + ask_qty;
if total.is_zero() {
return None;
}
Some((bid.price.value() * ask_qty + ask.price.value() * bid_qty) / total)
}
pub fn imbalance(&self) -> Option<Decimal> {
let bid_vol = self.total_bid_volume();
let ask_vol = self.total_ask_volume();
let total = bid_vol + ask_vol;
if total == Decimal::ZERO {
return None;
}
Some((bid_vol - ask_vol) / total)
}
pub fn depth_ratio(&self, n: usize) -> Option<Decimal> {
let bid_vol: Decimal = self.bids.values().rev().take(n).copied().sum();
let ask_vol: Decimal = self.asks.values().take(n).copied().sum();
if ask_vol.is_zero() {
return None;
}
Some(bid_vol / ask_vol)
}
#[deprecated(since = "2.1.0", note = "Use `weighted_mid` instead")]
pub fn weighted_mid_price(&self) -> Option<Decimal> {
self.weighted_mid()
}
pub fn price_levels_between(&self, side: Side, lo: Price, hi: Price) -> Vec<PriceLevel> {
let lo_val = lo.value();
let hi_val = hi.value();
match side {
Side::Bid => self
.bids
.range(lo_val..=hi_val)
.map(|(p, q)| PriceLevel {
price: Price::new(*p).unwrap_or(lo),
quantity: crate::types::Quantity::new(*q).unwrap_or_else(|_| crate::types::Quantity::zero()),
})
.collect(),
Side::Ask => self
.asks
.range(lo_val..=hi_val)
.map(|(p, q)| PriceLevel {
price: Price::new(*p).unwrap_or(lo),
quantity: crate::types::Quantity::new(*q).unwrap_or_else(|_| crate::types::Quantity::zero()),
})
.collect(),
}
}
pub fn tick_size(&self) -> Option<Decimal> {
let bid_tick = self
.bids
.keys()
.collect::<Vec<_>>()
.windows(2)
.map(|w| (*w[1] - *w[0]).abs())
.filter(|d| !d.is_zero())
.reduce(Decimal::min);
let ask_tick = self
.asks
.keys()
.collect::<Vec<_>>()
.windows(2)
.map(|w| (*w[1] - *w[0]).abs())
.filter(|d| !d.is_zero())
.reduce(Decimal::min);
match (bid_tick, ask_tick) {
(Some(b), Some(a)) => Some(b.min(a)),
(Some(b), None) => Some(b),
(None, Some(a)) => Some(a),
(None, None) => None,
}
}
pub fn bid_ask_ratio(&self) -> Option<Decimal> {
let bid = self.total_bid_volume();
let ask = self.total_ask_volume();
if ask.is_zero() || bid.is_zero() {
return None;
}
Some(bid / ask)
}
pub fn price_impact(&self, side: crate::types::Side, qty: crate::types::Quantity) -> Option<Decimal> {
use crate::types::Side;
if qty.is_zero() {
return None;
}
let levels: Vec<_> = match side {
Side::Bid => {
let mut asks: Vec<_> = self.asks.iter().collect();
asks.sort_by(|a, b| a.0.cmp(b.0));
asks.into_iter().map(|(p, q)| (*p, *q)).collect()
}
Side::Ask => {
let mut bids: Vec<_> = self.bids.iter().collect();
bids.sort_by(|a, b| b.0.cmp(a.0));
bids.into_iter().map(|(p, q)| (*p, *q)).collect()
}
};
let target = qty.value();
let mut remaining = target;
let mut notional = Decimal::ZERO;
for (price, level_qty) in levels {
let fill = level_qty.min(remaining);
notional += price * fill;
remaining -= fill;
if remaining <= Decimal::ZERO {
break;
}
}
if remaining > Decimal::ZERO {
None } else {
Some(notional / target)
}
}
pub fn bid_depth(&self, n: usize) -> Vec<PriceLevel> {
self.bids
.iter()
.rev()
.take(n)
.map(|(price, qty)| PriceLevel {
price: Price::new(*price).unwrap(),
quantity: Quantity::new(*qty).unwrap(),
})
.collect()
}
pub fn ask_depth(&self, n: usize) -> Vec<PriceLevel> {
self.asks
.iter()
.take(n)
.map(|(price, qty)| PriceLevel {
price: Price::new(*price).unwrap(),
quantity: Quantity::new(*qty).unwrap(),
})
.collect()
}
pub fn depth_imbalance(&self) -> Option<Decimal> {
let bid_qty: Decimal = self.bids.values().sum();
let ask_qty: Decimal = self.asks.values().sum();
let total = bid_qty + ask_qty;
if total.is_zero() {
return None;
}
Some((bid_qty - ask_qty) / total)
}
pub fn ask_bid_ratio(&self) -> Option<Decimal> {
let bid_qty: Decimal = self.bids.values().sum();
let ask_qty: Decimal = self.asks.values().sum();
if bid_qty.is_zero() {
return None;
}
Some(ask_qty / bid_qty)
}
pub fn total_bid_depth(&self) -> Decimal {
self.bids.values().sum()
}
pub fn total_ask_depth(&self) -> Decimal {
self.asks.values().sum()
}
pub fn price_at_volume(&self, side: Side, target_qty: Decimal) -> Option<Price> {
if target_qty.is_zero() {
return None;
}
let mut remaining = target_qty;
let mut last_price: Option<Price> = None;
match side {
Side::Ask => {
for (&px, &qty) in &self.asks {
last_price = Price::new(px).ok();
if qty >= remaining {
return last_price;
}
remaining -= qty;
}
}
Side::Bid => {
for (&px, &qty) in self.bids.iter().rev() {
last_price = Price::new(px).ok();
if qty >= remaining {
return last_price;
}
remaining -= qty;
}
}
}
last_price
}
pub fn top_n_bid_levels(&self, n: usize) -> Vec<PriceLevel> {
if n == 0 {
return vec![];
}
self.bids
.iter()
.rev()
.take(n)
.filter_map(|(&px, &qty)| {
let price = Price::new(px).ok()?;
let quantity = Quantity::new(qty).ok()?;
Some(PriceLevel { price, quantity })
})
.collect()
}
pub fn top_n_ask_levels(&self, n: usize) -> Vec<PriceLevel> {
if n == 0 {
return vec![];
}
self.asks
.iter()
.take(n)
.filter_map(|(&px, &qty)| {
let price = Price::new(px).ok()?;
let quantity = Quantity::new(qty).ok()?;
Some(PriceLevel { price, quantity })
})
.collect()
}
pub fn cumulative_bid_qty(&self, n: usize) -> Decimal {
if n == 0 {
return Decimal::ZERO;
}
self.bids.iter().rev().take(n).map(|(_, &qty)| qty).sum()
}
pub fn bid_depth_skew(&self, n: usize) -> Option<Decimal> {
if n == 0 {
return None;
}
let bid_qty = self.cumulative_bid_qty(n);
let ask_qty = self.cumulative_ask_qty(n);
let total = bid_qty + ask_qty;
if total.is_zero() {
return None;
}
bid_qty.checked_div(total)
}
pub fn spread_bps(&self) -> Option<Decimal> {
let bid = self.best_bid()?.price.value();
let ask = self.best_ask()?.price.value();
let mid = (bid + ask) / Decimal::TWO;
if mid.is_zero() {
return None;
}
let spread = ask - bid;
spread.checked_div(mid).map(|r| r * Decimal::from(10_000u32))
}
pub fn cumulative_ask_qty(&self, n: usize) -> Decimal {
if n == 0 {
return Decimal::ZERO;
}
self.asks.iter().take(n).map(|(_, &qty)| qty).sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn make_book() -> OrderBook {
OrderBook::new(Symbol::new("AAPL").unwrap())
}
fn set_delta(side: Side, price: &str, qty: &str, seq: u64) -> BookDelta {
BookDelta {
side,
price: Price::new(price.parse().unwrap()).unwrap(),
quantity: Quantity::new(qty.parse().unwrap()).unwrap(),
action: DeltaAction::Set,
sequence: seq,
}
}
fn remove_delta(side: Side, price: &str, seq: u64) -> BookDelta {
BookDelta {
side,
price: Price::new(price.parse().unwrap()).unwrap(),
quantity: Quantity::zero(),
action: DeltaAction::Remove,
sequence: seq,
}
}
#[test]
fn test_orderbook_apply_delta_updates_bid() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
.unwrap();
let best = book.best_bid().unwrap();
assert_eq!(best.price.value(), dec!(100));
assert_eq!(best.quantity.value(), dec!(10));
}
#[test]
fn test_orderbook_apply_delta_updates_ask() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "101", "5", 1))
.unwrap();
let best = book.best_ask().unwrap();
assert_eq!(best.price.value(), dec!(101));
assert_eq!(best.quantity.value(), dec!(5));
}
#[test]
fn test_orderbook_sequence_mismatch_returns_error() {
let mut book = make_book();
let result = book.apply_delta(set_delta(Side::Bid, "100", "10", 2));
assert!(matches!(
result,
Err(FinError::SequenceMismatch {
expected: 1,
got: 2
})
));
}
#[test]
fn test_orderbook_sequence_advances_correctly() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
.unwrap();
assert_eq!(book.sequence(), 1);
book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
.unwrap();
assert_eq!(book.sequence(), 2);
}
#[test]
fn test_orderbook_best_bid_max_price() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "99", "10", 1))
.unwrap();
book.apply_delta(set_delta(Side::Bid, "100", "5", 2))
.unwrap();
book.apply_delta(set_delta(Side::Bid, "98", "20", 3))
.unwrap();
let best = book.best_bid().unwrap();
assert_eq!(best.price.value(), dec!(100));
}
#[test]
fn test_orderbook_best_ask_min_price() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "102", "10", 1))
.unwrap();
book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
.unwrap();
book.apply_delta(set_delta(Side::Ask, "103", "20", 3))
.unwrap();
let best = book.best_ask().unwrap();
assert_eq!(best.price.value(), dec!(101));
}
#[test]
fn test_orderbook_spread_positive() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
.unwrap();
book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
.unwrap();
let spread = book.spread().unwrap();
assert_eq!(spread, dec!(1));
assert!(spread > Decimal::ZERO);
}
#[test]
fn test_orderbook_mid_price() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
.unwrap();
book.apply_delta(set_delta(Side::Ask, "102", "5", 2))
.unwrap();
let mid = book.mid_price().unwrap();
assert_eq!(mid, dec!(101));
}
#[test]
fn test_orderbook_spread_none_when_empty() {
let book = make_book();
assert!(book.spread().is_none());
}
#[test]
fn test_orderbook_vwap_insufficient_liquidity() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "101", "5", 1))
.unwrap();
let result = book.vwap_for_qty(Side::Ask, Quantity::new(dec!(100)).unwrap());
assert!(matches!(result, Err(FinError::InsufficientLiquidity(_))));
}
#[test]
fn test_orderbook_vwap_single_level() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "100", "10", 1))
.unwrap();
let vwap = book
.vwap_for_qty(Side::Ask, Quantity::new(dec!(5)).unwrap())
.unwrap();
assert_eq!(vwap, dec!(100));
}
#[test]
fn test_orderbook_vwap_multi_level() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
.unwrap();
book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
.unwrap();
let vwap = book
.vwap_for_qty(Side::Ask, Quantity::new(dec!(10)).unwrap())
.unwrap();
assert_eq!(vwap, dec!(100.5));
}
#[test]
fn test_orderbook_remove_level_delta() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
.unwrap();
book.apply_delta(remove_delta(Side::Bid, "100", 2)).unwrap();
assert!(book.best_bid().is_none());
}
#[test]
fn test_orderbook_top_bids_order() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "98", "10", 1))
.unwrap();
book.apply_delta(set_delta(Side::Bid, "100", "5", 2))
.unwrap();
book.apply_delta(set_delta(Side::Bid, "99", "20", 3))
.unwrap();
let top = book.top_bids(2);
assert_eq!(top[0].price.value(), dec!(100));
assert_eq!(top[1].price.value(), dec!(99));
}
#[test]
fn test_orderbook_top_asks_order() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "103", "10", 1))
.unwrap();
book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
.unwrap();
book.apply_delta(set_delta(Side::Ask, "102", "20", 3))
.unwrap();
let top = book.top_asks(2);
assert_eq!(top[0].price.value(), dec!(101));
assert_eq!(top[1].price.value(), dec!(102));
}
#[test]
fn test_orderbook_bid_count_ask_count() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "1", 1))
.unwrap();
book.apply_delta(set_delta(Side::Ask, "101", "1", 2))
.unwrap();
assert_eq!(book.bid_count(), 1);
assert_eq!(book.ask_count(), 1);
}
#[test]
fn test_orderbook_vwap_zero_qty_returns_zero() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "100", "10", 1))
.unwrap();
let vwap = book.vwap_for_qty(Side::Ask, Quantity::zero()).unwrap();
assert_eq!(vwap, Decimal::ZERO);
}
#[test]
fn test_apply_delta_rejects_inverted_spread() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
.unwrap();
let result = book.apply_delta(set_delta(Side::Bid, "101", "5", 2));
assert!(
matches!(result, Err(FinError::InvertedSpread { .. })),
"expected InvertedSpread, got {:?}",
result
);
}
#[test]
fn test_apply_delta_inverted_spread_rolls_back_sequence() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
.unwrap();
assert_eq!(book.sequence(), 1);
let _ = book.apply_delta(set_delta(Side::Bid, "101", "5", 2));
assert_eq!(
book.sequence(),
1,
"sequence must not advance on rejected delta"
);
}
#[test]
fn test_apply_delta_inverted_spread_rolled_back_book_state() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
.unwrap();
let _ = book.apply_delta(set_delta(Side::Bid, "101", "5", 2));
assert!(
book.best_bid().is_none(),
"rejected bid must not appear in book"
);
}
#[test]
fn test_empty_book_mid_price_returns_none() {
let book = make_book();
assert!(
book.mid_price().is_none(),
"empty book mid_price must be None"
);
}
#[test]
fn test_empty_book_best_bid_returns_none() {
let book = make_book();
assert!(book.best_bid().is_none());
}
#[test]
fn test_empty_book_best_ask_returns_none() {
let book = make_book();
assert!(book.best_ask().is_none());
}
#[test]
fn test_best_bid_after_many_inserts_and_removes() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
.unwrap();
book.apply_delta(set_delta(Side::Bid, "105", "5", 2))
.unwrap();
book.apply_delta(set_delta(Side::Bid, "103", "8", 3))
.unwrap();
book.apply_delta(remove_delta(Side::Bid, "105", 4)).unwrap();
let best = book.best_bid().unwrap();
assert_eq!(
best.price.value(),
dec!(103),
"best bid after removing top level must be 103"
);
}
#[test]
fn test_best_ask_after_many_inserts_and_removes() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "110", "10", 1))
.unwrap();
book.apply_delta(set_delta(Side::Ask, "108", "5", 2))
.unwrap();
book.apply_delta(set_delta(Side::Ask, "109", "8", 3))
.unwrap();
book.apply_delta(remove_delta(Side::Ask, "108", 4)).unwrap();
let best = book.best_ask().unwrap();
assert_eq!(
best.price.value(),
dec!(109),
"best ask after removing top level must be 109"
);
}
#[test]
fn test_crossed_book_ask_at_bid_price_rejected() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
.unwrap();
let result = book.apply_delta(set_delta(Side::Ask, "100", "5", 2));
assert!(
matches!(result, Err(FinError::InvertedSpread { .. })),
"ask at bid price must produce InvertedSpread"
);
}
#[test]
fn test_empty_book_spread_returns_none() {
let book = make_book();
assert!(book.spread().is_none());
}
#[test]
fn test_orderbook_snapshot_returns_top_n_both_sides() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "99", "10", 1)).unwrap();
book.apply_delta(set_delta(Side::Bid, "100", "5", 2)).unwrap();
book.apply_delta(set_delta(Side::Ask, "101", "3", 3)).unwrap();
book.apply_delta(set_delta(Side::Ask, "102", "7", 4)).unwrap();
let (bids, asks) = book.snapshot(2);
assert_eq!(bids.len(), 2);
assert_eq!(asks.len(), 2);
assert_eq!(bids[0].price.value(), dec!(100));
assert_eq!(asks[0].price.value(), dec!(101));
}
#[test]
fn test_orderbook_snapshot_empty_book() {
let book = make_book();
let (bids, asks) = book.snapshot(5);
assert!(bids.is_empty());
assert!(asks.is_empty());
}
#[test]
fn test_orderbook_clear_removes_all_levels() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "99", "10", 1)).unwrap();
book.apply_delta(set_delta(Side::Ask, "101", "5", 2)).unwrap();
assert_eq!(book.bid_count(), 1);
assert_eq!(book.ask_count(), 1);
book.clear();
assert_eq!(book.bid_count(), 0);
assert_eq!(book.ask_count(), 0);
assert_eq!(book.sequence(), 0);
}
#[test]
fn test_orderbook_clear_allows_fresh_deltas() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
book.clear();
assert!(book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).is_ok());
}
#[test]
fn test_orderbook_total_bid_volume() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
book.apply_delta(set_delta(Side::Bid, "99", "3", 2)).unwrap();
assert_eq!(book.total_bid_volume(), dec!(8));
}
#[test]
fn test_orderbook_total_ask_volume() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "101", "4", 1)).unwrap();
book.apply_delta(set_delta(Side::Ask, "102", "6", 2)).unwrap();
assert_eq!(book.total_ask_volume(), dec!(10));
}
#[test]
fn test_orderbook_total_bid_volume_empty() {
let book = make_book();
assert_eq!(book.total_bid_volume(), dec!(0));
}
#[test]
fn test_orderbook_imbalance_balanced() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
book.apply_delta(set_delta(Side::Ask, "101", "5", 2)).unwrap();
assert_eq!(book.imbalance().unwrap(), dec!(0));
}
#[test]
fn test_orderbook_imbalance_bid_heavy() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "9", 1)).unwrap();
book.apply_delta(set_delta(Side::Ask, "101", "1", 2)).unwrap();
assert_eq!(book.imbalance().unwrap(), dec!(0.8));
}
#[test]
fn test_orderbook_imbalance_ask_heavy() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "1", 1)).unwrap();
book.apply_delta(set_delta(Side::Ask, "101", "9", 2)).unwrap();
assert_eq!(book.imbalance().unwrap(), dec!(-0.8));
}
#[test]
fn test_orderbook_imbalance_empty_returns_none() {
let book = make_book();
assert!(book.imbalance().is_none());
}
#[test]
fn test_orderbook_has_price_bid_present() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
let price = Price::new(dec!(100)).unwrap();
assert!(book.has_price(Side::Bid, price));
assert!(!book.has_price(Side::Ask, price));
}
#[test]
fn test_orderbook_has_price_ask_present() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "101", "3", 1)).unwrap();
let price = Price::new(dec!(101)).unwrap();
assert!(book.has_price(Side::Ask, price));
assert!(!book.has_price(Side::Bid, price));
}
#[test]
fn test_orderbook_has_price_absent() {
let book = make_book();
let price = Price::new(dec!(100)).unwrap();
assert!(!book.has_price(Side::Bid, price));
assert!(!book.has_price(Side::Ask, price));
}
#[test]
fn test_orderbook_has_price_false_after_remove() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
book.apply_delta(BookDelta {
side: Side::Bid,
price: Price::new(dec!(100)).unwrap(),
quantity: Quantity::zero(),
action: DeltaAction::Remove,
sequence: 2,
})
.unwrap();
let price = Price::new(dec!(100)).unwrap();
assert!(!book.has_price(Side::Bid, price));
}
#[test]
fn test_orderbook_level_count_bids() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
assert_eq!(book.level_count(Side::Bid), 2);
assert_eq!(book.level_count(Side::Ask), 0);
}
#[test]
fn test_orderbook_level_count_asks() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "101", "3", 1)).unwrap();
assert_eq!(book.level_count(Side::Ask), 1);
assert_eq!(book.level_count(Side::Bid), 0);
}
#[test]
fn test_orderbook_weighted_mid_equal_qty() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
book.apply_delta(set_delta(Side::Ask, "102", "5", 2)).unwrap();
assert_eq!(book.weighted_mid().unwrap(), dec!(101));
}
#[test]
fn test_orderbook_weighted_mid_bid_heavy() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "9", 1)).unwrap();
book.apply_delta(set_delta(Side::Ask, "110", "1", 2)).unwrap();
assert_eq!(book.weighted_mid().unwrap(), dec!(109));
}
#[test]
fn test_orderbook_weighted_mid_empty_returns_none() {
let book = make_book();
assert!(book.weighted_mid().is_none());
}
#[test]
fn test_orderbook_bid_ask_ratio_equal_volumes() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
book.apply_delta(set_delta(Side::Ask, "101", "10", 2)).unwrap();
assert_eq!(book.bid_ask_ratio().unwrap(), dec!(1));
}
#[test]
fn test_orderbook_bid_ask_ratio_bid_heavy() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "20", 1)).unwrap();
book.apply_delta(set_delta(Side::Ask, "101", "10", 2)).unwrap();
assert_eq!(book.bid_ask_ratio().unwrap(), dec!(2));
}
#[test]
fn test_orderbook_bid_ask_ratio_empty_returns_none() {
let book = make_book();
assert!(book.bid_ask_ratio().is_none());
}
#[test]
fn test_orderbook_price_impact_buy_single_level() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "101", "10", 1)).unwrap();
let qty = Quantity::new(dec!(5)).unwrap();
let avg = book.price_impact(Side::Bid, qty).unwrap();
assert_eq!(avg, dec!(101));
}
#[test]
fn test_orderbook_price_impact_buy_spans_two_levels() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "100", "5", 1)).unwrap();
book.apply_delta(set_delta(Side::Ask, "102", "5", 2)).unwrap();
let qty = Quantity::new(dec!(10)).unwrap();
let avg = book.price_impact(Side::Bid, qty).unwrap();
assert_eq!(avg, dec!(101));
}
#[test]
fn test_orderbook_price_impact_insufficient_depth_returns_none() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "101", "3", 1)).unwrap();
let qty = Quantity::new(dec!(10)).unwrap();
assert!(book.price_impact(Side::Bid, qty).is_none());
}
#[test]
fn test_orderbook_price_impact_zero_qty_returns_none() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "101", "10", 1)).unwrap();
let qty = Quantity::zero();
assert!(book.price_impact(Side::Bid, qty).is_none());
}
#[test]
fn test_orderbook_depth_at_existing_bid_level() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "99", "5", 1)).unwrap();
let price = Price::new(dec!(99)).unwrap();
assert_eq!(book.depth_at(Side::Bid, price), Some(dec!(5)));
}
#[test]
fn test_orderbook_depth_at_absent_level_returns_none() {
let book = make_book();
let price = Price::new(dec!(50)).unwrap();
assert!(book.depth_at(Side::Bid, price).is_none());
assert!(book.depth_at(Side::Ask, price).is_none());
}
#[test]
fn test_orderbook_bid_depth_returns_top_n_descending() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
book.apply_delta(set_delta(Side::Bid, "98", "3", 3)).unwrap();
let levels = book.bid_depth(2);
assert_eq!(levels.len(), 2);
assert_eq!(levels[0].price.value(), dec!(100)); assert_eq!(levels[1].price.value(), dec!(99));
}
#[test]
fn test_orderbook_ask_depth_returns_top_n_ascending() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "101", "10", 1)).unwrap();
book.apply_delta(set_delta(Side::Ask, "102", "5", 2)).unwrap();
book.apply_delta(set_delta(Side::Ask, "103", "3", 3)).unwrap();
let levels = book.ask_depth(2);
assert_eq!(levels.len(), 2);
assert_eq!(levels[0].price.value(), dec!(101)); assert_eq!(levels[1].price.value(), dec!(102));
}
#[test]
fn test_orderbook_bid_depth_fewer_than_n() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
let levels = book.bid_depth(5);
assert_eq!(levels.len(), 1);
}
#[test]
fn test_orderbook_ask_depth_empty_book() {
let book = make_book();
assert!(book.ask_depth(3).is_empty());
}
#[test]
fn test_orderbook_remove_all_bids_clears_bid_side() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
book.remove_all(Side::Bid);
assert!(book.best_bid().is_none());
}
#[test]
fn test_orderbook_remove_all_bids_leaves_asks_intact() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
book.apply_delta(set_delta(Side::Ask, "101", "5", 2)).unwrap();
book.remove_all(Side::Bid);
assert!(book.best_bid().is_none());
assert!(book.best_ask().is_some());
}
#[test]
fn test_orderbook_remove_all_asks_clears_ask_side() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Ask, "101", "5", 1)).unwrap();
book.apply_delta(set_delta(Side::Ask, "102", "3", 2)).unwrap();
book.remove_all(Side::Ask);
assert!(book.best_ask().is_none());
}
#[test]
fn test_orderbook_total_levels_sums_both_sides() {
let mut book = make_book();
book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
book.apply_delta(set_delta(Side::Ask, "101", "8", 3)).unwrap();
assert_eq!(book.total_levels(), 3);
}
#[test]
fn test_orderbook_total_levels_empty_book() {
let book = make_book();
assert_eq!(book.total_levels(), 0);
}
}