Skip to main content

orderbook_rs/orderbook/
error.rs

1//! Order book error types
2
3use pricelevel::{PriceLevelError, Side};
4use std::fmt;
5
6/// Errors that can occur within the OrderBook
7#[derive(Debug)]
8#[non_exhaustive]
9pub enum OrderBookError {
10    /// Error from underlying price level operations
11    PriceLevelError(PriceLevelError),
12
13    /// Order not found in the book
14    OrderNotFound(String),
15
16    /// Invalid price level
17    InvalidPriceLevel(u128),
18
19    /// Price crossing (bid >= ask)
20    PriceCrossing {
21        /// Price that would cause crossing
22        price: u128,
23        /// Side of the order
24        side: Side,
25        /// Best opposite price
26        opposite_price: u128,
27    },
28
29    /// Insufficient liquidity for market order
30    InsufficientLiquidity {
31        /// The side of the market order
32        side: Side,
33        /// Quantity requested
34        requested: u64,
35        /// Quantity available
36        available: u64,
37    },
38
39    /// Operation not permitted for specified order type
40    InvalidOperation {
41        /// Description of the error
42        message: String,
43    },
44
45    /// Error while serializing snapshot data
46    SerializationError {
47        /// Underlying error message
48        message: String,
49    },
50
51    /// Error while deserializing snapshot data
52    DeserializationError {
53        /// Underlying error message
54        message: String,
55    },
56
57    /// Snapshot integrity check failed
58    ChecksumMismatch {
59        /// Expected checksum value
60        expected: String,
61        /// Actual checksum value
62        actual: String,
63    },
64
65    /// Order price is not a multiple of the configured tick size
66    InvalidTickSize {
67        /// The order price that failed validation
68        price: u128,
69        /// The configured tick size
70        tick_size: u128,
71    },
72
73    /// Order quantity is not a multiple of the configured lot size
74    InvalidLotSize {
75        /// The order quantity that failed validation
76        quantity: u64,
77        /// The configured lot size
78        lot_size: u64,
79    },
80
81    /// Order quantity is outside the allowed min/max range
82    OrderSizeOutOfRange {
83        /// The order quantity that failed validation
84        quantity: u64,
85        /// The configured minimum order size, if any
86        min: Option<u64>,
87        /// The configured maximum order size, if any
88        max: Option<u64>,
89    },
90
91    /// Order rejected because `user_id` is `Hash32::zero()` while
92    /// Self-Trade Prevention is enabled. All orders must carry a non-zero
93    /// `user_id` when STP mode is active.
94    MissingUserId {
95        /// The order ID that was rejected
96        order_id: pricelevel::Id,
97    },
98
99    /// Self-trade prevention triggered: the incoming order would have
100    /// matched against a resting order from the same user.
101    SelfTradePrevented {
102        /// The STP mode that was active
103        mode: crate::orderbook::stp::STPMode,
104        /// The taker (incoming) order ID
105        taker_order_id: pricelevel::Id,
106        /// The user ID that triggered the STP check
107        user_id: pricelevel::Hash32,
108    },
109
110    /// Failed to publish a trade event to NATS JetStream.
111    #[cfg(feature = "nats")]
112    NatsPublishError {
113        /// Description of the publish failure
114        message: String,
115    },
116
117    /// Failed to serialize a trade event for NATS publishing.
118    #[cfg(feature = "nats")]
119    NatsSerializationError {
120        /// Description of the serialization failure
121        message: String,
122    },
123}
124
125impl fmt::Display for OrderBookError {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        match self {
128            OrderBookError::PriceLevelError(err) => write!(f, "Price level error: {err}"),
129            OrderBookError::OrderNotFound(id) => write!(f, "Order not found: {id}"),
130            OrderBookError::InvalidPriceLevel(price) => write!(f, "Invalid price level: {price}"),
131            OrderBookError::PriceCrossing {
132                price,
133                side,
134                opposite_price,
135            } => {
136                write!(
137                    f,
138                    "Price crossing: {side} {price} would cross opposite at {opposite_price}"
139                )
140            }
141            OrderBookError::InsufficientLiquidity {
142                side,
143                requested,
144                available,
145            } => {
146                write!(
147                    f,
148                    "Insufficient liquidity for {side} order: requested {requested}, available {available}"
149                )
150            }
151            OrderBookError::InvalidOperation { message } => {
152                write!(f, "Invalid operation: {message}")
153            }
154            OrderBookError::SerializationError { message } => {
155                write!(f, "Serialization error: {message}")
156            }
157            OrderBookError::DeserializationError { message } => {
158                write!(f, "Deserialization error: {message}")
159            }
160            OrderBookError::ChecksumMismatch { expected, actual } => {
161                write!(
162                    f,
163                    "Checksum mismatch: expected {expected}, but computed {actual}"
164                )
165            }
166            OrderBookError::InvalidTickSize { price, tick_size } => {
167                write!(
168                    f,
169                    "invalid tick size: price {price} is not a multiple of tick size {tick_size}"
170                )
171            }
172            OrderBookError::InvalidLotSize { quantity, lot_size } => {
173                write!(
174                    f,
175                    "invalid lot size: quantity {quantity} is not a multiple of lot size {lot_size}"
176                )
177            }
178            OrderBookError::OrderSizeOutOfRange { quantity, min, max } => {
179                write!(
180                    f,
181                    "order size out of range: quantity {quantity}, min {min:?}, max {max:?}"
182                )
183            }
184            OrderBookError::MissingUserId { order_id } => {
185                write!(
186                    f,
187                    "missing user_id: order {order_id} rejected because STP is enabled and user_id is zero"
188                )
189            }
190            OrderBookError::SelfTradePrevented {
191                mode,
192                taker_order_id,
193                user_id,
194            } => {
195                write!(
196                    f,
197                    "self-trade prevented ({mode}): taker {taker_order_id}, user {user_id}"
198                )
199            }
200            #[cfg(feature = "nats")]
201            OrderBookError::NatsPublishError { message } => {
202                write!(f, "nats publish error: {message}")
203            }
204            #[cfg(feature = "nats")]
205            OrderBookError::NatsSerializationError { message } => {
206                write!(f, "nats serialization error: {message}")
207            }
208        }
209    }
210}
211
212impl std::error::Error for OrderBookError {}
213
214impl From<PriceLevelError> for OrderBookError {
215    fn from(err: PriceLevelError) -> Self {
216        OrderBookError::PriceLevelError(err)
217    }
218}
219
220impl Clone for OrderBookError {
221    fn clone(&self) -> Self {
222        match self {
223            OrderBookError::PriceLevelError(err) => {
224                // PriceLevelError doesn't implement Clone, so we manually clone each variant
225                let cloned_err = match err {
226                    PriceLevelError::ParseError { message } => PriceLevelError::ParseError {
227                        message: message.clone(),
228                    },
229                    PriceLevelError::InvalidFormat => PriceLevelError::InvalidFormat,
230                    PriceLevelError::UnknownOrderType(s) => {
231                        PriceLevelError::UnknownOrderType(s.clone())
232                    }
233                    PriceLevelError::MissingField(s) => PriceLevelError::MissingField(s.clone()),
234                    PriceLevelError::InvalidFieldValue { field, value } => {
235                        PriceLevelError::InvalidFieldValue {
236                            field: field.clone(),
237                            value: value.clone(),
238                        }
239                    }
240                    PriceLevelError::InvalidOperation { message } => {
241                        PriceLevelError::InvalidOperation {
242                            message: message.clone(),
243                        }
244                    }
245                    PriceLevelError::SerializationError { message } => {
246                        PriceLevelError::SerializationError {
247                            message: message.clone(),
248                        }
249                    }
250                    PriceLevelError::DeserializationError { message } => {
251                        PriceLevelError::DeserializationError {
252                            message: message.clone(),
253                        }
254                    }
255                    PriceLevelError::ChecksumMismatch { expected, actual } => {
256                        PriceLevelError::ChecksumMismatch {
257                            expected: expected.clone(),
258                            actual: actual.clone(),
259                        }
260                    }
261                };
262                OrderBookError::PriceLevelError(cloned_err)
263            }
264            OrderBookError::OrderNotFound(s) => OrderBookError::OrderNotFound(s.clone()),
265            OrderBookError::InvalidPriceLevel(p) => OrderBookError::InvalidPriceLevel(*p),
266            OrderBookError::PriceCrossing {
267                price,
268                side,
269                opposite_price,
270            } => OrderBookError::PriceCrossing {
271                price: *price,
272                side: *side,
273                opposite_price: *opposite_price,
274            },
275            OrderBookError::InsufficientLiquidity {
276                side,
277                requested,
278                available,
279            } => OrderBookError::InsufficientLiquidity {
280                side: *side,
281                requested: *requested,
282                available: *available,
283            },
284            OrderBookError::InvalidOperation { message } => OrderBookError::InvalidOperation {
285                message: message.clone(),
286            },
287            OrderBookError::SerializationError { message } => OrderBookError::SerializationError {
288                message: message.clone(),
289            },
290            OrderBookError::DeserializationError { message } => {
291                OrderBookError::DeserializationError {
292                    message: message.clone(),
293                }
294            }
295            OrderBookError::ChecksumMismatch { expected, actual } => {
296                OrderBookError::ChecksumMismatch {
297                    expected: expected.clone(),
298                    actual: actual.clone(),
299                }
300            }
301            OrderBookError::InvalidTickSize { price, tick_size } => {
302                OrderBookError::InvalidTickSize {
303                    price: *price,
304                    tick_size: *tick_size,
305                }
306            }
307            OrderBookError::InvalidLotSize { quantity, lot_size } => {
308                OrderBookError::InvalidLotSize {
309                    quantity: *quantity,
310                    lot_size: *lot_size,
311                }
312            }
313            OrderBookError::OrderSizeOutOfRange { quantity, min, max } => {
314                OrderBookError::OrderSizeOutOfRange {
315                    quantity: *quantity,
316                    min: *min,
317                    max: *max,
318                }
319            }
320            OrderBookError::MissingUserId { order_id } => OrderBookError::MissingUserId {
321                order_id: *order_id,
322            },
323            OrderBookError::SelfTradePrevented {
324                mode,
325                taker_order_id,
326                user_id,
327            } => OrderBookError::SelfTradePrevented {
328                mode: *mode,
329                taker_order_id: *taker_order_id,
330                user_id: *user_id,
331            },
332            #[cfg(feature = "nats")]
333            OrderBookError::NatsPublishError { message } => OrderBookError::NatsPublishError {
334                message: message.clone(),
335            },
336            #[cfg(feature = "nats")]
337            OrderBookError::NatsSerializationError { message } => {
338                OrderBookError::NatsSerializationError {
339                    message: message.clone(),
340                }
341            }
342        }
343    }
344}
345
346/// Errors that can occur in BookManager operations
347#[derive(Debug, Clone)]
348#[non_exhaustive]
349pub enum ManagerError {
350    /// Trade processor has already been started
351    ProcessorAlreadyStarted,
352}
353
354impl fmt::Display for ManagerError {
355    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
356        match self {
357            ManagerError::ProcessorAlreadyStarted => {
358                write!(f, "trade processor already started")
359            }
360        }
361    }
362}
363
364impl std::error::Error for ManagerError {}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369    use pricelevel::{Hash32, Id};
370
371    #[test]
372    fn test_clone_order_not_found() {
373        let error = OrderBookError::OrderNotFound("order123".to_string());
374        let cloned = error.clone();
375        assert!(matches!(cloned, OrderBookError::OrderNotFound(ref s) if s == "order123"));
376    }
377
378    #[test]
379    fn test_clone_invalid_price_level() {
380        let error = OrderBookError::InvalidPriceLevel(12345);
381        let cloned = error.clone();
382        assert!(matches!(cloned, OrderBookError::InvalidPriceLevel(12345)));
383    }
384
385    #[test]
386    fn test_clone_price_crossing() {
387        let error = OrderBookError::PriceCrossing {
388            price: 100,
389            side: Side::Buy,
390            opposite_price: 99,
391        };
392        let cloned = error.clone();
393        assert!(matches!(
394            cloned,
395            OrderBookError::PriceCrossing {
396                price: 100,
397                side: Side::Buy,
398                opposite_price: 99
399            }
400        ));
401    }
402
403    #[test]
404    fn test_clone_insufficient_liquidity() {
405        let error = OrderBookError::InsufficientLiquidity {
406            side: Side::Sell,
407            requested: 1000,
408            available: 500,
409        };
410        let cloned = error.clone();
411        assert!(matches!(
412            cloned,
413            OrderBookError::InsufficientLiquidity {
414                side: Side::Sell,
415                requested: 1000,
416                available: 500
417            }
418        ));
419    }
420
421    #[test]
422    fn test_clone_invalid_operation() {
423        let error = OrderBookError::InvalidOperation {
424            message: "Cannot cancel filled order".to_string(),
425        };
426        let cloned = error.clone();
427        assert!(matches!(
428            cloned,
429            OrderBookError::InvalidOperation { ref message } if message == "Cannot cancel filled order"
430        ));
431    }
432
433    #[test]
434    fn test_clone_serialization_error() {
435        let error = OrderBookError::SerializationError {
436            message: "Failed to serialize".to_string(),
437        };
438        let cloned = error.clone();
439        assert!(matches!(
440            cloned,
441            OrderBookError::SerializationError { ref message } if message == "Failed to serialize"
442        ));
443    }
444
445    #[test]
446    fn test_clone_checksum_mismatch() {
447        let error = OrderBookError::ChecksumMismatch {
448            expected: "abc123".to_string(),
449            actual: "def456".to_string(),
450        };
451        let cloned = error.clone();
452        assert!(matches!(
453            cloned,
454            OrderBookError::ChecksumMismatch { ref expected, ref actual }
455            if expected == "abc123" && actual == "def456"
456        ));
457    }
458
459    #[test]
460    fn test_clone_invalid_tick_size() {
461        let error = OrderBookError::InvalidTickSize {
462            price: 10050,
463            tick_size: 100,
464        };
465        let cloned = error.clone();
466        assert!(matches!(
467            cloned,
468            OrderBookError::InvalidTickSize {
469                price: 10050,
470                tick_size: 100
471            }
472        ));
473    }
474
475    #[test]
476    fn test_clone_invalid_lot_size() {
477        let error = OrderBookError::InvalidLotSize {
478            quantity: 75,
479            lot_size: 10,
480        };
481        let cloned = error.clone();
482        assert!(matches!(
483            cloned,
484            OrderBookError::InvalidLotSize {
485                quantity: 75,
486                lot_size: 10
487            }
488        ));
489    }
490
491    #[test]
492    fn test_clone_order_size_out_of_range() {
493        let error = OrderBookError::OrderSizeOutOfRange {
494            quantity: 5,
495            min: Some(10),
496            max: Some(1000),
497        };
498        let cloned = error.clone();
499        assert!(matches!(
500            cloned,
501            OrderBookError::OrderSizeOutOfRange {
502                quantity: 5,
503                min: Some(10),
504                max: Some(1000)
505            }
506        ));
507    }
508
509    #[test]
510    fn test_clone_missing_user_id() {
511        let order_id = Id::new_uuid();
512        let error = OrderBookError::MissingUserId { order_id };
513        let cloned = error.clone();
514        assert!(matches!(
515            cloned,
516            OrderBookError::MissingUserId { order_id: id } if id == order_id
517        ));
518    }
519
520    #[test]
521    fn test_clone_self_trade_prevented() {
522        let taker_id = Id::new_uuid();
523        let user_id = Hash32::from([1u8; 32]);
524        let error = OrderBookError::SelfTradePrevented {
525            mode: crate::orderbook::stp::STPMode::CancelMaker,
526            taker_order_id: taker_id,
527            user_id,
528        };
529        let cloned = error.clone();
530        assert!(matches!(
531            cloned,
532            OrderBookError::SelfTradePrevented {
533                mode: crate::orderbook::stp::STPMode::CancelMaker,
534                taker_order_id: id,
535                user_id: uid
536            } if id == taker_id && uid == user_id
537        ));
538    }
539
540    #[test]
541    fn test_clone_price_level_error_parse_error() {
542        let price_level_err = PriceLevelError::ParseError {
543            message: "Parse failed".to_string(),
544        };
545        let error = OrderBookError::PriceLevelError(price_level_err);
546        let cloned = error.clone();
547        assert!(matches!(
548            cloned,
549            OrderBookError::PriceLevelError(PriceLevelError::ParseError { ref message })
550            if message == "Parse failed"
551        ));
552    }
553
554    #[test]
555    fn test_clone_price_level_error_invalid_format() {
556        let price_level_err = PriceLevelError::InvalidFormat;
557        let error = OrderBookError::PriceLevelError(price_level_err);
558        let cloned = error.clone();
559        assert!(matches!(
560            cloned,
561            OrderBookError::PriceLevelError(PriceLevelError::InvalidFormat)
562        ));
563    }
564
565    #[test]
566    fn test_clone_price_level_error_unknown_order_type() {
567        let price_level_err = PriceLevelError::UnknownOrderType("CUSTOM".to_string());
568        let error = OrderBookError::PriceLevelError(price_level_err);
569        let cloned = error.clone();
570        assert!(matches!(
571            cloned,
572            OrderBookError::PriceLevelError(PriceLevelError::UnknownOrderType(ref s))
573            if s == "CUSTOM"
574        ));
575    }
576
577    #[test]
578    fn test_clone_price_level_error_checksum_mismatch() {
579        let price_level_err = PriceLevelError::ChecksumMismatch {
580            expected: "hash1".to_string(),
581            actual: "hash2".to_string(),
582        };
583        let error = OrderBookError::PriceLevelError(price_level_err);
584        let cloned = error.clone();
585        assert!(matches!(
586            cloned,
587            OrderBookError::PriceLevelError(PriceLevelError::ChecksumMismatch {
588                ref expected,
589                ref actual
590            }) if expected == "hash1" && actual == "hash2"
591        ));
592    }
593}