use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, VecDeque};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Side {
Buy,
Sell,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RestingOrder {
pub id: u64,
pub agent: usize,
pub qty: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum OrderKind {
Limit {
side: Side,
price_tick: i64,
qty: u64,
},
Market {
side: Side,
qty: u64,
},
Cancel {
id: u64,
},
Modify {
id: u64,
new_qty: u64,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Fill {
pub price_tick: i64,
pub qty: u64,
pub maker_id: u64,
pub maker_agent: usize,
pub taker_agent: usize,
pub taker_side: Side,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct LadderSnapshot {
pub bids: Vec<[i64; 2]>,
pub asks: Vec<[i64; 2]>,
pub mid: f64,
pub microprice: f64,
pub queue_imbalance: f64,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct SweepCost {
pub avg_px_tick: f64,
pub slippage_ticks: f64,
pub filled_qty: u64,
}
pub struct OrderBook {
bids: BTreeMap<i64, VecDeque<RestingOrder>>,
asks: BTreeMap<i64, VecDeque<RestingOrder>>,
tick_size: f64,
next_order_id: u64,
}
impl OrderBook {
pub fn new(tick_size: f64) -> Self {
OrderBook {
bids: BTreeMap::new(),
asks: BTreeMap::new(),
tick_size,
next_order_id: 0,
}
}
pub fn tick_size(&self) -> f64 {
self.tick_size
}
pub fn next_order_id(&self) -> u64 {
self.next_order_id
}
pub fn best_bid(&self) -> Option<i64> {
self.bids.keys().next_back().copied()
}
pub fn best_ask(&self) -> Option<i64> {
self.asks.keys().next().copied()
}
pub fn process_limit(
&mut self,
side: Side,
price_tick: i64,
qty: u64,
agent: usize,
) -> Vec<Fill> {
let id = self.next_order_id;
self.next_order_id += 1;
let (fills, remaining) = self.match_against(side, Some(price_tick), qty, agent);
if remaining > 0 {
let level = match side {
Side::Buy => self.bids.entry(price_tick).or_default(),
Side::Sell => self.asks.entry(price_tick).or_default(),
};
level.push_back(RestingOrder {
id,
agent,
qty: remaining,
});
}
fills
}
pub fn process_market(&mut self, side: Side, qty: u64, agent: usize) -> Vec<Fill> {
self.match_against(side, None, qty, agent).0
}
fn match_against(
&mut self,
side: Side,
limit: Option<i64>,
qty: u64,
agent: usize,
) -> (Vec<Fill>, u64) {
let mut remaining = qty;
let mut fills = Vec::new();
while remaining > 0 {
let best = match side {
Side::Buy => self.asks.keys().next().copied(),
Side::Sell => self.bids.keys().next_back().copied(),
};
let Some(price) = best else { break };
if let Some(lim) = limit {
let crosses = match side {
Side::Buy => price <= lim,
Side::Sell => price >= lim,
};
if !crosses {
break;
}
}
let book = match side {
Side::Buy => &mut self.asks,
Side::Sell => &mut self.bids,
};
let level = book.get_mut(&price).unwrap();
while remaining > 0 {
let Some(head) = level.front_mut() else { break };
let traded = remaining.min(head.qty);
let maker_id = head.id;
let maker_agent = head.agent;
head.qty -= traded;
remaining -= traded;
if head.qty == 0 {
level.pop_front();
}
fills.push(Fill {
price_tick: price,
qty: traded,
maker_id,
maker_agent,
taker_agent: agent,
taker_side: side,
});
}
if level.is_empty() {
book.remove(&price);
}
}
(fills, remaining)
}
pub fn cancel_order(&mut self, id: u64) -> bool {
let Some((side, price)) = self.locate(id) else {
return false;
};
let book = match side {
Side::Buy => &mut self.bids,
Side::Sell => &mut self.asks,
};
let level = book.get_mut(&price).unwrap();
let pos = level.iter().position(|o| o.id == id).unwrap();
level.remove(pos);
if level.is_empty() {
book.remove(&price);
}
true
}
pub fn modify_order(&mut self, id: u64, new_qty: u64) -> bool {
if new_qty == 0 {
return self.cancel_order(id);
}
let Some((side, price)) = self.locate(id) else {
return false;
};
let book = match side {
Side::Buy => &mut self.bids,
Side::Sell => &mut self.asks,
};
let level = book.get_mut(&price).unwrap();
let pos = level.iter().position(|o| o.id == id).unwrap();
if new_qty <= level[pos].qty {
level[pos].qty = new_qty;
} else {
let mut ord = level.remove(pos).unwrap();
ord.qty = new_qty;
level.push_back(ord);
}
true
}
fn locate(&self, id: u64) -> Option<(Side, i64)> {
for (&price, level) in &self.bids {
if level.iter().any(|o| o.id == id) {
return Some((Side::Buy, price));
}
}
for (&price, level) in &self.asks {
if level.iter().any(|o| o.id == id) {
return Some((Side::Sell, price));
}
}
None
}
pub fn step(&mut self, orders: &[(usize, OrderKind)]) -> Vec<Fill> {
let mut idx: Vec<usize> = (0..orders.len()).collect();
idx.sort_by_key(|&i| (orders[i].0, i));
let mut fills = Vec::new();
for i in idx {
let (agent, kind) = orders[i];
match kind {
OrderKind::Limit {
side,
price_tick,
qty,
} => fills.extend(self.process_limit(side, price_tick, qty, agent)),
OrderKind::Market { side, qty } => {
fills.extend(self.process_market(side, qty, agent))
}
OrderKind::Cancel { id } => {
self.cancel_order(id);
}
OrderKind::Modify { id, new_qty } => {
self.modify_order(id, new_qty);
}
}
}
fills
}
pub fn depth_ladder(&self, levels: usize) -> LadderSnapshot {
let bids: Vec<[i64; 2]> = self
.bids
.iter()
.rev()
.take(levels)
.map(|(&p, q)| [p, level_qty(q) as i64])
.collect();
let asks: Vec<[i64; 2]> = self
.asks
.iter()
.take(levels)
.map(|(&p, q)| [p, level_qty(q) as i64])
.collect();
let best_bid = self.bids.iter().next_back();
let best_ask = self.asks.iter().next();
let (mid, microprice, queue_imbalance) = match (best_bid, best_ask) {
(Some((&bp, bq)), Some((&ap, aq))) => {
let bqty = level_qty(bq) as f64;
let aqty = level_qty(aq) as f64;
let total = bqty + aqty;
let mid = (bp + ap) as f64 / 2.0;
let micro = (bp as f64 * aqty + ap as f64 * bqty) / total;
let imb = (bqty - aqty) / total;
(mid, micro, imb)
}
(Some((&bp, _)), None) => (bp as f64, bp as f64, 1.0),
(None, Some((&ap, _))) => (ap as f64, ap as f64, -1.0),
(None, None) => (0.0, 0.0, 0.0),
};
LadderSnapshot {
bids,
asks,
mid,
microprice,
queue_imbalance,
}
}
pub fn uncross(&self) -> Option<(i64, u64)> {
let mut candidates: Vec<i64> = self.bids.keys().chain(self.asks.keys()).copied().collect();
candidates.sort_unstable();
candidates.dedup();
let mut best: Option<(u64, u64, i64)> = None;
for &p in &candidates {
let demand: u64 = self.bids.range(p..).map(|(_, l)| level_qty(l)).sum();
let supply: u64 = self.asks.range(..=p).map(|(_, l)| level_qty(l)).sum();
let matched = demand.min(supply);
if matched == 0 {
continue;
}
let imbalance = demand.abs_diff(supply);
let better = match best {
None => true,
Some((bm, bi, bt)) => {
matched > bm
|| (matched == bm && imbalance < bi)
|| (matched == bm && imbalance == bi && p < bt)
}
};
if better {
best = Some((matched, imbalance, p));
}
}
best.map(|(matched, _, tick)| (tick, matched))
}
pub fn sweep_cost(&self, side: Side, qty: u64) -> SweepCost {
let mut remaining = qty;
let mut filled: u64 = 0;
let mut notional: i128 = 0;
let best = match side {
Side::Buy => self.best_ask(),
Side::Sell => self.best_bid(),
};
let mut consume = |price: i64, level: &VecDeque<RestingOrder>| {
if remaining == 0 {
return;
}
let take = remaining.min(level_qty(level));
notional += price as i128 * take as i128;
filled += take;
remaining -= take;
};
match side {
Side::Buy => {
for (&price, level) in self.asks.iter() {
consume(price, level);
}
}
Side::Sell => {
for (&price, level) in self.bids.iter().rev() {
consume(price, level);
}
}
}
let avg_px_tick = if filled > 0 {
notional as f64 / filled as f64
} else {
0.0
};
let slippage_ticks = match best {
Some(b) if filled > 0 => (avg_px_tick - b as f64).abs(),
_ => 0.0,
};
SweepCost {
avg_px_tick,
slippage_ticks,
filled_qty: filled,
}
}
}
fn level_qty(level: &VecDeque<RestingOrder>) -> u64 {
level.iter().map(|o| o.qty).sum()
}
#[cfg(test)]
mod tests {
use super::*;
fn fnv1a(bytes: &[u8]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
const GOLDEN_TAPE_FNV1A: u64 = 0x8bbc_a7c3_2cea_d625;
#[test]
fn time_priority_earlier_order_fills_first() {
let mut b = OrderBook::new(1.0);
let a_id = b.next_order_id();
b.process_limit(Side::Sell, 100, 5, 0);
let _b_id = b.next_order_id();
b.process_limit(Side::Sell, 100, 5, 1);
let fills = b.process_limit(Side::Buy, 100, 1, 2);
assert_eq!(fills.len(), 1);
assert_eq!(fills[0].maker_id, a_id);
assert_eq!(fills[0].maker_agent, 0);
}
#[test]
fn partial_fill_decrements_the_head() {
let mut b = OrderBook::new(1.0);
let a_id = b.next_order_id();
b.process_limit(Side::Sell, 100, 10, 0);
let fills = b.process_market(Side::Buy, 4, 1);
assert_eq!(fills, vec![single(100, 4, a_id, 0, 1, Side::Buy)]);
let more = b.process_market(Side::Buy, 6, 2);
assert_eq!(more, vec![single(100, 6, a_id, 0, 2, Side::Buy)]);
assert!(b.best_ask().is_none());
}
#[test]
fn crossing_limit_matches_then_rests_remainder() {
let mut b = OrderBook::new(1.0);
b.process_limit(Side::Sell, 100, 3, 0);
let rest_id = b.next_order_id();
let fills = b.process_limit(Side::Buy, 100, 8, 1);
assert_eq!(fills.len(), 1);
assert_eq!(fills[0].qty, 3);
assert_eq!(b.best_bid(), Some(100));
assert!(b.best_ask().is_none());
let ladder = b.depth_ladder(1);
assert_eq!(ladder.bids, vec![[100, 5]]);
assert!(b.cancel_order(rest_id));
assert!(b.best_bid().is_none());
}
#[test]
fn market_order_walks_multiple_levels() {
let mut b = OrderBook::new(1.0);
b.process_limit(Side::Sell, 100, 2, 0);
b.process_limit(Side::Sell, 101, 2, 1);
b.process_limit(Side::Sell, 102, 2, 2);
let fills = b.process_market(Side::Buy, 5, 9);
let prices: Vec<i64> = fills.iter().map(|f| f.price_tick).collect();
let qtys: Vec<u64> = fills.iter().map(|f| f.qty).collect();
assert_eq!(prices, vec![100, 101, 102]);
assert_eq!(qtys, vec![2, 2, 1]);
assert_eq!(b.depth_ladder(3).asks, vec![[102, 1]]);
}
#[test]
fn cancel_removes_and_frees_the_level() {
let mut b = OrderBook::new(1.0);
let id = b.next_order_id();
b.process_limit(Side::Buy, 99, 7, 0);
assert!(b.cancel_order(id));
assert!(b.best_bid().is_none());
assert!(b.process_market(Side::Sell, 1, 1).is_empty());
assert!(!b.cancel_order(id));
}
#[test]
fn modify_decrease_keeps_increase_loses_priority() {
let mut b = OrderBook::new(1.0);
let a_id = b.next_order_id();
b.process_limit(Side::Buy, 100, 10, 0);
let b_id = b.next_order_id();
b.process_limit(Side::Buy, 100, 10, 1);
assert!(b.modify_order(a_id, 5));
assert_eq!(b.process_market(Side::Sell, 1, 9)[0].maker_id, a_id);
assert!(b.modify_order(a_id, 8));
assert_eq!(b.process_market(Side::Sell, 1, 9)[0].maker_id, b_id);
}
#[test]
fn step_is_canonical_order_deterministic() {
let seed_book = || {
let mut b = OrderBook::new(1.0);
b.process_limit(Side::Sell, 105, 100, 7);
b.process_limit(Side::Buy, 95, 100, 7);
b
};
let batch: Vec<(usize, OrderKind)> = vec![
(
0,
OrderKind::Limit {
side: Side::Buy,
price_tick: 105,
qty: 5,
},
),
(
1,
OrderKind::Limit {
side: Side::Sell,
price_tick: 95,
qty: 7,
},
),
(
2,
OrderKind::Limit {
side: Side::Buy,
price_tick: 106,
qty: 3,
},
),
];
let mut reversed = batch.clone();
reversed.reverse();
let mut forward = seed_book();
let mut backward = seed_book();
let f1 = forward.step(&batch);
let f2 = backward.step(&reversed);
assert_eq!(
serde_json::to_string(&f1).unwrap(),
serde_json::to_string(&f2).unwrap(),
"a re-sorted batch must yield the identical tape"
);
assert!(!f1.is_empty());
}
#[test]
fn empty_book_market_order_is_a_noop() {
let mut b = OrderBook::new(1.0);
assert!(b.process_market(Side::Buy, 10, 0).is_empty());
assert!(b.process_market(Side::Sell, 10, 0).is_empty());
let ladder = b.depth_ladder(5);
assert!(ladder.bids.is_empty() && ladder.asks.is_empty());
assert_eq!(ladder.mid, 0.0);
assert_eq!(ladder.microprice, 0.0);
assert_eq!(ladder.queue_imbalance, 0.0);
}
#[test]
fn ladder_derives_mid_microprice_and_imbalance() {
let mut b = OrderBook::new(1.0);
b.process_limit(Side::Buy, 100, 6, 0);
b.process_limit(Side::Sell, 102, 2, 1);
let l = b.depth_ladder(1);
assert_eq!(l.bids, vec![[100, 6]]);
assert_eq!(l.asks, vec![[102, 2]]);
assert_eq!(l.mid, 101.0);
assert_eq!(l.microprice, (100.0 * 2.0 + 102.0 * 6.0) / 8.0);
assert_eq!(l.queue_imbalance, (6.0 - 2.0) / 8.0);
}
fn single(
price_tick: i64,
qty: u64,
maker_id: u64,
maker_agent: usize,
taker_agent: usize,
taker_side: Side,
) -> Fill {
Fill {
price_tick,
qty,
maker_id,
maker_agent,
taker_agent,
taker_side,
}
}
fn scripted_tape() -> Vec<Fill> {
let mut b = OrderBook::new(0.01);
let mut tape = Vec::new();
let batch: Vec<(usize, OrderKind)> = vec![
(
2,
OrderKind::Limit {
side: Side::Sell,
price_tick: 102,
qty: 4,
},
),
(
0,
OrderKind::Limit {
side: Side::Sell,
price_tick: 101,
qty: 5,
},
),
(
1,
OrderKind::Limit {
side: Side::Sell,
price_tick: 101,
qty: 3,
},
),
(
3,
OrderKind::Limit {
side: Side::Buy,
price_tick: 100,
qty: 6,
},
),
];
tape.extend(b.step(&batch));
let modify_id = b.next_order_id();
b.process_limit(Side::Buy, 100, 4, 5);
b.modify_order(modify_id, 2);
tape.extend(b.process_limit(Side::Buy, 102, 10, 6));
tape.extend(b.process_market(Side::Sell, 7, 7));
tape
}
#[test]
fn golden_tape_hash_is_stable() {
let json = serde_json::to_string(&scripted_tape()).unwrap();
assert_eq!(fnv1a(json.as_bytes()), GOLDEN_TAPE_FNV1A);
}
#[test]
fn scripted_tape_is_reproducible() {
assert_eq!(scripted_tape(), scripted_tape());
}
fn rest(book: &mut OrderBook, side: Side, price: i64, qty: u64, agent: usize) {
let id = book.next_order_id;
book.next_order_id += 1;
let level = match side {
Side::Buy => book.bids.entry(price).or_default(),
Side::Sell => book.asks.entry(price).or_default(),
};
level.push_back(RestingOrder { id, agent, qty });
}
#[test]
fn uncross_picks_the_volume_maximizing_tick() {
let mut b = OrderBook::new(1.0);
for (p, q) in [(102, 5), (101, 5), (100, 5)] {
rest(&mut b, Side::Buy, p, q, 0);
}
for (p, q) in [(100, 5), (101, 5), (102, 5)] {
rest(&mut b, Side::Sell, p, q, 1);
}
assert_eq!(b.uncross(), Some((101, 10)));
}
#[test]
fn uncross_returns_none_on_an_uncrossed_book() {
let mut b = OrderBook::new(1.0);
b.process_limit(Side::Buy, 99, 10, 0);
b.process_limit(Side::Sell, 101, 10, 1);
assert_eq!(b.uncross(), None);
}
#[test]
fn uncross_tie_breaks_by_lower_tick_when_imbalance_is_equal() {
let mut b = OrderBook::new(1.0);
rest(&mut b, Side::Buy, 101, 10, 0);
rest(&mut b, Side::Buy, 100, 10, 0);
rest(&mut b, Side::Sell, 100, 10, 1);
rest(&mut b, Side::Sell, 101, 10, 1);
assert_eq!(b.uncross(), Some((100, 10)));
}
#[test]
fn uncross_tie_breaks_by_min_imbalance_over_tick() {
let mut b = OrderBook::new(1.0);
rest(&mut b, Side::Buy, 102, 10, 0);
rest(&mut b, Side::Buy, 100, 15, 0);
rest(&mut b, Side::Sell, 100, 10, 1);
rest(&mut b, Side::Sell, 102, 5, 1);
assert_eq!(b.uncross(), Some((102, 10)));
}
#[test]
fn sweep_cost_walks_asks_and_leaves_the_book_unchanged() {
let mut b = OrderBook::new(1.0);
b.process_limit(Side::Sell, 100, 2, 0);
b.process_limit(Side::Sell, 101, 2, 1);
b.process_limit(Side::Sell, 102, 2, 2);
let before = b.depth_ladder(3);
let c = b.sweep_cost(Side::Buy, 5);
assert_eq!(c.filled_qty, 5);
assert_eq!(c.avg_px_tick, 504.0 / 5.0);
assert_eq!(c.slippage_ticks, (504.0_f64 / 5.0 - 100.0).abs());
assert_eq!(b.depth_ladder(3), before);
assert_eq!(b.best_ask(), Some(100));
}
#[test]
fn sweep_cost_walks_bids_for_a_sell() {
let mut b = OrderBook::new(1.0);
b.process_limit(Side::Buy, 100, 2, 0);
b.process_limit(Side::Buy, 99, 2, 1);
b.process_limit(Side::Buy, 98, 2, 2);
let c = b.sweep_cost(Side::Sell, 5);
assert_eq!(c.filled_qty, 5);
assert_eq!(c.avg_px_tick, 496.0 / 5.0);
assert_eq!(c.slippage_ticks, (100.0_f64 - 496.0 / 5.0).abs());
assert_eq!(b.best_bid(), Some(100));
}
#[test]
fn sweep_cost_partial_fills_when_depth_runs_dry() {
let mut b = OrderBook::new(1.0);
b.process_limit(Side::Sell, 101, 6, 0);
let c = b.sweep_cost(Side::Buy, 100);
assert_eq!(c.filled_qty, 6);
assert_eq!(c.avg_px_tick, 101.0);
assert_eq!(c.slippage_ticks, 0.0);
}
#[test]
fn sweep_cost_on_an_empty_book_fills_nothing() {
let b = OrderBook::new(1.0);
let c = b.sweep_cost(Side::Buy, 10);
assert_eq!(c.filled_qty, 0);
assert_eq!(c.avg_px_tick, 0.0);
assert_eq!(c.slippage_ticks, 0.0);
}
}