clob_sync/order_book.rs
1use nonempty::NonEmpty;
2
3use crate::execution::{Execution, Executions};
4use crate::order::Symbol;
5use crate::order::{Order, OrderSide};
6use crate::order_book_side::{BookSide, InMemoryOrderBookSide, OrderBookSide};
7use crate::prelude::*;
8
9/// A factory trait for creating order book instances.
10///
11/// Implementors provide a specific order book side implementation.
12pub trait OrderBookFactory {
13 /// The order book side type
14 type BookSide: OrderBookSide;
15
16 /// Creates a new order book for the given symbol.
17 fn create_order_book(symbol: Symbol) -> OrderBook<Self::BookSide>;
18}
19
20/// An in-memory order book factory that creates order books with
21/// binary heap-based priority queues.
22///
23/// # Example
24///
25/// ```
26/// use clob_sync::prelude::*;
27/// use clob_sync::order_book::InMemoryOrderBookFactory;
28///
29/// let book = InMemoryOrderBookFactory::create_order_book(Symbol::from("BTC-USD"));
30/// ```
31///
32/// or:
33///
34/// ```
35/// use clob_sync::prelude::*;
36/// let book = InMemoryOrderBookFactory::create_order_book(Symbol::from("BTC-USD"));
37/// ```
38#[derive(Debug)]
39pub struct InMemoryOrderBookFactory;
40
41impl OrderBookFactory for InMemoryOrderBookFactory {
42 type BookSide = InMemoryOrderBookSide;
43
44 fn create_order_book(symbol: Symbol) -> OrderBook<Self::BookSide> {
45 OrderBook {
46 symbol,
47 bids: InMemoryOrderBookSide::new(BookSide::Bids),
48 asks: InMemoryOrderBookSide::new(BookSide::Asks),
49 }
50 }
51}
52
53/// A Central Limit Order Book (CLOB) for a single symbol.
54///
55/// The order book maintains buy orders (bids) and sell orders (asks),
56/// executing trades when orders cross.
57///
58/// # Type Parameters
59///
60/// * `S` - The order book side implementation
61///
62/// # Example
63///
64/// ```
65/// use clob_sync::prelude::*;
66/// use clob_sync::order::{Order, OrderType, OrderSide, Quantity, Price, Symbol};
67/// use clob_sync::order_book::InMemoryOrderBookFactory;
68/// use std::str::FromStr;
69///
70/// fn main() -> Result<()> {
71/// let mut book = InMemoryOrderBookFactory::create_order_book(Symbol::from("BTC-USD"));
72///
73/// // Add a sell order
74/// let sell_order = Order::new(
75/// OrderType::Limit(Price::try_from("50000.00").unwrap()),
76/// Quantity::try_from("1.0").unwrap(),
77/// OrderSide::Sell,
78/// Symbol::from("BTC-USD"),
79/// );
80/// book.execute(&sell_order)?;
81///
82/// // Add a matching buy order
83/// let buy_order = Order::new(
84/// OrderType::Limit(Price::try_from("50000.00").unwrap()),
85/// Quantity::try_from("0.5").unwrap(),
86/// OrderSide::Buy,
87/// Symbol::from("BTC-USD"),
88/// );
89/// let result = book.execute(&buy_order)?;
90/// assert!(matches!(result, Executions::Executed(_)));
91///
92/// Ok(())
93/// }
94/// ```
95#[derive(Debug)]
96pub struct OrderBook<S>
97where
98 S: OrderBookSide,
99{
100 symbol: Symbol,
101 bids: S,
102 asks: S,
103}
104
105impl<S> OrderBook<S>
106where
107 S: OrderBookSide,
108{
109 /// Executes an order against the order book.
110 ///
111 /// For market orders, execution happens at the best available price.
112 /// For limit orders, execution happens only if the order crosses the
113 /// opposite side of the book.
114 ///
115 /// # Arguments
116 ///
117 /// * `order` - The order to execute
118 ///
119 /// # Returns
120 ///
121 /// Returns [`Executions`] indicating:
122 /// - `AllocatedNoExecutions`: Order was added to book without matching
123 /// - `Executed`: Order was fully or partially matched
124 ///
125 /// # Errors
126 ///
127 /// Returns [`Error::InvalidSymbol`] if the order symbol doesn't match
128 /// the order book symbol.
129 pub fn execute(&mut self, order: &Order) -> Result<Executions> {
130 if order.symbol != self.symbol {
131 return Err(Error::InvalidSymbol {
132 expected: self.symbol,
133 received: order.symbol,
134 });
135 }
136 let order = &order.clone().with_id();
137
138 let crosses = match order.side {
139 OrderSide::Buy => self.asks.try_cross(order)?,
140 OrderSide::Sell => self.bids.try_cross(order)?,
141 };
142
143 if let Some(leaves_quantity) = crosses.remaining_incoming_order {
144 let remaining_order = order.with_quantity(leaves_quantity);
145 match order.side {
146 OrderSide::Buy => self.bids.allocate(remaining_order),
147 OrderSide::Sell => self.asks.allocate(remaining_order),
148 }
149 }
150
151 if crosses.matches.is_empty() {
152 Ok(Executions::AllocatedNoExecutions)
153 } else {
154 Ok(Executions::Executed(
155 NonEmpty::from_vec(
156 crosses
157 .matches
158 .iter()
159 .flat_map(Vec::<Execution>::from)
160 .collect(),
161 )
162 .expect("unexpected condition: matches cannot be empty here"),
163 ))
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::{
172 insta_test_utils::insta_settings_with_masking_filters,
173 order::{Price, Quantity},
174 };
175 use insta::assert_debug_snapshot;
176
177 #[test]
178 fn test_order_book_factory() {
179 assert_debug_snapshot!(InMemoryOrderBookFactory::create_order_book(Symbol::from("AAPL")), @r#"
180 OrderBook {
181 symbol: Symbol(
182 u!("AAPL"),
183 ),
184 bids: InMemoryOrderBookSide {
185 side: Bids,
186 orders: [],
187 },
188 asks: InMemoryOrderBookSide {
189 side: Asks,
190 orders: [],
191 },
192 }
193 "#);
194 }
195
196 #[test]
197 fn test_displaced_symbol() {
198 let mut book =
199 InMemoryOrderBookFactory::create_order_book(Symbol::from("IBM"));
200
201 let result = book.execute(&Order::new(
202 OrderType::Limit(Price::from(100)),
203 Quantity::from(10u64),
204 OrderSide::Buy,
205 Symbol::from("AAPL"),
206 ));
207 assert_debug_snapshot!(result, @r#"
208 Err(
209 InvalidSymbol {
210 expected: Symbol(
211 u!("IBM"),
212 ),
213 received: Symbol(
214 u!("AAPL"),
215 ),
216 },
217 )
218 "#);
219 }
220
221 #[test]
222 fn test_execute() {
223 let mut book =
224 InMemoryOrderBookFactory::create_order_book(Symbol::from("AAPL"));
225
226 let result = book.execute(&Order::new(
227 OrderType::Limit(Price::from(100)),
228 Quantity::from(10u64),
229 OrderSide::Buy,
230 Symbol::from("AAPL"),
231 ));
232 assert_debug_snapshot!(result, @r"
233 Ok(
234 AllocatedNoExecutions,
235 )
236 ");
237
238 insta_settings_with_masking_filters().bind(|| {
239 let result = book.execute(&Order::new(
240 OrderType::Limit(Price::from(100)),
241 Quantity::from(10u64),
242 OrderSide::Sell,
243 Symbol::from("AAPL"),
244 ));
245 assert_debug_snapshot!(result, @r"
246 Ok(
247 Executed(
248 NonEmpty {
249 head: FullExecution(
250 OrderId(********-****-****-****-************),
251 ),
252 tail: [
253 FullExecution(
254 OrderId(********-****-****-****-************),
255 ),
256 ],
257 },
258 ),
259 )
260 ");
261 });
262 }
263}