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        utils::current_timestamp_millis,
110        LimitOrderOptions, MarketOrderOptions, Side,
111    };
112
113    use super::*;
114
115    #[test]
116    fn test_builder_with_defaults() {
117        let ob = OrderBookBuilder::new("BTCUSD").build();
118        assert_eq!(ob.symbol(), "BTCUSD");
119        assert!(!ob.journaling);
120    }
121
122    #[test]
123    fn test_builder_with_journaling_enabled() {
124        let ob = OrderBookBuilder::new("ETHUSD").with_journaling(true).build();
125
126        assert_eq!(ob.symbol(), "ETHUSD");
127        assert!(ob.journaling);
128    }
129
130    #[test]
131    fn test_builder_with_options_struct() {
132        let opts = OrderBookOptions { journaling: true, ..Default::default() };
133
134        let ob = OrderBookBuilder::new("DOGEUSD").with_options(opts.clone()).build();
135
136        assert_eq!(ob.symbol(), "DOGEUSD");
137        assert_eq!(ob.journaling, opts.journaling);
138    }
139
140    #[test]
141    fn test_builder_with_snapshot() {
142        // crea snapshot finto
143        let snap = Snapshot {
144            orders: HashMap::new(),
145            bids: BTreeMap::new(),
146            asks: BTreeMap::new(),
147            last_op: 42,
148            next_order_id: 100,
149            ts: current_timestamp_millis(),
150        };
151
152        let book = OrderBookBuilder::new("BTCUSD").with_snapshot(snap).build();
153
154        assert_eq!(book.last_op, 42);
155        assert_eq!(book.next_order_id, 100);
156        assert_eq!(book.orders.len(), 0);
157    }
158
159    #[test]
160    fn test_builder_with_replay_logs() {
161        // Create a vector of fake journal logs to replay
162        let logs = vec![
163            JournalLog {
164                op_id: 1,
165                ts: 123457,
166                op: JournalOp::Limit,
167                o: OrderOptions::Limit(LimitOrderOptions {
168                    quantity: 10,
169                    price: 1100,
170                    side: Side::Sell,
171                    post_only: None,
172                    time_in_force: None,
173                }),
174            },
175            JournalLog {
176                op_id: 2,
177                ts: 123457,
178                op: JournalOp::Limit,
179                o: OrderOptions::Limit(LimitOrderOptions {
180                    quantity: 10,
181                    price: 1000,
182                    side: Side::Buy,
183                    post_only: None,
184                    time_in_force: None,
185                }),
186            },
187            JournalLog {
188                op_id: 3,
189                ts: 123456,
190                op: JournalOp::Market,
191                o: OrderOptions::Market(MarketOrderOptions { quantity: 5, side: Side::Buy }),
192            },
193        ];
194
195        // Build the order book using the builder with replay logs
196        let ob = OrderBookBuilder::new("BTCUSD").with_replay_logs(logs.clone()).build();
197
198        // Check that the total number of orders matches the logs applied
199        assert_eq!(ob.orders.len(), 2);
200
201        // Verify that the orders match the original logs
202        assert_eq!(ob.get_order(0).unwrap().remaining_qty, 5);
203        assert_eq!(ob.get_order(1).unwrap().remaining_qty, 10);
204    }
205}