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,
}
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,
}
}
}
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());
}
}