Skip to main content

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}