Skip to content

Commit f830f26

Browse files
committed
feat: add simple order book
1 parent 8890620 commit f830f26

File tree

3 files changed

+361
-0
lines changed

3 files changed

+361
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ ndarray-rand = "0.15.0"
4747
ndarray-stats = "0.6.0"
4848
ndrustfft = "0.5.0"
4949
num-complex = { version = "0.4.6", features = ["rand"] }
50+
ordered-float = "5.0.0"
5051
plotly = { version = "0.10.0", features = ["plotly_ndarray"] }
5152
polars = { version = "0.43.1", features = ["lazy"], optional = true }
5253
prettytable-rs = "0.10.0"

src/quant.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::fmt::Display;
22

33
pub mod bonds;
44
pub mod calibration;
5+
pub mod order_book;
56
pub mod pricing;
67
pub mod strategies;
78
pub mod r#trait;

src/quant/order_book.rs

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
use ordered_float::OrderedFloat;
2+
use std::collections::{BTreeMap, HashMap, VecDeque};
3+
use std::time::{SystemTime, UNIX_EPOCH};
4+
5+
/// A total‑ordered price key (wraps `f64` so it can live in a `BTreeMap`).
6+
pub type Price = OrderedFloat<f64>;
7+
8+
/// Order side.
9+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10+
pub enum Side {
11+
Buy,
12+
Sell,
13+
}
14+
15+
/// A single limit order resting in the order book.
16+
#[derive(Debug, Clone)]
17+
pub struct Order {
18+
pub id: u64,
19+
pub side: Side,
20+
pub price: f64,
21+
pub size: f64,
22+
pub timestamp: u128, // µs since Unix epoch – used for *time* priority
23+
}
24+
25+
/// Executed trade (taker × maker).
26+
#[derive(Debug, Clone, PartialEq)]
27+
pub struct Trade {
28+
pub taker_side: Side,
29+
pub price: f64,
30+
pub size: f64,
31+
pub taker_id: u64,
32+
pub maker_id: u64,
33+
}
34+
35+
pub struct OrderBook {
36+
bids: BTreeMap<Price, VecDeque<Order>>, // best bid = last key
37+
asks: BTreeMap<Price, VecDeque<Order>>, // best ask = first key
38+
index: HashMap<u64, (Side, Price)>, // id → (side, price) for O(1) cancel
39+
next_id: u64,
40+
}
41+
42+
impl OrderBook {
43+
/// New empty book.
44+
pub fn new() -> Self {
45+
Self {
46+
bids: BTreeMap::default(),
47+
asks: BTreeMap::default(),
48+
index: HashMap::new(),
49+
next_id: 0,
50+
}
51+
}
52+
53+
/// Execute a market order – consume liquidity until either the desired
54+
/// `size` is filled or the book runs out of contra‑side orders. The order
55+
/// never rests in the book.
56+
pub fn execute_order(&mut self, side: Side, mut size: f64) -> (u64, Vec<Trade>, f64) {
57+
assert!(size > 0.0, "size must be positive");
58+
self.next_id += 1;
59+
let taker_id = self.next_id;
60+
let mut trades = Vec::new();
61+
62+
match side {
63+
Side::Buy => {
64+
let mut empty_prices = Vec::new();
65+
let price_keys: Vec<Price> = self.asks.keys().copied().collect();
66+
for ask_price in price_keys {
67+
if size == 0.0 {
68+
break;
69+
}
70+
if let Some(queue) = self.asks.get_mut(&ask_price) {
71+
while size > 0.0 {
72+
if let Some(maker) = queue.front_mut() {
73+
let traded = size.min(maker.size);
74+
size -= traded;
75+
maker.size -= traded;
76+
trades.push(Trade {
77+
taker_side: Side::Buy,
78+
price: maker.price,
79+
size: traded,
80+
taker_id,
81+
maker_id: maker.id,
82+
});
83+
if maker.size == 0.0 {
84+
let maker_id = maker.id;
85+
queue.pop_front();
86+
self.index.remove(&maker_id);
87+
}
88+
} else {
89+
break;
90+
}
91+
}
92+
if queue.is_empty() {
93+
empty_prices.push(ask_price);
94+
}
95+
}
96+
}
97+
for p in empty_prices {
98+
self.asks.remove(&p);
99+
}
100+
}
101+
Side::Sell => {
102+
let mut empty_prices = Vec::new();
103+
let price_keys: Vec<Price> = self.bids.keys().copied().collect();
104+
for bid_price in price_keys.into_iter().rev() {
105+
// high → low
106+
if size == 0.0 {
107+
break;
108+
}
109+
if let Some(queue) = self.bids.get_mut(&bid_price) {
110+
while size > 0.0 {
111+
if let Some(maker) = queue.front_mut() {
112+
let traded = size.min(maker.size);
113+
size -= traded;
114+
maker.size -= traded;
115+
trades.push(Trade {
116+
taker_side: Side::Sell,
117+
price: maker.price,
118+
size: traded,
119+
taker_id,
120+
maker_id: maker.id,
121+
});
122+
if maker.size == 0.0 {
123+
let maker_id = maker.id;
124+
queue.pop_front();
125+
self.index.remove(&maker_id);
126+
}
127+
} else {
128+
break;
129+
}
130+
}
131+
if queue.is_empty() {
132+
empty_prices.push(bid_price);
133+
}
134+
}
135+
}
136+
for p in empty_prices {
137+
self.bids.remove(&p);
138+
}
139+
}
140+
}
141+
142+
(taker_id, trades, size) // any leftover size could not be executed
143+
}
144+
145+
/// Add limit order.
146+
pub fn add_order(&mut self, side: Side, price: f64, mut size: f64) -> (u64, Vec<Trade>) {
147+
assert!(size > 0.0, "size must be positive");
148+
self.next_id += 1;
149+
let taker_id = self.next_id;
150+
let timestamp = SystemTime::now()
151+
.duration_since(UNIX_EPOCH)
152+
.expect("time went backwards")
153+
.as_micros();
154+
let mut trades = Vec::<Trade>::new();
155+
156+
match side {
157+
Side::Buy => {
158+
let mut empty_prices = Vec::new();
159+
let cross_prices: Vec<Price> = self
160+
.asks
161+
.range_mut(..=Price::from(price))
162+
.map(|(&p, _)| p)
163+
.collect();
164+
for ask_price in cross_prices {
165+
if size == 0.0 {
166+
break;
167+
}
168+
if let Some(queue) = self.asks.get_mut(&ask_price) {
169+
while size > 0.0 {
170+
if let Some(maker) = queue.front_mut() {
171+
let traded = size.min(maker.size);
172+
size -= traded;
173+
maker.size -= traded;
174+
trades.push(Trade {
175+
taker_side: Side::Buy,
176+
price: maker.price,
177+
size: traded,
178+
taker_id,
179+
maker_id: maker.id,
180+
});
181+
if maker.size == 0.0 {
182+
// remove maker order → also drop from index
183+
let maker_id = maker.id;
184+
queue.pop_front();
185+
self.index.remove(&maker_id);
186+
}
187+
} else {
188+
break;
189+
}
190+
}
191+
if queue.is_empty() {
192+
empty_prices.push(ask_price);
193+
}
194+
}
195+
}
196+
for p in empty_prices {
197+
self.asks.remove(&p);
198+
}
199+
if size > 0.0 {
200+
let order = Order {
201+
id: taker_id,
202+
side,
203+
price,
204+
size,
205+
timestamp,
206+
};
207+
self
208+
.bids
209+
.entry(Price::from(price))
210+
.or_default()
211+
.push_back(order);
212+
self.index.insert(taker_id, (side, Price::from(price)));
213+
}
214+
}
215+
Side::Sell => {
216+
let mut empty_prices = Vec::new();
217+
let cross_prices: Vec<Price> = self
218+
.bids
219+
.range_mut(Price::from(price)..)
220+
.map(|(&p, _)| p)
221+
.collect();
222+
for bid_price in cross_prices.into_iter().rev() {
223+
// high → low
224+
if size == 0.0 {
225+
break;
226+
}
227+
if let Some(queue) = self.bids.get_mut(&bid_price) {
228+
while size > 0.0 {
229+
if let Some(maker) = queue.front_mut() {
230+
let traded = size.min(maker.size);
231+
size -= traded;
232+
maker.size -= traded;
233+
trades.push(Trade {
234+
taker_side: Side::Sell,
235+
price: maker.price,
236+
size: traded,
237+
taker_id,
238+
maker_id: maker.id,
239+
});
240+
if maker.size == 0.0 {
241+
let maker_id = maker.id;
242+
queue.pop_front();
243+
self.index.remove(&maker_id);
244+
}
245+
} else {
246+
break;
247+
}
248+
}
249+
if queue.is_empty() {
250+
empty_prices.push(bid_price);
251+
}
252+
}
253+
}
254+
for p in empty_prices {
255+
self.bids.remove(&p);
256+
}
257+
if size > 0.0 {
258+
let order = Order {
259+
id: taker_id,
260+
side,
261+
price,
262+
size,
263+
timestamp,
264+
};
265+
self
266+
.asks
267+
.entry(Price::from(price))
268+
.or_default()
269+
.push_back(order);
270+
self.index.insert(taker_id, (side, Price::from(price)));
271+
}
272+
}
273+
}
274+
(taker_id, trades)
275+
}
276+
277+
/// Cancel an existing order by id.
278+
pub fn cancel_order(&mut self, id: u64) -> bool {
279+
let Some((side, price)) = self.index.remove(&id) else {
280+
return false;
281+
};
282+
let book = match side {
283+
Side::Buy => &mut self.bids,
284+
Side::Sell => &mut self.asks,
285+
};
286+
if let Some(queue) = book.get_mut(&price) {
287+
if let Some(pos) = queue.iter().position(|o| o.id == id) {
288+
queue.remove(pos);
289+
}
290+
if queue.is_empty() {
291+
book.remove(&price);
292+
}
293+
}
294+
true
295+
}
296+
297+
pub fn best_bid(&self) -> Option<(f64, f64)> {
298+
let (&price, queue) = self.bids.iter().next_back()?;
299+
Some((price.into_inner(), queue.iter().map(|o| o.size).sum()))
300+
}
301+
302+
pub fn best_ask(&self) -> Option<(f64, f64)> {
303+
let (&price, queue) = self.asks.iter().next()?;
304+
Some((price.into_inner(), queue.iter().map(|o| o.size).sum()))
305+
}
306+
307+
pub fn depth(&self) -> (Vec<(f64, f64)>, Vec<(f64, f64)>) {
308+
let bids = self
309+
.bids
310+
.iter()
311+
.rev()
312+
.map(|(&p, q)| (p.into_inner(), q.iter().map(|o| o.size).sum()))
313+
.collect();
314+
let asks = self
315+
.asks
316+
.iter()
317+
.map(|(&p, q)| (p.into_inner(), q.iter().map(|o| o.size).sum()))
318+
.collect();
319+
(bids, asks)
320+
}
321+
}
322+
323+
#[cfg(test)]
324+
mod tests {
325+
use super::*;
326+
327+
#[test]
328+
fn add_and_cancel() {
329+
let mut ob = OrderBook::new();
330+
let (id1, _) = ob.add_order(Side::Buy, 10.0, 5.0);
331+
let (_id2, _) = ob.add_order(Side::Buy, 10.0, 3.0);
332+
assert_eq!(ob.best_bid().unwrap().1, 8.0);
333+
assert!(ob.cancel_order(id1));
334+
assert_eq!(ob.best_bid().unwrap().1, 3.0);
335+
assert!(!ob.cancel_order(999));
336+
}
337+
338+
#[test]
339+
fn match_flow() {
340+
let mut ob = OrderBook::new();
341+
ob.add_order(Side::Buy, 100.0, 5.0);
342+
let (_, trades) = ob.add_order(Side::Sell, 99.0, 3.0);
343+
assert_eq!(trades.len(), 1);
344+
assert_eq!(trades[0].size, 3.0);
345+
assert_eq!(ob.best_bid().unwrap().1, 2.0);
346+
}
347+
348+
#[test]
349+
fn execute_market_order() {
350+
let mut ob = OrderBook::new();
351+
ob.add_order(Side::Sell, 101.0, 4.0); // best ask
352+
ob.add_order(Side::Sell, 102.0, 2.0);
353+
let (_id, trades, leftover) = ob.execute_order(Side::Buy, 5.0);
354+
assert_eq!(trades.len(), 2);
355+
assert_eq!(leftover, 0.0);
356+
// Book now has 1 @ 102 ask remaining
357+
assert_eq!(ob.best_ask().unwrap().1, 1.0);
358+
}
359+
}

0 commit comments

Comments
 (0)