rust_order_book/
builder.rs

1//! Builder for configuring and constructing an [`OrderBook`].
2//!
3//! This module provides the [`OrderBookBuilder`] struct, which allows
4//! incremental configuration of an [`OrderBook`] before instantiating it.
5//!
6//! # Example
7//! ```rust
8//! use rust_order_book::OrderBookBuilder;
9//!
10//! let ob = OrderBookBuilder::new("BTCUSD")
11//!     .with_journaling(true)
12//!     .build();
13//! ```
14use crate::{
15    journal::{JournalLog, Snapshot},
16    OrderBook, OrderBookOptions,
17};
18
19/// A builder for constructing an [`OrderBook`] with custom options.
20///
21/// Use this struct to configure optional features such as journaling
22/// before creating an [`OrderBook`] instance.
23pub struct OrderBookBuilder {
24    symbol: String,
25    options: OrderBookOptions,
26}
27
28impl OrderBookBuilder {
29    /// Creates a new builder instance for the given symbol.
30    ///
31    /// # Parameters
32    /// - `symbol`: The market symbol (e.g., `"BTCUSD"`)
33    pub fn new(symbol: impl Into<String>) -> Self {
34        Self { symbol: symbol.into(), options: OrderBookOptions::default() }
35    }
36
37    /// Sets all options in bulk via an [`OrderBookOptions`] struct.
38    ///
39    /// This method can be used for advanced configuration.
40    pub fn with_options(mut self, options: OrderBookOptions) -> Self {
41        self.options = options;
42        self
43    }
44
45    /// Attaches a snapshot to this builder, so that the constructed [`OrderBook`]
46    /// will be restored to the state captured in the snapshot rather than starting
47    /// empty.
48    ///
49    /// # Parameters
50    /// - `snapshot`: A previously captured [`Snapshot`] representing the full state
51    ///   of an order book at a given point in time.
52    ///
53    /// # Returns
54    /// The builder itself, allowing method chaining.
55    pub fn with_snapshot(mut self, snapshot: Snapshot) -> Self {
56        self.options.snapshot = Some(snapshot);
57        self
58    }
59
60    /// Sets a sequence of journal logs to be replayed after snapshot restoration.
61    ///
62    /// This allows the order book to reconstruct its state by first restoring a snapshot
63    /// (if provided) and then applying all operations contained in the logs.
64    ///
65    /// # Parameters
66    /// - `logs`: A vector of [`JournalLog`] entries to replay. Logs should ideally be in
67    ///   chronological order (`op_id` ascending), but `replay_logs` will sort them internally.
68    ///
69    /// # Returns
70    /// Returns `self` to allow chaining with other builder methods.
71    pub fn with_replay_logs(mut self, logs: Vec<JournalLog>) -> Self {
72        self.options.replay_logs = Some(logs);
73        self
74    }
75
76    /// Enables or disables journaling.
77    ///
78    /// # Parameters
79    /// - `enabled`: `true` to enable journaling
80    pub fn with_journaling(mut self, enabled: bool) -> Self {
81        self.options.journaling = enabled;
82        self
83    }
84
85    /// Builds and returns a fully configured [`OrderBook`] instance.
86    ///
87    /// # Returns
88    /// An [`OrderBook`] with the configured options.
89    pub fn build(self) -> OrderBook {
90        let mut ob = OrderBook::new(self.symbol.as_str(), self.options.clone());
91        if let Some(snapshot) = &self.options.snapshot {
92            ob.restore_snapshot(snapshot.clone());
93        }
94
95        if let Some(logs) = self.options.replay_logs {
96            ob.replay_logs(logs).unwrap(); // panic if logs are invalid
97        }
98
99        ob
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use std::collections::{BTreeMap, HashMap};
106
107    use crate::{
108        enums::{JournalOp, OrderOptions},
109        order::{OrderId, Price, Quantity},
110        utils::current_timestamp_millis,
111        LimitOrderOptions, MarketOrderOptions, Side,
112    };
113
114    use super::*;
115
116    #[test]
117    fn test_builder_with_defaults() {
118        let ob = OrderBookBuilder::new("BTCUSD").build();
119        assert_eq!(ob.symbol(), "BTCUSD");
120        assert!(!ob.journaling);
121    }
122
123    #[test]
124    fn test_builder_with_journaling_enabled() {
125        let ob = OrderBookBuilder::new("ETHUSD").with_journaling(true).build();
126
127        assert_eq!(ob.symbol(), "ETHUSD");
128        assert!(ob.journaling);
129    }
130
131    #[test]
132    fn test_builder_with_options_struct() {
133        let opts = OrderBookOptions { journaling: true, ..Default::default() };
134
135        let ob = OrderBookBuilder::new("DOGEUSD").with_options(opts.clone()).build();
136
137        assert_eq!(ob.symbol(), "DOGEUSD");
138        assert_eq!(ob.journaling, opts.journaling);
139    }
140
141    #[test]
142    fn test_builder_with_snapshot() {
143        // crea snapshot finto
144        let snap = Snapshot {
145            orders: HashMap::new(),
146            bids: BTreeMap::new(),
147            asks: BTreeMap::new(),
148            last_op: 42,
149            next_order_id: OrderId(100),
150            ts: current_timestamp_millis(),
151        };
152
153        let book = OrderBookBuilder::new("BTCUSD").with_snapshot(snap).build();
154
155        assert_eq!(book.last_op, 42);
156        assert_eq!(book.next_order_id, OrderId(100));
157        assert_eq!(book.orders.len(), 0);
158    }
159
160    #[test]
161    fn test_builder_with_replay_logs() {
162        // Create a vector of fake journal logs to replay
163        let logs = vec![
164            JournalLog {
165                op_id: 1,
166                ts: 123457,
167                op: JournalOp::Limit,
168                o: OrderOptions::Limit(LimitOrderOptions {
169                    quantity: Quantity(10),
170                    price: Price(1100),
171                    side: Side::Sell,
172                    post_only: None,
173                    time_in_force: None,
174                }),
175            },
176            JournalLog {
177                op_id: 2,
178                ts: 123457,
179                op: JournalOp::Limit,
180                o: OrderOptions::Limit(LimitOrderOptions {
181                    quantity: Quantity(10),
182                    price: Price(1000),
183                    side: Side::Buy,
184                    post_only: None,
185                    time_in_force: None,
186                }),
187            },
188            JournalLog {
189                op_id: 3,
190                ts: 123456,
191                op: JournalOp::Market,
192                o: OrderOptions::Market(MarketOrderOptions {
193                    quantity: Quantity(5),
194                    side: Side::Buy,
195                }),
196            },
197        ];
198
199        // Build the order book using the builder with replay logs
200        let ob = OrderBookBuilder::new("BTCUSD").with_replay_logs(logs.clone()).build();
201
202        // Check that the total number of orders matches the logs applied
203        assert_eq!(ob.orders.len(), 2);
204
205        // Verify that the orders match the original logs
206        assert_eq!(ob.get_order(OrderId(0)).unwrap().remaining_qty(), Quantity(5));
207        assert_eq!(ob.get_order(OrderId(1)).unwrap().remaining_qty(), Quantity(10));
208    }
209}