use crate::error::StreamError;
use rust_decimal::Decimal;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum BookSide {
Bid,
Ask,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PriceLevel {
pub price: Decimal,
pub quantity: Decimal,
}
impl PriceLevel {
pub fn new(price: Decimal, quantity: Decimal) -> Self {
Self { price, quantity }
}
pub fn notional(&self) -> Decimal {
self.price * self.quantity
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BookDelta {
pub symbol: String,
pub side: BookSide,
pub price: Decimal,
pub quantity: Decimal,
pub sequence: Option<u64>,
}
impl BookDelta {
pub fn new(
symbol: impl Into<String>,
side: BookSide,
price: Decimal,
quantity: Decimal,
) -> Self {
Self {
symbol: symbol.into(),
side,
price,
quantity,
sequence: None,
}
}
pub fn with_sequence(mut self, seq: u64) -> Self {
self.sequence = Some(seq);
self
}
pub fn is_delete(&self) -> bool {
self.quantity.is_zero()
}
pub fn is_add(&self) -> bool {
!self.is_delete()
}
}
impl std::fmt::Display for BookDelta {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let side = match self.side {
BookSide::Bid => "Bid",
BookSide::Ask => "Ask",
};
match self.sequence {
Some(seq) => write!(
f,
"{} {} {} x {} seq={}",
self.symbol, side, self.price, self.quantity, seq
),
None => write!(
f,
"{} {} {} x {}",
self.symbol, side, self.price, self.quantity
),
}
}
}
pub struct OrderBook {
symbol: String,
bids: BTreeMap<Decimal, Decimal>, asks: BTreeMap<Decimal, Decimal>, last_sequence: Option<u64>,
}
impl OrderBook {
pub fn new(symbol: impl Into<String>) -> Self {
Self {
symbol: symbol.into(),
bids: BTreeMap::new(),
asks: BTreeMap::new(),
last_sequence: None,
}
}
#[must_use = "errors from apply() must be handled to avoid missed gaps or crossed-book state"]
pub fn apply(&mut self, delta: BookDelta) -> Result<(), StreamError> {
if delta.symbol != self.symbol {
return Err(StreamError::BookReconstructionFailed {
symbol: self.symbol.clone(),
reason: format!(
"delta symbol '{}' does not match book '{}'",
delta.symbol, self.symbol
),
});
}
if let (Some(last), Some(incoming)) = (self.last_sequence, delta.sequence) {
let expected = last + 1;
if incoming != expected {
return Err(StreamError::SequenceGap {
symbol: self.symbol.clone(),
expected,
got: incoming,
});
}
}
let map = match delta.side {
BookSide::Bid => &mut self.bids,
BookSide::Ask => &mut self.asks,
};
if delta.quantity.is_zero() {
map.remove(&delta.price);
} else {
map.insert(delta.price, delta.quantity);
}
if let Some(seq) = delta.sequence {
self.last_sequence = Some(seq);
}
self.check_crossed()
}
#[must_use = "errors from reset() indicate a crossed snapshot and must be handled"]
pub fn reset(
&mut self,
bids: Vec<PriceLevel>,
asks: Vec<PriceLevel>,
) -> Result<(), StreamError> {
self.bids.clear();
self.asks.clear();
self.last_sequence = None;
for lvl in bids {
if !lvl.quantity.is_zero() {
self.bids.insert(lvl.price, lvl.quantity);
}
}
for lvl in asks {
if !lvl.quantity.is_zero() {
self.asks.insert(lvl.price, lvl.quantity);
}
}
self.check_crossed()
}
pub fn best_bid(&self) -> Option<PriceLevel> {
self.bids
.iter()
.next_back()
.map(|(p, q)| PriceLevel::new(*p, *q))
}
pub fn best_ask(&self) -> Option<PriceLevel> {
self.asks
.iter()
.next()
.map(|(p, q)| PriceLevel::new(*p, *q))
}
pub fn best_bid_qty(&self) -> Option<Decimal> {
self.best_bid().map(|l| l.quantity)
}
pub fn best_ask_qty(&self) -> Option<Decimal> {
self.best_ask().map(|l| l.quantity)
}
pub fn mid_price(&self) -> Option<Decimal> {
let bid = self.best_bid()?.price;
let ask = self.best_ask()?.price;
Some((bid + ask) / Decimal::from(2))
}
pub fn weighted_mid_price(&self) -> Option<Decimal> {
let bid = self.best_bid()?;
let ask = self.best_ask()?;
let total_qty = bid.quantity + ask.quantity;
if total_qty.is_zero() {
return None;
}
Some((bid.price * ask.quantity + ask.price * bid.quantity) / total_qty)
}
pub fn top_of_book(&self) -> Option<(PriceLevel, PriceLevel)> {
Some((self.best_bid()?, self.best_ask()?))
}
pub fn price_range(&self) -> Option<Decimal> {
let worst_bid = *self.bids.iter().next()?.0; let best_ask = self.best_ask_price()?;
Some(best_ask - worst_bid)
}
pub fn spread(&self) -> Option<Decimal> {
let bid = self.best_bid()?.price;
let ask = self.best_ask()?.price;
Some(ask - bid)
}
pub fn is_empty(&self) -> bool {
self.bids.is_empty() && self.asks.is_empty()
}
pub fn total_notional_both_sides(&self) -> Decimal {
self.total_notional(BookSide::Bid) + self.total_notional(BookSide::Ask)
}
pub fn price_level_exists(&self, side: BookSide, price: Decimal) -> bool {
match side {
BookSide::Bid => self.bids.contains_key(&price),
BookSide::Ask => self.asks.contains_key(&price),
}
}
pub fn clear(&mut self) {
self.bids.clear();
self.asks.clear();
self.last_sequence = None;
}
pub fn bid_depth(&self) -> usize {
self.bids.len()
}
pub fn ask_depth(&self) -> usize {
self.asks.len()
}
pub fn bid_volume_total(&self) -> Decimal {
self.bids.values().copied().sum()
}
pub fn ask_volume_total(&self) -> Decimal {
self.asks.values().copied().sum()
}
pub fn total_notional(&self, side: BookSide) -> Decimal {
match side {
BookSide::Bid => self.bids.iter().map(|(p, q)| *p * *q).sum(),
BookSide::Ask => self.asks.iter().map(|(p, q)| *p * *q).sum(),
}
}
pub fn is_one_sided(&self) -> bool {
(self.bid_depth() > 0) != (self.ask_depth() > 0)
}
pub fn level_count_imbalance(&self) -> Option<f64> {
let total = self.bid_depth() + self.ask_depth();
if total == 0 {
return None;
}
let diff = self.bid_depth() as f64 - self.ask_depth() as f64;
Some(diff / total as f64)
}
pub fn bid_ask_spread_bps(&self) -> Option<f64> {
use rust_decimal::prelude::ToPrimitive;
let mid = self.mid_price()?;
if mid.is_zero() {
return None;
}
let spread_f = self.spread()?.to_f64()?;
let mid_f = mid.to_f64()?;
Some(spread_f / mid_f * 10_000.0)
}
#[deprecated(since = "2.2.0", note = "Use `bid_volume_total()` instead")]
pub fn total_bid_volume(&self) -> Decimal {
self.bid_volume_total()
}
#[deprecated(since = "2.2.0", note = "Use `ask_volume_total()` instead")]
pub fn total_ask_volume(&self) -> Decimal {
self.ask_volume_total()
}
pub fn cumulative_bid_volume(&self, n: usize) -> Decimal {
self.bids.values().rev().take(n).copied().sum()
}
pub fn cumulative_ask_volume(&self, n: usize) -> Decimal {
self.asks.values().take(n).copied().sum()
}
pub fn top_n_bids(&self, n: usize) -> Vec<PriceLevel> {
self.bids
.iter()
.rev()
.take(n)
.map(|(p, q)| PriceLevel::new(*p, *q))
.collect()
}
pub fn top_n_asks(&self, n: usize) -> Vec<PriceLevel> {
self.asks
.iter()
.take(n)
.map(|(p, q)| PriceLevel::new(*p, *q))
.collect()
}
pub fn depth_ratio(&self, n: usize) -> Option<f64> {
use rust_decimal::prelude::ToPrimitive;
let ask_vol = self.cumulative_ask_volume(n);
if ask_vol.is_zero() {
return None;
}
(self.cumulative_bid_volume(n) / ask_vol).to_f64()
}
pub fn ask_wall(&self, min_qty: Decimal) -> Option<PriceLevel> {
self.asks
.iter()
.find(|(_, qty)| **qty >= min_qty)
.map(|(price, qty)| PriceLevel::new(*price, *qty))
}
pub fn bid_wall(&self, min_qty: Decimal) -> Option<PriceLevel> {
self.bids
.iter()
.rev()
.find(|(_, qty)| **qty >= min_qty)
.map(|(price, qty)| PriceLevel::new(*price, *qty))
}
pub fn bid_levels_above(&self, price: Decimal) -> usize {
self.bids.range((std::ops::Bound::Excluded(&price), std::ops::Bound::Unbounded)).count()
}
pub fn ask_levels_below(&self, price: Decimal) -> usize {
self.asks.range(..price).count()
}
pub fn bid_ask_volume_ratio(&self) -> Option<f64> {
use rust_decimal::prelude::ToPrimitive;
let bid = self.bid_volume_total();
let ask = self.ask_volume_total();
if bid.is_zero() || ask.is_zero() {
return None;
}
let bid_f = bid.to_f64()?;
let ask_f = ask.to_f64()?;
Some(bid_f / ask_f)
}
pub fn top_n_bid_volume(&self, n: usize) -> Decimal {
self.cumulative_bid_volume(n)
}
pub fn imbalance_ratio(&self) -> Option<f64> {
use rust_decimal::prelude::ToPrimitive;
let bid = self.bid_volume_total();
let ask = self.ask_volume_total();
let total = bid + ask;
if total.is_zero() {
return None;
}
let bid_f = bid.to_f64()?;
let ask_f = ask.to_f64()?;
let total_f = bid_f + ask_f;
Some((bid_f - ask_f) / total_f)
}
pub fn top_n_ask_volume(&self, n: usize) -> Decimal {
self.cumulative_ask_volume(n)
}
pub fn has_ask_at(&self, price: Decimal) -> bool {
self.asks.get(&price).map_or(false, |q| !q.is_zero())
}
pub fn bid_ask_depth(&self) -> (usize, usize) {
(self.bid_depth(), self.ask_depth())
}
pub fn total_book_volume(&self) -> Decimal {
self.total_volume()
}
pub fn price_range_bids(&self) -> Option<Decimal> {
if self.bid_depth() < 2 {
return None;
}
let best = self.best_bid_price()?;
let worst = *self.bids.keys().next()?;
Some(best - worst)
}
pub fn spread_pct(&self) -> Option<f64> {
use rust_decimal::prelude::ToPrimitive;
let mid = self.mid_price()?;
if mid.is_zero() {
return None;
}
let spread = self.spread()?;
(spread / mid * Decimal::from(100)).to_f64()
}
pub fn is_tight_spread(&self, threshold: Decimal) -> bool {
self.spread().map_or(false, |s| s <= threshold)
}
pub fn total_depth(&self) -> usize {
self.bid_depth() + self.ask_depth()
}
pub fn total_volume(&self) -> Decimal {
self.bid_volume_total() + self.ask_volume_total()
}
pub fn symbol(&self) -> &str {
&self.symbol
}
pub fn last_sequence(&self) -> Option<u64> {
self.last_sequence
}
pub fn quote_imbalance(&self) -> Option<f64> {
use rust_decimal::prelude::ToPrimitive;
let bid_qty = self.best_bid_qty()?;
let ask_qty = self.best_ask_qty()?;
let total = bid_qty + ask_qty;
if total.is_zero() {
return None;
}
(bid_qty / total).to_f64()
}
pub fn contains_bid(&self, price: Decimal) -> bool {
self.bids.contains_key(&price)
}
pub fn contains_ask(&self, price: Decimal) -> bool {
self.asks.contains_key(&price)
}
pub fn volume_at_bid(&self, price: Decimal) -> Option<Decimal> {
self.bids.get(&price).copied()
}
pub fn volume_at_ask(&self, price: Decimal) -> Option<Decimal> {
self.asks.get(&price).copied()
}
pub fn level_count(&self, side: BookSide) -> usize {
match side {
BookSide::Bid => self.bid_depth(),
BookSide::Ask => self.ask_depth(),
}
}
pub fn level_count_both_sides(&self) -> usize {
self.total_depth()
}
pub fn ask_price_at_rank(&self, n: usize) -> Option<Decimal> {
self.asks.keys().nth(n).copied()
}
pub fn bid_price_at_rank(&self, n: usize) -> Option<Decimal> {
self.bids.keys().nth_back(n).copied()
}
pub fn quote_density(&self, side: BookSide) -> Option<Decimal> {
let map = match side {
BookSide::Bid => &self.bids,
BookSide::Ask => &self.asks,
};
if map.len() < 2 { return None; }
let min_p = *map.keys().next()?;
let max_p = *map.keys().next_back()?;
let range = max_p - min_p;
if range.is_zero() { return None; }
Some(Decimal::from(map.len()) / range)
}
pub fn bid_ask_qty_ratio(&self) -> Option<f64> {
self.bid_ask_ratio()
}
pub fn top_bid_qty(&self) -> Option<Decimal> {
self.best_bid_qty()
}
pub fn top_ask_qty(&self) -> Option<Decimal> {
self.best_ask_qty()
}
pub fn cumulative_bid_qty(&self, n: usize) -> Decimal {
self.cumulative_bid_volume(n)
}
pub fn cumulative_ask_qty(&self, n: usize) -> Decimal {
self.cumulative_ask_volume(n)
}
pub fn ladder_balance(&self, n: usize) -> Option<f64> {
use rust_decimal::prelude::ToPrimitive;
let ask_qty = self.cumulative_ask_qty(n);
if ask_qty.is_zero() { return None; }
(self.cumulative_bid_qty(n) / ask_qty).to_f64()
}
pub fn ask_bid_level_ratio(&self) -> Option<f64> {
if self.bid_depth() == 0 { return None; }
Some(self.ask_depth() as f64 / self.bid_depth() as f64)
}
pub fn depth_at_price(&self, price: Decimal, side: BookSide) -> Option<Decimal> {
match side {
BookSide::Bid => self.bids.get(&price).copied(),
BookSide::Ask => self.asks.get(&price).copied(),
}
}
pub fn bid_ask_ratio(&self) -> Option<f64> {
use rust_decimal::prelude::ToPrimitive;
let ask = self.ask_volume_total();
if ask.is_zero() {
return None;
}
(self.bid_volume_total() / ask).to_f64()
}
pub fn all_bids(&self) -> Vec<PriceLevel> {
self.bids
.iter()
.rev()
.map(|(p, q)| PriceLevel::new(*p, *q))
.collect()
}
pub fn all_asks(&self) -> Vec<PriceLevel> {
self.asks
.iter()
.map(|(p, q)| PriceLevel::new(*p, *q))
.collect()
}
pub fn top_bids(&self, n: usize) -> Vec<PriceLevel> {
self.bids
.iter()
.rev()
.take(n)
.map(|(p, q)| PriceLevel::new(*p, *q))
.collect()
}
pub fn top_asks(&self, n: usize) -> Vec<PriceLevel> {
self.asks
.iter()
.take(n)
.map(|(p, q)| PriceLevel::new(*p, *q))
.collect()
}
pub fn imbalance(&self) -> Option<f64> {
use rust_decimal::prelude::ToPrimitive;
let bid_qty = self.best_bid()?.quantity;
let ask_qty = self.best_ask()?.quantity;
let total = bid_qty + ask_qty;
if total.is_zero() {
return None;
}
let imb = (bid_qty - ask_qty) / total;
imb.to_f64()
}
pub fn bid_ask_imbalance(&self, n: usize) -> Option<f64> {
use rust_decimal::prelude::ToPrimitive;
let bid_vol = self.cumulative_bid_volume(n);
let ask_vol = self.cumulative_ask_volume(n);
if bid_vol.is_zero() || ask_vol.is_zero() {
return None;
}
let total = bid_vol + ask_vol;
((bid_vol - ask_vol) / total).to_f64()
}
pub fn vwap(&self, side: BookSide, n: usize) -> Option<Decimal> {
let levels = match side {
BookSide::Bid => self.top_bids(n),
BookSide::Ask => self.top_asks(n),
};
let total_vol: Decimal = levels.iter().map(|l| l.quantity).sum();
if total_vol.is_zero() {
return None;
}
let price_vol_sum: Decimal = levels.iter().map(|l| l.price * l.quantity).sum();
Some(price_vol_sum / total_vol)
}
pub fn price_at_volume(&self, side: BookSide, target_volume: Decimal) -> Option<Decimal> {
if target_volume.is_zero() {
return None;
}
let levels: Vec<(Decimal, Decimal)> = match side {
BookSide::Bid => self.bids.iter().rev().map(|(p, q)| (*p, *q)).collect(),
BookSide::Ask => self.asks.iter().map(|(p, q)| (*p, *q)).collect(),
};
if levels.is_empty() {
return None;
}
let mut remaining = target_volume;
let mut notional = Decimal::ZERO;
let mut filled = Decimal::ZERO;
for (price, qty) in &levels {
if remaining.is_zero() {
break;
}
let take = (*qty).min(remaining);
notional += price * take;
filled += take;
remaining -= take;
}
if filled.is_zero() {
return None;
}
Some(notional / filled)
}
pub fn depth_imbalance(&self, n: usize) -> Option<f64> {
use rust_decimal::prelude::ToPrimitive;
if n == 0 {
return None;
}
let bid_vol = self.cumulative_bid_volume(n);
let ask_vol = self.cumulative_ask_volume(n);
let total = bid_vol + ask_vol;
if total.is_zero() {
return None;
}
((bid_vol - ask_vol) / total).to_f64()
}
pub fn levels(&self, side: BookSide, n: usize) -> Vec<PriceLevel> {
match side {
BookSide::Bid => self
.bids
.iter()
.rev()
.take(n)
.map(|(p, q)| PriceLevel::new(*p, *q))
.collect(),
BookSide::Ask => self
.asks
.iter()
.take(n)
.map(|(p, q)| PriceLevel::new(*p, *q))
.collect(),
}
}
pub fn bid_volume_at_price(&self, price: Decimal) -> Option<Decimal> {
self.bids.get(&price).copied()
}
pub fn ask_volume_at_price(&self, price: Decimal) -> Option<Decimal> {
self.asks.get(&price).copied()
}
pub fn snapshot(&self) -> (Vec<PriceLevel>, Vec<PriceLevel>) {
(self.all_bids(), self.all_asks())
}
pub fn best_bid_price(&self) -> Option<Decimal> {
self.best_bid().map(|l| l.price)
}
pub fn best_ask_price(&self) -> Option<Decimal> {
self.best_ask().map(|l| l.price)
}
pub fn is_crossed(&self) -> bool {
self.best_bid_price().zip(self.best_ask_price()).map_or(false, |(b, a)| b >= a)
}
pub fn has_bids(&self) -> bool {
self.bid_depth() > 0
}
pub fn has_asks(&self) -> bool {
self.ask_depth() > 0
}
pub fn ask_price_range(&self) -> Option<Decimal> {
let best = self.best_ask_price()?;
let worst = *self.asks.keys().next_back()?;
Some(worst - best)
}
pub fn bid_price_range(&self) -> Option<Decimal> {
let best = self.best_bid_price()?;
let worst = *self.bids.keys().next()?;
Some(best - worst)
}
pub fn mid_spread_ratio(&self) -> Option<f64> {
use rust_decimal::prelude::ToPrimitive;
let spread = self.spread()?;
let mid = self.mid_price()?;
if mid.is_zero() {
return None;
}
(spread / mid).to_f64()
}
pub fn volume_imbalance(&self) -> Option<f64> {
use rust_decimal::prelude::ToPrimitive;
let bid = self.bid_volume_total();
let ask = self.ask_volume_total();
let total = bid + ask;
if total.is_zero() {
return None;
}
((bid - ask) / total).to_f64()
}
fn check_crossed(&self) -> Result<(), StreamError> {
if let (Some(bid), Some(ask)) = (self.best_bid(), self.best_ask()) {
if bid.price >= ask.price {
return Err(StreamError::BookCrossed {
symbol: self.symbol.clone(),
bid: bid.price,
ask: ask.price,
});
}
}
Ok(())
}
pub fn fee_estimate(&self, side: BookSide, qty: Decimal, fee_bps: Decimal) -> Option<Decimal> {
if qty <= Decimal::ZERO { return None; }
let best_price = match side {
BookSide::Bid => self.best_bid_price()?,
BookSide::Ask => self.best_ask_price()?,
};
let impact = self.price_impact(side, qty).unwrap_or(Decimal::ZERO);
let fill_price = best_price + impact;
Some(fill_price * qty * fee_bps / Decimal::from(10_000u32))
}
pub fn spread_ticks(&self, tick_size: Decimal) -> Option<Decimal> {
if tick_size.is_zero() { return None; }
let spread = self.spread()?;
Some(spread / tick_size)
}
pub fn spread_bps(&self) -> Option<f64> {
use rust_decimal::prelude::ToPrimitive;
let mid = self.mid_price()?;
if mid.is_zero() {
return None;
}
let spread = self.spread()?;
(spread / mid * Decimal::from(10_000u32)).to_f64()
}
pub fn depth_at_pct(&self, side: BookSide, pct: f64) -> Option<Decimal> {
use rust_decimal::prelude::FromPrimitive;
if pct < 0.0 { return None; }
let pct_dec = Decimal::from_f64(pct / 100.0)?;
match side {
BookSide::Bid => {
let best = self.best_bid_price()?;
let threshold = best * (Decimal::ONE - pct_dec);
Some(self.bids.range(threshold..).map(|(_, q)| q).sum())
}
BookSide::Ask => {
let best = self.best_ask_price()?;
let threshold = best * (Decimal::ONE + pct_dec);
Some(self.asks.range(..=threshold).map(|(_, q)| q).sum())
}
}
}
pub fn microprice(&self) -> Option<Decimal> {
let bid = self.best_bid()?;
let ask = self.best_ask()?;
let total_qty = bid.quantity + ask.quantity;
if total_qty.is_zero() { return None; }
Some((ask.quantity * bid.price + bid.quantity * ask.price) / total_qty)
}
pub fn best_n_levels(&self, side: BookSide, n: usize) -> Vec<(Decimal, Decimal)> {
match side {
BookSide::Bid => self.bids.iter().rev().take(n)
.map(|(&p, &q)| (p, q)).collect(),
BookSide::Ask => self.asks.iter().take(n)
.map(|(&p, &q)| (p, q)).collect(),
}
}
pub fn price_impact(&self, side: BookSide, qty: Decimal) -> Option<Decimal> {
if qty <= Decimal::ZERO { return None; }
let best_price = match side {
BookSide::Bid => self.best_bid_price()?,
BookSide::Ask => self.best_ask_price()?,
};
let mut remaining = qty;
let mut cost = Decimal::ZERO;
let levels: Box<dyn Iterator<Item = (&Decimal, &Decimal)>> = match side {
BookSide::Bid => Box::new(self.bids.iter().rev()),
BookSide::Ask => Box::new(self.asks.iter()),
};
for (&price, &level_qty) in levels {
if remaining <= Decimal::ZERO { break; }
let filled = remaining.min(level_qty);
cost += price * filled;
remaining -= filled;
}
if remaining > Decimal::ZERO { return None; } let avg_fill = cost / qty;
Some((avg_fill - best_price).abs())
}
pub fn total_value_at_level(&self, side: BookSide, price: Decimal) -> Option<Decimal> {
match side {
BookSide::Bid => self.bids.get(&price).map(|&q| price * q),
BookSide::Ask => self.asks.get(&price).map(|&q| price * q),
}
}
pub fn price_impact_buy(&self, quantity: Decimal) -> Option<Decimal> {
if quantity <= Decimal::ZERO {
return None;
}
let mut remaining = quantity;
let mut cost = Decimal::ZERO;
for (&price, &qty) in &self.asks {
if remaining.is_zero() { break; }
let fill = remaining.min(qty);
cost += fill * price;
remaining -= fill;
}
if !remaining.is_zero() { return None; }
Some(cost / quantity)
}
pub fn price_impact_sell(&self, quantity: Decimal) -> Option<Decimal> {
if quantity <= Decimal::ZERO {
return None;
}
let mut remaining = quantity;
let mut proceeds = Decimal::ZERO;
for (&price, &qty) in self.bids.iter().rev() {
if remaining.is_zero() { break; }
let fill = remaining.min(qty);
proceeds += fill * price;
remaining -= fill;
}
if !remaining.is_zero() { return None; }
Some(proceeds / quantity)
}
#[deprecated(note = "use ask_depth() instead")]
pub fn ask_level_count(&self) -> usize {
self.ask_depth()
}
#[deprecated(note = "use bid_depth() instead")]
pub fn bid_level_count(&self) -> usize {
self.bid_depth()
}
pub fn ask_volume_within(&self, price_range: Decimal) -> Decimal {
self.best_ask().map_or(Decimal::ZERO, |best| {
self.asks.range(..=(best.price + price_range)).map(|(_, &q)| q).sum()
})
}
pub fn bid_volume_within(&self, price_range: Decimal) -> Decimal {
self.best_bid().map_or(Decimal::ZERO, |best| {
self.bids.range((best.price - price_range)..).map(|(_, &q)| q).sum()
})
}
pub fn ask_volume_above(&self, price: Decimal) -> Decimal {
use std::ops::Bound::Excluded;
self.asks
.range((Excluded(price), std::ops::Bound::Unbounded))
.map(|(_, &q)| q)
.sum()
}
pub fn bid_volume_below(&self, price: Decimal) -> Decimal {
use std::ops::Bound::Unbounded;
use std::ops::Bound::Excluded;
self.bids
.range((Unbounded, Excluded(price)))
.map(|(_, &q)| q)
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn book(symbol: &str) -> OrderBook {
OrderBook::new(symbol)
}
fn delta(symbol: &str, side: BookSide, price: Decimal, qty: Decimal) -> BookDelta {
BookDelta::new(symbol, side, price, qty)
}
#[test]
fn test_order_book_apply_bid_level() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
.unwrap();
assert_eq!(b.best_bid().unwrap().price, dec!(50000));
}
#[test]
fn test_order_book_apply_ask_level() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2)))
.unwrap();
assert_eq!(b.best_ask().unwrap().price, dec!(50100));
}
#[test]
fn test_order_book_remove_level_with_zero_qty() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(0)))
.unwrap();
assert!(b.best_bid().is_none());
}
#[test]
fn test_order_book_best_bid_is_highest() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1)))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(2)))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(3)))
.unwrap();
assert_eq!(b.best_bid().unwrap().price, dec!(50000));
}
#[test]
fn test_order_book_best_ask_is_lowest() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(1)))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2)))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50300), dec!(3)))
.unwrap();
assert_eq!(b.best_ask().unwrap().price, dec!(50100));
}
#[test]
fn test_order_book_mid_price() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)))
.unwrap();
assert_eq!(b.mid_price().unwrap(), dec!(50050));
}
#[test]
fn test_order_book_spread() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)))
.unwrap();
assert_eq!(b.spread().unwrap(), dec!(100));
}
#[test]
fn test_order_book_crossed_returns_error() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50000), dec!(1)))
.unwrap();
let result = b.apply(delta("BTC-USD", BookSide::Bid, dec!(50001), dec!(1)));
assert!(matches!(result, Err(StreamError::BookCrossed { .. })));
}
#[test]
fn test_order_book_wrong_symbol_delta_rejected() {
let mut b = book("BTC-USD");
let result = b.apply(delta("ETH-USD", BookSide::Bid, dec!(3000), dec!(1)));
assert!(matches!(
result,
Err(StreamError::BookReconstructionFailed { .. })
));
}
#[test]
fn test_order_book_reset_clears_and_reloads() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49000), dec!(5)))
.unwrap();
b.reset(
vec![PriceLevel::new(dec!(50000), dec!(1))],
vec![PriceLevel::new(dec!(50100), dec!(1))],
)
.unwrap();
assert_eq!(b.bid_depth(), 1);
assert_eq!(b.best_bid().unwrap().price, dec!(50000));
}
#[test]
fn test_order_book_reset_ignores_zero_qty_levels() {
let mut b = book("BTC-USD");
b.reset(
vec![
PriceLevel::new(dec!(50000), dec!(1)),
PriceLevel::new(dec!(49900), dec!(0)),
],
vec![PriceLevel::new(dec!(50100), dec!(1))],
)
.unwrap();
assert_eq!(b.bid_depth(), 1);
}
#[test]
fn test_order_book_reset_clears_sequence() {
let mut b = book("BTC-USD");
b.apply(
delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1)).with_sequence(5),
)
.unwrap();
assert_eq!(b.last_sequence(), Some(5));
b.reset(
vec![PriceLevel::new(dec!(50000), dec!(1))],
vec![PriceLevel::new(dec!(50100), dec!(1))],
)
.unwrap();
assert_eq!(b.last_sequence(), None);
}
#[test]
fn test_order_book_depth_counts() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1)))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(1)))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)))
.unwrap();
assert_eq!(b.bid_depth(), 2);
assert_eq!(b.ask_depth(), 1);
}
#[test]
fn test_order_book_top_bids_descending() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(3)))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(2)))
.unwrap();
let top = b.top_bids(2);
assert_eq!(top[0].price, dec!(50000));
assert_eq!(top[1].price, dec!(49900));
}
#[test]
fn test_order_book_top_asks_ascending() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50300), dec!(3)))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(2)))
.unwrap();
let top = b.top_asks(2);
assert_eq!(top[0].price, dec!(50100));
assert_eq!(top[1].price, dec!(50200));
}
#[test]
fn test_book_delta_with_sequence() {
let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)).with_sequence(42);
assert_eq!(d.sequence, Some(42));
}
#[test]
fn test_order_book_sequence_tracking() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)).with_sequence(7))
.unwrap();
assert_eq!(b.last_sequence(), Some(7));
}
#[test]
fn test_order_book_sequence_gap_detected() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1)).with_sequence(1))
.unwrap();
let result =
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)).with_sequence(3));
assert!(matches!(
result,
Err(StreamError::SequenceGap { expected: 2, got: 3, .. })
));
}
#[test]
fn test_order_book_sequential_deltas_accepted() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1)).with_sequence(1))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)).with_sequence(2))
.unwrap();
assert_eq!(b.last_sequence(), Some(2));
}
#[test]
fn test_order_book_mid_price_empty_returns_none() {
let b = book("BTC-USD");
assert!(b.mid_price().is_none());
}
#[test]
fn test_price_level_new() {
let lvl = PriceLevel::new(dec!(100), dec!(5));
assert_eq!(lvl.price, dec!(100));
assert_eq!(lvl.quantity, dec!(5));
}
#[test]
fn test_contains_bid_present() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
.unwrap();
assert!(b.contains_bid(dec!(50000)));
assert!(!b.contains_bid(dec!(49999)));
}
#[test]
fn test_contains_ask_present() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2)))
.unwrap();
assert!(b.contains_ask(dec!(50100)));
assert!(!b.contains_ask(dec!(50200)));
}
#[test]
fn test_contains_bid_removed_after_zero_qty() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(0)))
.unwrap();
assert!(!b.contains_bid(dec!(50000)));
}
#[test]
fn test_book_delta_serde_roundtrip() {
let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))
.with_sequence(42);
let json = serde_json::to_string(&d).unwrap();
let d2: BookDelta = serde_json::from_str(&json).unwrap();
assert_eq!(d2.symbol, "BTC-USD");
assert_eq!(d2.price, dec!(50000));
assert_eq!(d2.sequence, Some(42));
}
#[test]
fn test_volume_at_bid_present() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(3)))
.unwrap();
assert_eq!(b.volume_at_bid(dec!(50000)), Some(dec!(3)));
assert_eq!(b.volume_at_bid(dec!(49999)), None);
}
#[test]
fn test_volume_at_ask_present() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(5)))
.unwrap();
assert_eq!(b.volume_at_ask(dec!(50100)), Some(dec!(5)));
assert_eq!(b.volume_at_ask(dec!(50200)), None);
}
#[test]
fn test_book_delta_display_with_sequence() {
let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))
.with_sequence(42);
let s = d.to_string();
assert!(s.contains("BTC-USD"));
assert!(s.contains("Bid"));
assert!(s.contains("seq=42"));
}
#[test]
fn test_book_delta_display_without_sequence() {
let d = BookDelta::new("ETH-USD", BookSide::Ask, dec!(3000), dec!(2));
let s = d.to_string();
assert!(s.contains("ETH-USD"));
assert!(s.contains("Ask"));
assert!(!s.contains("seq="));
}
#[test]
fn test_book_delta_is_delete_zero_qty() {
let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(0));
assert!(d.is_delete());
}
#[test]
fn test_book_delta_is_delete_nonzero_qty() {
let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(1));
assert!(!d.is_delete());
}
#[test]
fn test_snapshot_bids_descending_asks_ascending() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(1)))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(2)))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(1)))
.unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(3)))
.unwrap();
let (bids, asks) = b.snapshot();
assert_eq!(bids[0].price, dec!(50000));
assert_eq!(bids[1].price, dec!(49800));
assert_eq!(asks[0].price, dec!(50100));
assert_eq!(asks[1].price, dec!(50200));
}
#[test]
fn test_bid_ask_imbalance_balanced() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
let imb = b.bid_ask_imbalance(1).unwrap();
assert!((imb).abs() < 1e-9, "equal qty → ~0");
}
#[test]
fn test_bid_ask_imbalance_full_bid_pressure() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(10))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(0))).unwrap();
assert!(b.bid_ask_imbalance(1).is_none());
}
#[test]
fn test_bid_ask_imbalance_two_levels() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(3))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(2))).unwrap();
let imb = b.bid_ask_imbalance(2).unwrap();
assert!((imb).abs() < 1e-9);
}
#[test]
fn test_vwap_single_level_equals_price() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(5))).unwrap();
assert_eq!(b.vwap(BookSide::Ask, 1), Some(dec!(50100)));
}
#[test]
fn test_vwap_two_equal_qty_levels() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(1))).unwrap();
assert_eq!(b.vwap(BookSide::Bid, 2), Some(dec!(49900)));
}
#[test]
fn test_vwap_empty_side_returns_none() {
let b = book("BTC-USD");
assert!(b.vwap(BookSide::Ask, 5).is_none());
}
#[test]
fn test_depth_at_price_bid_present() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(3))).unwrap();
assert_eq!(b.depth_at_price(dec!(50000), BookSide::Bid), Some(dec!(3)));
}
#[test]
fn test_depth_at_price_ask_present() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2))).unwrap();
assert_eq!(b.depth_at_price(dec!(50100), BookSide::Ask), Some(dec!(2)));
}
#[test]
fn test_depth_at_price_absent_returns_none() {
let b = book("BTC-USD");
assert!(b.depth_at_price(dec!(99999), BookSide::Bid).is_none());
assert!(b.depth_at_price(dec!(99999), BookSide::Ask).is_none());
}
#[test]
fn test_bid_ask_ratio_equal_sides() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(5))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(5))).unwrap();
let ratio = b.bid_ask_ratio().unwrap();
assert!((ratio - 1.0).abs() < 1e-9);
}
#[test]
fn test_bid_ask_ratio_more_bids() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(10))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(5))).unwrap();
let ratio = b.bid_ask_ratio().unwrap();
assert!((ratio - 2.0).abs() < 1e-9);
}
#[test]
fn test_bid_ask_ratio_no_asks_returns_none() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(5))).unwrap();
assert!(b.bid_ask_ratio().is_none());
}
#[test]
fn test_all_bids_sorted_descending() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(2))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(3))).unwrap();
let bids = b.all_bids();
assert_eq!(bids.len(), 3);
assert_eq!(bids[0].price, dec!(50000));
assert_eq!(bids[1].price, dec!(49900));
assert_eq!(bids[2].price, dec!(49800));
}
#[test]
fn test_all_asks_sorted_ascending() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50300), dec!(2))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(3))).unwrap();
let asks = b.all_asks();
assert_eq!(asks.len(), 3);
assert_eq!(asks[0].price, dec!(50100));
assert_eq!(asks[1].price, dec!(50200));
assert_eq!(asks[2].price, dec!(50300));
}
#[test]
fn test_all_bids_empty_returns_empty() {
let b = book("BTC-USD");
assert!(b.all_bids().is_empty());
}
#[test]
fn test_spread_pct_basic() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(100), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
let pct = b.spread_pct().unwrap();
assert!((pct - 100.0 / 100.5).abs() < 1e-9, "got {pct}");
}
#[test]
fn test_spread_pct_empty_book_returns_none() {
let b = book("X");
assert!(b.spread_pct().is_none());
}
#[test]
fn test_total_depth_counts_both_sides() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Bid, dec!(98), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
assert_eq!(b.total_depth(), 3);
}
#[test]
fn test_total_depth_empty_is_zero() {
let b = book("X");
assert_eq!(b.total_depth(), 0);
}
#[test]
fn test_total_volume_sums_both_sides() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(99), dec!(3))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(101), dec!(5))).unwrap();
assert_eq!(b.total_volume(), dec!(8));
}
#[test]
fn test_total_volume_empty_is_zero() {
let b = book("X");
assert_eq!(b.total_volume(), dec!(0));
}
#[test]
fn test_level_count_empty() {
let b = book("BTC-USD");
assert_eq!(b.level_count(BookSide::Bid), 0);
assert_eq!(b.level_count(BookSide::Ask), 0);
}
#[test]
fn test_level_count_matches_depth_methods() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(2))).unwrap();
assert_eq!(b.level_count(BookSide::Bid), b.bid_depth());
assert_eq!(b.level_count(BookSide::Ask), b.ask_depth());
}
#[test]
fn test_price_level_notional() {
let level = PriceLevel::new(dec!(50000), dec!(2));
assert_eq!(level.notional(), dec!(100000));
}
#[test]
fn test_price_level_notional_zero_qty() {
let level = PriceLevel::new(dec!(100), dec!(0));
assert_eq!(level.notional(), dec!(0));
}
#[test]
fn test_weighted_mid_price_equal_qtys_is_arithmetic_mid() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(100), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(102), dec!(1))).unwrap();
assert_eq!(b.weighted_mid_price().unwrap(), dec!(101));
}
#[test]
fn test_weighted_mid_price_skews_toward_larger_qty() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(100), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(102), dec!(3))).unwrap();
assert_eq!(b.weighted_mid_price().unwrap(), dec!(100.5));
}
#[test]
fn test_weighted_mid_price_empty_returns_none() {
let b = book("X");
assert!(b.weighted_mid_price().is_none());
}
#[test]
fn test_is_empty_new_book() {
let b = book("BTC-USD");
assert!(b.is_empty());
}
#[test]
fn test_is_empty_false_with_bid() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
assert!(!b.is_empty());
}
#[test]
fn test_is_empty_false_with_ask() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
assert!(!b.is_empty());
}
#[test]
fn test_is_empty_true_after_removing_all_levels() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(0))).unwrap(); assert!(b.is_empty());
}
#[test]
fn test_clear_empty_book_is_noop() {
let mut b = book("BTC-USD");
b.clear();
assert!(b.is_empty());
}
#[test]
fn test_clear_removes_all_levels() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(2))).unwrap();
b.clear();
assert!(b.is_empty());
}
#[test]
fn test_clear_allows_fresh_apply_after() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
b.clear();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(5))).unwrap();
assert_eq!(b.bid_depth(), 1);
}
#[test]
fn test_total_notional_bid_side() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(2))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(3))).unwrap();
assert_eq!(b.total_notional(BookSide::Bid), dec!(249700));
}
#[test]
fn test_total_notional_ask_side() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(2))).unwrap();
assert_eq!(b.total_notional(BookSide::Ask), dec!(150500));
}
#[test]
fn test_total_notional_empty_side_is_zero() {
let b = book("BTC-USD");
assert_eq!(b.total_notional(BookSide::Bid), dec!(0));
assert_eq!(b.total_notional(BookSide::Ask), dec!(0));
}
#[test]
fn test_cumulative_bid_volume_top_two() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(5))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(3))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(98), dec!(2))).unwrap();
assert_eq!(b.cumulative_bid_volume(2), dec!(8));
}
#[test]
fn test_cumulative_ask_volume_top_two() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(4))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(6))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(103), dec!(1))).unwrap();
assert_eq!(b.cumulative_ask_volume(2), dec!(10));
}
#[test]
fn test_cumulative_volume_empty_returns_zero() {
let b = book("BTC-USD");
assert_eq!(b.cumulative_bid_volume(5), dec!(0));
assert_eq!(b.cumulative_ask_volume(5), dec!(0));
}
#[test]
fn test_top_n_bids_best_first() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(2))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(98), dec!(3))).unwrap();
let top2 = b.top_n_bids(2);
assert_eq!(top2.len(), 2);
assert_eq!(top2[0].price, dec!(100)); assert_eq!(top2[1].price, dec!(99));
}
#[test]
fn test_top_n_asks_best_first() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(2))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(103), dec!(3))).unwrap();
let top2 = b.top_n_asks(2);
assert_eq!(top2.len(), 2);
assert_eq!(top2[0].price, dec!(101)); assert_eq!(top2[1].price, dec!(102));
}
#[test]
fn test_depth_ratio_balanced_book() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(5))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(5))).unwrap();
let ratio = b.depth_ratio(1).unwrap();
assert!((ratio - 1.0).abs() < 1e-9);
}
#[test]
fn test_depth_ratio_empty_asks_returns_none() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(5))).unwrap();
assert!(b.depth_ratio(1).is_none());
}
#[test]
fn test_is_one_sided_bids_only() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
assert!(b.is_one_sided());
}
#[test]
fn test_is_one_sided_asks_only() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
assert!(b.is_one_sided());
}
#[test]
fn test_is_one_sided_false_with_both_sides() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
assert!(!b.is_one_sided());
}
#[test]
fn test_is_one_sided_false_for_empty_book() {
let b = book("BTC-USD");
assert!(!b.is_one_sided());
}
#[test]
fn test_bid_ask_spread_bps_known_value() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
let bps = b.bid_ask_spread_bps().unwrap();
assert!((bps - 1.0 / 100.5 * 10_000.0).abs() < 0.01);
}
#[test]
fn test_bid_ask_spread_bps_none_when_one_sided() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
assert!(b.bid_ask_spread_bps().is_none());
}
#[test]
fn test_bid_ask_spread_bps_none_for_empty_book() {
let b = book("BTC-USD");
assert!(b.bid_ask_spread_bps().is_none());
}
#[test]
fn test_ask_wall_returns_cheapest_ask_above_threshold() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(10))).unwrap();
let wall = b.ask_wall(dec!(5)).unwrap();
assert_eq!(wall.price, dec!(50200));
assert_eq!(wall.quantity, dec!(10));
}
#[test]
fn test_ask_wall_none_when_no_level_meets_threshold() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
assert!(b.ask_wall(dec!(5)).is_none());
}
#[test]
fn test_bid_wall_returns_highest_bid_above_threshold() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(10))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(2))).unwrap();
let wall = b.bid_wall(dec!(5)).unwrap();
assert_eq!(wall.price, dec!(49900));
assert_eq!(wall.quantity, dec!(10));
}
#[test]
fn test_bid_wall_none_when_no_level_meets_threshold() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1))).unwrap();
assert!(b.bid_wall(dec!(5)).is_none());
}
#[test]
fn test_level_count_imbalance_balanced_sides() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
assert_eq!(b.level_count_imbalance(), Some(0.0));
}
#[test]
fn test_level_count_imbalance_bids_only() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(98), dec!(1))).unwrap();
assert_eq!(b.level_count_imbalance(), Some(1.0));
}
#[test]
fn test_level_count_imbalance_none_for_empty_book() {
let b = book("BTC-USD");
assert!(b.level_count_imbalance().is_none());
}
#[test]
fn test_total_bid_volume_sums_all_levels() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(3))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(98), dec!(2))).unwrap();
assert_eq!(b.total_bid_volume(), dec!(5));
}
#[test]
fn test_total_ask_volume_sums_all_levels() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(4))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(1))).unwrap();
assert_eq!(b.total_ask_volume(), dec!(5));
}
#[test]
fn test_total_bid_volume_zero_for_empty_side() {
let b = book("BTC-USD");
assert_eq!(b.total_bid_volume(), dec!(0));
}
#[test]
fn test_bid_levels_above_counts_strictly_above() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(101), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(102), dec!(1))).unwrap();
assert_eq!(b.bid_levels_above(dec!(100)), 2);
}
#[test]
fn test_bid_levels_above_zero_when_none_above() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
assert_eq!(b.bid_levels_above(dec!(100)), 0);
}
#[test]
fn test_ask_levels_below_counts_strictly_below() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(100), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(1))).unwrap();
assert_eq!(b.ask_levels_below(dec!(102)), 2);
}
#[test]
fn test_ask_levels_below_zero_for_empty_book() {
let b = book("BTC-USD");
assert_eq!(b.ask_levels_below(dec!(100)), 0);
}
#[test]
fn test_bid_ask_volume_ratio_returns_correct_ratio() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(3))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
let ratio = b.bid_ask_volume_ratio().unwrap();
assert!((ratio - 3.0).abs() < 1e-10);
}
#[test]
fn test_bid_ask_volume_ratio_none_when_ask_empty() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
assert!(b.bid_ask_volume_ratio().is_none());
}
#[test]
fn test_bid_ask_volume_ratio_none_when_bid_empty() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
assert!(b.bid_ask_volume_ratio().is_none());
}
#[test]
fn test_top_n_bid_volume_sums_top_levels() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap(); b.apply(delta("BTC-USD", BookSide::Bid, dec!(101), dec!(2))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(102), dec!(3))).unwrap(); assert_eq!(b.top_n_bid_volume(2), dec!(5));
}
#[test]
fn test_top_n_bid_volume_all_when_n_exceeds_levels() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(101), dec!(2))).unwrap();
assert_eq!(b.top_n_bid_volume(5), dec!(3));
}
#[test]
fn test_top_n_bid_volume_zero_for_empty_book() {
let b = book("BTC-USD");
assert_eq!(b.top_n_bid_volume(3), dec!(0));
}
#[test]
fn test_imbalance_ratio_positive_when_more_bids() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(3))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
let ratio = b.imbalance_ratio().unwrap();
assert!((ratio - 0.5).abs() < 1e-10);
}
#[test]
fn test_imbalance_ratio_negative_when_more_asks() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(3))).unwrap();
let ratio = b.imbalance_ratio().unwrap();
assert!((ratio - (-0.5)).abs() < 1e-10);
}
#[test]
fn test_imbalance_ratio_none_when_both_empty() {
let b = book("BTC-USD");
assert!(b.imbalance_ratio().is_none());
}
#[test]
fn test_top_n_ask_volume_sums_lowest_asks() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(100), dec!(2))).unwrap(); b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(3))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(5))).unwrap(); assert_eq!(b.top_n_ask_volume(2), dec!(5));
}
#[test]
fn test_top_n_ask_volume_zero_for_empty_book() {
let b = book("BTC-USD");
assert_eq!(b.top_n_ask_volume(3), dec!(0));
}
#[test]
fn test_has_ask_at_true_when_ask_exists() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(2))).unwrap();
assert!(b.has_ask_at(dec!(101)));
}
#[test]
fn test_has_ask_at_false_when_no_ask_at_price() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(1))).unwrap();
assert!(!b.has_ask_at(dec!(101)));
}
#[test]
fn test_bid_ask_depth_correct_counts() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
assert_eq!(b.bid_ask_depth(), (2, 1));
}
#[test]
fn test_bid_ask_depth_zero_for_empty_book() {
let b = book("BTC-USD");
assert_eq!(b.bid_ask_depth(), (0, 0));
}
#[test]
fn test_best_bid_qty_returns_top_bid_quantity() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(3))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(5))).unwrap();
assert_eq!(b.best_bid_qty(), Some(dec!(3)));
}
#[test]
fn test_best_ask_qty_returns_top_ask_quantity() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(7))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(102), dec!(2))).unwrap();
assert_eq!(b.best_ask_qty(), Some(dec!(7)));
}
#[test]
fn test_best_bid_qty_none_when_no_bids() {
let b = book("BTC-USD");
assert!(b.best_bid_qty().is_none());
}
#[test]
fn test_best_ask_qty_none_when_no_asks() {
let b = book("BTC-USD");
assert!(b.best_ask_qty().is_none());
}
#[test]
fn test_total_book_volume_sum_of_bids_and_asks() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(3))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(2))).unwrap();
assert_eq!(b.total_book_volume(), dec!(5));
}
#[test]
fn test_total_book_volume_zero_on_empty_book() {
let b = book("BTC-USD");
assert_eq!(b.total_book_volume(), dec!(0));
}
#[test]
fn test_price_range_bids_correct_range() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(99), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Bid, dec!(97), dec!(1))).unwrap();
assert_eq!(b.price_range_bids(), Some(dec!(3)));
}
#[test]
fn test_price_range_bids_none_with_single_bid() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
assert!(b.price_range_bids().is_none());
}
#[test]
fn test_is_tight_spread_true_when_spread_at_threshold() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(101), dec!(1))).unwrap();
assert!(b.is_tight_spread(dec!(1)));
}
#[test]
fn test_is_tight_spread_false_when_spread_above_threshold() {
let mut b = book("BTC-USD");
b.apply(delta("BTC-USD", BookSide::Bid, dec!(100), dec!(1))).unwrap();
b.apply(delta("BTC-USD", BookSide::Ask, dec!(103), dec!(1))).unwrap();
assert!(!b.is_tight_spread(dec!(1)));
}
#[test]
fn test_is_tight_spread_false_when_empty() {
let b = book("BTC-USD");
assert!(!b.is_tight_spread(dec!(10)));
}
#[test]
fn test_best_bid_price_returns_price() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(99), dec!(5))).unwrap();
b.apply(delta("X", BookSide::Bid, dec!(98), dec!(3))).unwrap();
assert_eq!(b.best_bid_price(), Some(dec!(99)));
}
#[test]
fn test_best_ask_price_returns_price() {
let mut b = book("X");
b.apply(delta("X", BookSide::Ask, dec!(101), dec!(5))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(102), dec!(3))).unwrap();
assert_eq!(b.best_ask_price(), Some(dec!(101)));
}
#[test]
fn test_best_bid_price_none_when_empty() {
assert_eq!(book("X").best_bid_price(), None);
}
#[test]
fn test_best_ask_price_none_when_empty() {
assert_eq!(book("X").best_ask_price(), None);
}
#[test]
fn test_is_crossed_false_when_normal() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
assert!(!b.is_crossed());
}
#[test]
fn test_is_crossed_false_when_empty() {
assert!(!book("X").is_crossed());
}
#[test]
fn test_has_bids_true_when_bid_present() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
assert!(b.has_bids());
}
#[test]
fn test_has_bids_false_when_empty() {
assert!(!book("X").has_bids());
}
#[test]
fn test_has_asks_true_when_ask_present() {
let mut b = book("X");
b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
assert!(b.has_asks());
}
#[test]
fn test_has_asks_false_when_empty() {
assert!(!book("X").has_asks());
}
#[test]
fn test_ask_price_range_none_when_empty() {
assert_eq!(book("X").ask_price_range(), None);
}
#[test]
fn test_ask_price_range_zero_when_single_level() {
let mut b = book("X");
b.apply(delta("X", BookSide::Ask, dec!(100), dec!(1))).unwrap();
assert_eq!(b.ask_price_range(), Some(dec!(0)));
}
#[test]
fn test_ask_price_range_correct_with_multiple_levels() {
let mut b = book("X");
b.apply(delta("X", BookSide::Ask, dec!(100), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(102), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(105), dec!(1))).unwrap();
assert_eq!(b.ask_price_range(), Some(dec!(5)));
}
#[test]
fn test_bid_price_range_none_when_empty() {
assert_eq!(book("X").bid_price_range(), None);
}
#[test]
fn test_bid_price_range_correct_with_multiple_levels() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(98), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Bid, dec!(96), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Bid, dec!(94), dec!(1))).unwrap();
assert_eq!(b.bid_price_range(), Some(dec!(4)));
}
#[test]
fn test_mid_spread_ratio_none_when_empty() {
assert_eq!(book("X").mid_spread_ratio(), None);
}
#[test]
fn test_mid_spread_ratio_correct() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
let ratio = b.mid_spread_ratio().unwrap();
assert!((ratio - 0.02).abs() < 1e-9);
}
#[test]
fn test_volume_imbalance_none_when_empty() {
assert_eq!(book("X").volume_imbalance(), None);
}
#[test]
fn test_volume_imbalance_positive_when_more_bids() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(99), dec!(3))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
let imb = b.volume_imbalance().unwrap();
assert!((imb - 0.5).abs() < 1e-9);
}
#[test]
fn test_volume_imbalance_negative_when_more_asks() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(101), dec!(3))).unwrap();
let imb = b.volume_imbalance().unwrap();
assert!((imb - (-0.5)).abs() < 1e-9);
}
#[test]
fn test_volume_imbalance_zero_when_equal() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(99), dec!(2))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(101), dec!(2))).unwrap();
let imb = b.volume_imbalance().unwrap();
assert!(imb.abs() < 1e-9);
}
#[test]
fn test_ask_volume_within_sums_levels_in_range() {
let mut b = book("X");
b.apply(delta("X", BookSide::Ask, dec!(100), dec!(2))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(101), dec!(3))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(105), dec!(10))).unwrap();
let vol = b.ask_volume_within(dec!(2));
assert_eq!(vol, dec!(5)); }
#[test]
fn test_ask_volume_within_zero_when_empty() {
assert_eq!(book("X").ask_volume_within(dec!(10)), dec!(0));
}
#[test]
fn test_bid_volume_within_sums_levels_in_range() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(100), dec!(5))).unwrap();
b.apply(delta("X", BookSide::Bid, dec!(99), dec!(3))).unwrap();
b.apply(delta("X", BookSide::Bid, dec!(95), dec!(10))).unwrap();
let vol = b.bid_volume_within(dec!(2));
assert_eq!(vol, dec!(8)); }
#[test]
fn test_bid_volume_within_zero_when_empty() {
assert_eq!(book("X").bid_volume_within(dec!(10)), dec!(0));
}
#[test]
fn test_ask_level_count_zero_when_empty() {
assert_eq!(book("X").ask_level_count(), 0);
}
#[test]
fn test_ask_level_count_correct() {
let mut b = book("X");
b.apply(delta("X", BookSide::Ask, dec!(100), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(101), dec!(2))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(102), dec!(3))).unwrap();
assert_eq!(b.ask_level_count(), 3);
}
#[test]
fn test_bid_level_count_correct() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Bid, dec!(98), dec!(2))).unwrap();
assert_eq!(b.bid_level_count(), 2);
}
#[test]
fn test_price_impact_buy_correct_single_level() {
let mut b = book("X");
b.apply(delta("X", BookSide::Ask, dec!(100), dec!(10))).unwrap();
assert_eq!(b.price_impact_buy(dec!(5)), Some(dec!(100)));
}
#[test]
fn test_price_impact_buy_spans_levels() {
let mut b = book("X");
b.apply(delta("X", BookSide::Ask, dec!(100), dec!(5))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(101), dec!(5))).unwrap();
assert_eq!(b.price_impact_buy(dec!(10)), Some(dec!(100.5)));
}
#[test]
fn test_price_impact_buy_none_when_insufficient_liquidity() {
let mut b = book("X");
b.apply(delta("X", BookSide::Ask, dec!(100), dec!(3))).unwrap();
assert!(b.price_impact_buy(dec!(5)).is_none());
}
#[test]
fn test_price_impact_sell_correct_single_level() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(99), dec!(10))).unwrap();
assert_eq!(b.price_impact_sell(dec!(5)), Some(dec!(99)));
}
#[test]
fn test_total_value_at_level_bid_returns_price_times_qty() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(100), dec!(5))).unwrap();
assert_eq!(b.total_value_at_level(BookSide::Bid, dec!(100)), Some(dec!(500)));
}
#[test]
fn test_total_value_at_level_ask_returns_price_times_qty() {
let mut b = book("X");
b.apply(delta("X", BookSide::Ask, dec!(105), dec!(3))).unwrap();
assert_eq!(b.total_value_at_level(BookSide::Ask, dec!(105)), Some(dec!(315)));
}
#[test]
fn test_total_value_at_level_none_when_price_missing() {
let b = book("X");
assert!(b.total_value_at_level(BookSide::Bid, dec!(100)).is_none());
}
#[test]
fn test_ask_volume_above_sums_asks_above_price() {
let mut b = book("X");
b.apply(delta("X", BookSide::Ask, dec!(101), dec!(5))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(102), dec!(3))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(103), dec!(2))).unwrap();
assert_eq!(b.ask_volume_above(dec!(101)), dec!(5));
}
#[test]
fn test_ask_volume_above_zero_when_no_asks_above() {
let mut b = book("X");
b.apply(delta("X", BookSide::Ask, dec!(100), dec!(10))).unwrap();
assert_eq!(b.ask_volume_above(dec!(100)), dec!(0));
}
#[test]
fn test_bid_volume_below_sums_bids_below_price() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(98), dec!(4))).unwrap();
b.apply(delta("X", BookSide::Bid, dec!(99), dec!(6))).unwrap();
b.apply(delta("X", BookSide::Bid, dec!(100), dec!(2))).unwrap();
assert_eq!(b.bid_volume_below(dec!(100)), dec!(10));
}
#[test]
fn test_bid_volume_below_zero_when_no_bids_below() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(100), dec!(5))).unwrap();
assert_eq!(b.bid_volume_below(dec!(100)), dec!(0));
}
#[test]
fn test_total_notional_both_sides_sums_both() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(100), dec!(2))).unwrap(); b.apply(delta("X", BookSide::Ask, dec!(105), dec!(3))).unwrap(); assert_eq!(b.total_notional_both_sides(), dec!(515));
}
#[test]
fn test_total_notional_both_sides_zero_when_empty() {
let b = book("X");
assert_eq!(b.total_notional_both_sides(), dec!(0));
}
#[test]
fn test_level_count_both_sides_zero_when_empty() {
let b = book("X");
assert_eq!(b.level_count_both_sides(), 0);
}
#[test]
fn test_level_count_both_sides_counts_all_levels() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Bid, dec!(98), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
assert_eq!(b.level_count_both_sides(), 3);
}
#[test]
fn test_ask_price_at_rank_best_ask_at_zero() {
let mut b = book("X");
b.apply(delta("X", BookSide::Ask, dec!(101), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Ask, dec!(102), dec!(1))).unwrap();
assert_eq!(b.ask_price_at_rank(0), Some(dec!(101)));
}
#[test]
fn test_bid_price_at_rank_best_bid_at_zero() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(99), dec!(1))).unwrap();
b.apply(delta("X", BookSide::Bid, dec!(98), dec!(1))).unwrap();
assert_eq!(b.bid_price_at_rank(0), Some(dec!(99)));
}
#[test]
fn test_ask_price_at_rank_none_out_of_bounds() {
let b = book("X");
assert!(b.ask_price_at_rank(0).is_none());
}
#[test]
fn test_price_level_exists_true_when_present() {
let mut b = book("X");
b.apply(delta("X", BookSide::Bid, dec!(100), dec!(1))).unwrap();
assert!(b.price_level_exists(BookSide::Bid, dec!(100)));
}
#[test]
fn test_price_level_exists_false_when_absent() {
let b = book("X");
assert!(!b.price_level_exists(BookSide::Bid, dec!(100)));
}
#[test]
fn test_price_level_exists_ask_side() {
let mut b = book("X");
b.apply(delta("X", BookSide::Ask, dec!(101), dec!(5))).unwrap();
assert!(b.price_level_exists(BookSide::Ask, dec!(101)));
assert!(!b.price_level_exists(BookSide::Ask, dec!(102)));
}
}