Skip to main content

orderbook_rs/orderbook/
error.rs

1//! Order book error types
2
3use pricelevel::{Hash32, 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    /// Insufficient liquidity for a quote-notional market order. Returned by
40    /// the `*_by_amount` paths when the book cannot fund a single whole lot
41    /// against the requested notional. Distinct from
42    /// [`OrderBookError::InsufficientLiquidity`] so callers can pattern-match
43    /// on quote-vs-base semantics.
44    InsufficientLiquidityNotional {
45        /// The side of the market order
46        side: Side,
47        /// Notional (quote-asset value) requested
48        requested: u128,
49        /// Notional actually consumed before the walk gave up. Always `0`
50        /// when this error is constructed (a non-zero `spent` returns
51        /// `Ok(MatchResult)` with the partial fill instead).
52        spent: u128,
53    },
54
55    /// Operation not permitted for specified order type
56    InvalidOperation {
57        /// Description of the error
58        message: String,
59    },
60
61    /// New flow (submit / modify / replace) is rejected because the
62    /// kill switch is engaged. Cancel and mass-cancel paths still
63    /// operate so operators can drain the book in an orderly way.
64    KillSwitchActive,
65
66    /// Error while serializing snapshot data
67    SerializationError {
68        /// Underlying error message
69        message: String,
70    },
71
72    /// Error while deserializing snapshot data
73    DeserializationError {
74        /// Underlying error message
75        message: String,
76    },
77
78    /// Snapshot integrity check failed
79    ChecksumMismatch {
80        /// Expected checksum value
81        expected: String,
82        /// Actual checksum value
83        actual: String,
84    },
85
86    /// Order price is not a multiple of the configured tick size
87    InvalidTickSize {
88        /// The order price that failed validation
89        price: u128,
90        /// The configured tick size
91        tick_size: u128,
92    },
93
94    /// Order quantity is not a multiple of the configured lot size
95    InvalidLotSize {
96        /// The order quantity that failed validation
97        quantity: u64,
98        /// The configured lot size
99        lot_size: u64,
100    },
101
102    /// Order quantity is outside the allowed min/max range
103    OrderSizeOutOfRange {
104        /// The order quantity that failed validation
105        quantity: u64,
106        /// The configured minimum order size, if any
107        min: Option<u64>,
108        /// The configured maximum order size, if any
109        max: Option<u64>,
110    },
111
112    /// Order rejected because its `order_id` duplicates an order that is
113    /// already resting on the book. Admitting it would overwrite the
114    /// existing order's location and orphan it (the prior order could no
115    /// longer be cancelled or modified by id), so the engine rejects the
116    /// duplicate instead of silently replacing the live order. Maps to the
117    /// stable wire code `RejectReason::DuplicateOrderId`.
118    ///
119    /// This guards against sequential reuse of a *live* order's id. It is
120    /// not atomic against two concurrent submissions of the same fresh id
121    /// on the lock-free admission path — serializing order ids is the
122    /// ingress / sequencing layer's responsibility.
123    DuplicateOrderId {
124        /// The duplicate order ID that was rejected
125        order_id: pricelevel::Id,
126    },
127
128    /// Order rejected because `user_id` is `Hash32::zero()` while
129    /// Self-Trade Prevention is enabled. All orders must carry a non-zero
130    /// `user_id` when STP mode is active.
131    MissingUserId {
132        /// The order ID that was rejected
133        order_id: pricelevel::Id,
134    },
135
136    /// Self-trade prevention triggered: the incoming order would have
137    /// matched against a resting order from the same user.
138    SelfTradePrevented {
139        /// The STP mode that was active
140        mode: crate::orderbook::stp::STPMode,
141        /// The taker (incoming) order ID
142        taker_order_id: pricelevel::Id,
143        /// The user ID that triggered the STP check
144        user_id: pricelevel::Hash32,
145    },
146
147    /// Per-account open-order limit breached.
148    ///
149    /// Returned by limit-order admission when the requesting account
150    /// already has `current` resting orders and the configured ceiling
151    /// is `limit`. `current >= limit` always holds when this variant
152    /// is constructed.
153    RiskMaxOpenOrders {
154        /// Account that breached the limit.
155        account: Hash32,
156        /// Account's current resting-order count at check time.
157        current: u64,
158        /// Configured maximum.
159        limit: u64,
160    },
161
162    /// Per-account notional limit would be breached by this admission.
163    ///
164    /// `current + attempted > limit` always holds when this variant
165    /// is constructed. `attempted` is computed as
166    /// `submitted_quantity * submitted_price`.
167    RiskMaxNotional {
168        /// Account that breached the limit.
169        account: Hash32,
170        /// Account's current resting notional at check time (raw ticks).
171        current: u128,
172        /// Notional this submission would add (raw ticks).
173        attempted: u128,
174        /// Configured maximum (raw ticks).
175        limit: u128,
176    },
177
178    /// Submitted price exceeds the configured price band against the
179    /// reference price.
180    ///
181    /// `deviation_bps > limit_bps` always holds when this variant is
182    /// constructed.
183    RiskPriceBand {
184        /// Limit price submitted by the caller (raw ticks).
185        submitted: u128,
186        /// Resolved reference price at check time (raw ticks).
187        reference: u128,
188        /// Computed deviation in basis points. Saturates at `u32::MAX`.
189        deviation_bps: u32,
190        /// Configured maximum allowed deviation in basis points.
191        limit_bps: u32,
192    },
193
194    /// Failed to publish a trade event to NATS JetStream.
195    #[cfg(feature = "nats")]
196    NatsPublishError {
197        /// Description of the publish failure
198        message: String,
199    },
200
201    /// Failed to serialize a trade event for NATS publishing.
202    #[cfg(feature = "nats")]
203    NatsSerializationError {
204        /// Description of the serialization failure
205        message: String,
206    },
207}
208
209impl fmt::Display for OrderBookError {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        match self {
212            OrderBookError::PriceLevelError(err) => write!(f, "Price level error: {err}"),
213            OrderBookError::OrderNotFound(id) => write!(f, "Order not found: {id}"),
214            OrderBookError::InvalidPriceLevel(price) => write!(f, "Invalid price level: {price}"),
215            OrderBookError::PriceCrossing {
216                price,
217                side,
218                opposite_price,
219            } => {
220                write!(
221                    f,
222                    "Price crossing: {side} {price} would cross opposite at {opposite_price}"
223                )
224            }
225            OrderBookError::InsufficientLiquidity {
226                side,
227                requested,
228                available,
229            } => {
230                write!(
231                    f,
232                    "Insufficient liquidity for {side} order: requested {requested}, available {available}"
233                )
234            }
235            OrderBookError::InsufficientLiquidityNotional {
236                side,
237                requested,
238                spent,
239            } => {
240                write!(
241                    f,
242                    "Insufficient liquidity by notional for {side} order: requested {requested}, spent {spent}"
243                )
244            }
245            OrderBookError::InvalidOperation { message } => {
246                write!(f, "Invalid operation: {message}")
247            }
248            OrderBookError::KillSwitchActive => {
249                write!(
250                    f,
251                    "kill switch active: new order entry and modifications are halted"
252                )
253            }
254            OrderBookError::SerializationError { message } => {
255                write!(f, "Serialization error: {message}")
256            }
257            OrderBookError::DeserializationError { message } => {
258                write!(f, "Deserialization error: {message}")
259            }
260            OrderBookError::ChecksumMismatch { expected, actual } => {
261                write!(
262                    f,
263                    "Checksum mismatch: expected {expected}, but computed {actual}"
264                )
265            }
266            OrderBookError::InvalidTickSize { price, tick_size } => {
267                write!(
268                    f,
269                    "invalid tick size: price {price} is not a multiple of tick size {tick_size}"
270                )
271            }
272            OrderBookError::InvalidLotSize { quantity, lot_size } => {
273                write!(
274                    f,
275                    "invalid lot size: quantity {quantity} is not a multiple of lot size {lot_size}"
276                )
277            }
278            OrderBookError::OrderSizeOutOfRange { quantity, min, max } => {
279                write!(
280                    f,
281                    "order size out of range: quantity {quantity}, min {min:?}, max {max:?}"
282                )
283            }
284            OrderBookError::DuplicateOrderId { order_id } => {
285                write!(
286                    f,
287                    "duplicate order id: order {order_id} is already resting on the book"
288                )
289            }
290            OrderBookError::MissingUserId { order_id } => {
291                write!(
292                    f,
293                    "missing user_id: order {order_id} rejected because STP is enabled and user_id is zero"
294                )
295            }
296            OrderBookError::SelfTradePrevented {
297                mode,
298                taker_order_id,
299                user_id,
300            } => {
301                write!(
302                    f,
303                    "self-trade prevented ({mode}): taker {taker_order_id}, user {user_id}"
304                )
305            }
306            OrderBookError::RiskMaxOpenOrders {
307                account,
308                current,
309                limit,
310            } => {
311                write!(
312                    f,
313                    "risk: account {account} has {current} open orders (limit {limit})"
314                )
315            }
316            OrderBookError::RiskMaxNotional {
317                account,
318                current,
319                attempted,
320                limit,
321            } => {
322                write!(
323                    f,
324                    "risk: account {account} notional {current} + attempted {attempted} would exceed limit {limit}"
325                )
326            }
327            OrderBookError::RiskPriceBand {
328                submitted,
329                reference,
330                deviation_bps,
331                limit_bps,
332            } => {
333                write!(
334                    f,
335                    "risk: submitted price {submitted} deviates {deviation_bps} bps from reference {reference} (limit {limit_bps} bps)"
336                )
337            }
338            #[cfg(feature = "nats")]
339            OrderBookError::NatsPublishError { message } => {
340                write!(f, "nats publish error: {message}")
341            }
342            #[cfg(feature = "nats")]
343            OrderBookError::NatsSerializationError { message } => {
344                write!(f, "nats serialization error: {message}")
345            }
346        }
347    }
348}
349
350impl std::error::Error for OrderBookError {}
351
352impl From<PriceLevelError> for OrderBookError {
353    fn from(err: PriceLevelError) -> Self {
354        OrderBookError::PriceLevelError(err)
355    }
356}
357
358impl From<crate::orderbook::serialization::SerializationError> for OrderBookError {
359    /// Folds a typed [`SerializationError`](crate::orderbook::serialization::SerializationError)
360    /// into [`OrderBookError::SerializationError`], preserving the underlying
361    /// serde / bincode message via the error's `Display`. Enables
362    /// `?`-propagation of an `EventSerializer` failure on paths returning
363    /// `OrderBookError`.
364    fn from(err: crate::orderbook::serialization::SerializationError) -> Self {
365        OrderBookError::SerializationError {
366            message: err.to_string(),
367        }
368    }
369}
370
371impl Clone for OrderBookError {
372    fn clone(&self) -> Self {
373        match self {
374            OrderBookError::PriceLevelError(err) => {
375                // PriceLevelError doesn't implement Clone, so we manually clone each variant
376                let cloned_err = match err {
377                    PriceLevelError::ParseError { message } => PriceLevelError::ParseError {
378                        message: message.clone(),
379                    },
380                    PriceLevelError::InvalidFormat => PriceLevelError::InvalidFormat,
381                    PriceLevelError::UnknownOrderType(s) => {
382                        PriceLevelError::UnknownOrderType(s.clone())
383                    }
384                    PriceLevelError::MissingField(s) => PriceLevelError::MissingField(s.clone()),
385                    PriceLevelError::InvalidFieldValue { field, value } => {
386                        PriceLevelError::InvalidFieldValue {
387                            field: field.clone(),
388                            value: value.clone(),
389                        }
390                    }
391                    PriceLevelError::InvalidOperation { message } => {
392                        PriceLevelError::InvalidOperation {
393                            message: message.clone(),
394                        }
395                    }
396                    PriceLevelError::SerializationError { message } => {
397                        PriceLevelError::SerializationError {
398                            message: message.clone(),
399                        }
400                    }
401                    PriceLevelError::DeserializationError { message } => {
402                        PriceLevelError::DeserializationError {
403                            message: message.clone(),
404                        }
405                    }
406                    PriceLevelError::ChecksumMismatch { expected, actual } => {
407                        PriceLevelError::ChecksumMismatch {
408                            expected: expected.clone(),
409                            actual: actual.clone(),
410                        }
411                    }
412                };
413                OrderBookError::PriceLevelError(cloned_err)
414            }
415            OrderBookError::OrderNotFound(s) => OrderBookError::OrderNotFound(s.clone()),
416            OrderBookError::InvalidPriceLevel(p) => OrderBookError::InvalidPriceLevel(*p),
417            OrderBookError::PriceCrossing {
418                price,
419                side,
420                opposite_price,
421            } => OrderBookError::PriceCrossing {
422                price: *price,
423                side: *side,
424                opposite_price: *opposite_price,
425            },
426            OrderBookError::InsufficientLiquidity {
427                side,
428                requested,
429                available,
430            } => OrderBookError::InsufficientLiquidity {
431                side: *side,
432                requested: *requested,
433                available: *available,
434            },
435            OrderBookError::InsufficientLiquidityNotional {
436                side,
437                requested,
438                spent,
439            } => OrderBookError::InsufficientLiquidityNotional {
440                side: *side,
441                requested: *requested,
442                spent: *spent,
443            },
444            OrderBookError::InvalidOperation { message } => OrderBookError::InvalidOperation {
445                message: message.clone(),
446            },
447            OrderBookError::KillSwitchActive => OrderBookError::KillSwitchActive,
448            OrderBookError::SerializationError { message } => OrderBookError::SerializationError {
449                message: message.clone(),
450            },
451            OrderBookError::DeserializationError { message } => {
452                OrderBookError::DeserializationError {
453                    message: message.clone(),
454                }
455            }
456            OrderBookError::ChecksumMismatch { expected, actual } => {
457                OrderBookError::ChecksumMismatch {
458                    expected: expected.clone(),
459                    actual: actual.clone(),
460                }
461            }
462            OrderBookError::InvalidTickSize { price, tick_size } => {
463                OrderBookError::InvalidTickSize {
464                    price: *price,
465                    tick_size: *tick_size,
466                }
467            }
468            OrderBookError::InvalidLotSize { quantity, lot_size } => {
469                OrderBookError::InvalidLotSize {
470                    quantity: *quantity,
471                    lot_size: *lot_size,
472                }
473            }
474            OrderBookError::OrderSizeOutOfRange { quantity, min, max } => {
475                OrderBookError::OrderSizeOutOfRange {
476                    quantity: *quantity,
477                    min: *min,
478                    max: *max,
479                }
480            }
481            OrderBookError::DuplicateOrderId { order_id } => OrderBookError::DuplicateOrderId {
482                order_id: *order_id,
483            },
484            OrderBookError::MissingUserId { order_id } => OrderBookError::MissingUserId {
485                order_id: *order_id,
486            },
487            OrderBookError::SelfTradePrevented {
488                mode,
489                taker_order_id,
490                user_id,
491            } => OrderBookError::SelfTradePrevented {
492                mode: *mode,
493                taker_order_id: *taker_order_id,
494                user_id: *user_id,
495            },
496            OrderBookError::RiskMaxOpenOrders {
497                account,
498                current,
499                limit,
500            } => OrderBookError::RiskMaxOpenOrders {
501                account: *account,
502                current: *current,
503                limit: *limit,
504            },
505            OrderBookError::RiskMaxNotional {
506                account,
507                current,
508                attempted,
509                limit,
510            } => OrderBookError::RiskMaxNotional {
511                account: *account,
512                current: *current,
513                attempted: *attempted,
514                limit: *limit,
515            },
516            OrderBookError::RiskPriceBand {
517                submitted,
518                reference,
519                deviation_bps,
520                limit_bps,
521            } => OrderBookError::RiskPriceBand {
522                submitted: *submitted,
523                reference: *reference,
524                deviation_bps: *deviation_bps,
525                limit_bps: *limit_bps,
526            },
527            #[cfg(feature = "nats")]
528            OrderBookError::NatsPublishError { message } => OrderBookError::NatsPublishError {
529                message: message.clone(),
530            },
531            #[cfg(feature = "nats")]
532            OrderBookError::NatsSerializationError { message } => {
533                OrderBookError::NatsSerializationError {
534                    message: message.clone(),
535                }
536            }
537        }
538    }
539}
540
541/// Errors that can occur in BookManager operations
542#[derive(Debug, Clone)]
543#[non_exhaustive]
544pub enum ManagerError {
545    /// Trade processor has already been started
546    ProcessorAlreadyStarted,
547
548    /// An order book already exists for the symbol; `add_book` refuses to
549    /// overwrite it (which would silently drop the existing book's resting
550    /// orders).
551    BookAlreadyExists {
552        /// The symbol that already has a book.
553        symbol: String,
554    },
555}
556
557impl fmt::Display for ManagerError {
558    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
559        match self {
560            ManagerError::ProcessorAlreadyStarted => {
561                write!(f, "trade processor already started")
562            }
563            ManagerError::BookAlreadyExists { symbol } => {
564                write!(f, "order book already exists for symbol: {symbol}")
565            }
566        }
567    }
568}
569
570impl std::error::Error for ManagerError {}
571
572#[cfg(test)]
573mod tests {
574    use super::*;
575    use pricelevel::{Hash32, Id};
576
577    #[test]
578    fn test_clone_order_not_found() {
579        let error = OrderBookError::OrderNotFound("order123".to_string());
580        let cloned = error.clone();
581        assert!(matches!(cloned, OrderBookError::OrderNotFound(ref s) if s == "order123"));
582    }
583
584    #[test]
585    fn test_clone_invalid_price_level() {
586        let error = OrderBookError::InvalidPriceLevel(12345);
587        let cloned = error.clone();
588        assert!(matches!(cloned, OrderBookError::InvalidPriceLevel(12345)));
589    }
590
591    #[test]
592    fn test_clone_price_crossing() {
593        let error = OrderBookError::PriceCrossing {
594            price: 100,
595            side: Side::Buy,
596            opposite_price: 99,
597        };
598        let cloned = error.clone();
599        assert!(matches!(
600            cloned,
601            OrderBookError::PriceCrossing {
602                price: 100,
603                side: Side::Buy,
604                opposite_price: 99
605            }
606        ));
607    }
608
609    #[test]
610    fn test_clone_insufficient_liquidity() {
611        let error = OrderBookError::InsufficientLiquidity {
612            side: Side::Sell,
613            requested: 1000,
614            available: 500,
615        };
616        let cloned = error.clone();
617        assert!(matches!(
618            cloned,
619            OrderBookError::InsufficientLiquidity {
620                side: Side::Sell,
621                requested: 1000,
622                available: 500
623            }
624        ));
625    }
626
627    #[test]
628    fn test_clone_insufficient_liquidity_notional() {
629        let error = OrderBookError::InsufficientLiquidityNotional {
630            side: Side::Buy,
631            requested: 1_000_000,
632            spent: 0,
633        };
634        let cloned = error.clone();
635        assert!(matches!(
636            cloned,
637            OrderBookError::InsufficientLiquidityNotional {
638                side: Side::Buy,
639                requested: 1_000_000,
640                spent: 0
641            }
642        ));
643    }
644
645    #[test]
646    fn test_display_insufficient_liquidity_notional() {
647        let error = OrderBookError::InsufficientLiquidityNotional {
648            side: Side::Sell,
649            requested: 42,
650            spent: 0,
651        };
652        let s = format!("{error}");
653        assert!(s.contains("Insufficient liquidity by notional"));
654        assert!(s.contains("42"));
655    }
656
657    #[test]
658    fn test_clone_invalid_operation() {
659        let error = OrderBookError::InvalidOperation {
660            message: "Cannot cancel filled order".to_string(),
661        };
662        let cloned = error.clone();
663        assert!(matches!(
664            cloned,
665            OrderBookError::InvalidOperation { ref message } if message == "Cannot cancel filled order"
666        ));
667    }
668
669    #[test]
670    fn test_clone_serialization_error() {
671        let error = OrderBookError::SerializationError {
672            message: "Failed to serialize".to_string(),
673        };
674        let cloned = error.clone();
675        assert!(matches!(
676            cloned,
677            OrderBookError::SerializationError { ref message } if message == "Failed to serialize"
678        ));
679    }
680
681    #[test]
682    fn test_clone_checksum_mismatch() {
683        let error = OrderBookError::ChecksumMismatch {
684            expected: "abc123".to_string(),
685            actual: "def456".to_string(),
686        };
687        let cloned = error.clone();
688        assert!(matches!(
689            cloned,
690            OrderBookError::ChecksumMismatch { ref expected, ref actual }
691            if expected == "abc123" && actual == "def456"
692        ));
693    }
694
695    #[test]
696    fn test_clone_invalid_tick_size() {
697        let error = OrderBookError::InvalidTickSize {
698            price: 10050,
699            tick_size: 100,
700        };
701        let cloned = error.clone();
702        assert!(matches!(
703            cloned,
704            OrderBookError::InvalidTickSize {
705                price: 10050,
706                tick_size: 100
707            }
708        ));
709    }
710
711    #[test]
712    fn test_clone_invalid_lot_size() {
713        let error = OrderBookError::InvalidLotSize {
714            quantity: 75,
715            lot_size: 10,
716        };
717        let cloned = error.clone();
718        assert!(matches!(
719            cloned,
720            OrderBookError::InvalidLotSize {
721                quantity: 75,
722                lot_size: 10
723            }
724        ));
725    }
726
727    #[test]
728    fn test_clone_order_size_out_of_range() {
729        let error = OrderBookError::OrderSizeOutOfRange {
730            quantity: 5,
731            min: Some(10),
732            max: Some(1000),
733        };
734        let cloned = error.clone();
735        assert!(matches!(
736            cloned,
737            OrderBookError::OrderSizeOutOfRange {
738                quantity: 5,
739                min: Some(10),
740                max: Some(1000)
741            }
742        ));
743    }
744
745    #[test]
746    fn test_clone_missing_user_id() {
747        let order_id = Id::new_uuid();
748        let error = OrderBookError::MissingUserId { order_id };
749        let cloned = error.clone();
750        assert!(matches!(
751            cloned,
752            OrderBookError::MissingUserId { order_id: id } if id == order_id
753        ));
754    }
755
756    #[test]
757    fn test_clone_self_trade_prevented() {
758        let taker_id = Id::new_uuid();
759        let user_id = Hash32::from([1u8; 32]);
760        let error = OrderBookError::SelfTradePrevented {
761            mode: crate::orderbook::stp::STPMode::CancelMaker,
762            taker_order_id: taker_id,
763            user_id,
764        };
765        let cloned = error.clone();
766        assert!(matches!(
767            cloned,
768            OrderBookError::SelfTradePrevented {
769                mode: crate::orderbook::stp::STPMode::CancelMaker,
770                taker_order_id: id,
771                user_id: uid
772            } if id == taker_id && uid == user_id
773        ));
774    }
775
776    #[test]
777    fn test_clone_price_level_error_parse_error() {
778        let price_level_err = PriceLevelError::ParseError {
779            message: "Parse failed".to_string(),
780        };
781        let error = OrderBookError::PriceLevelError(price_level_err);
782        let cloned = error.clone();
783        assert!(matches!(
784            cloned,
785            OrderBookError::PriceLevelError(PriceLevelError::ParseError { ref message })
786            if message == "Parse failed"
787        ));
788    }
789
790    #[test]
791    fn test_clone_price_level_error_invalid_format() {
792        let price_level_err = PriceLevelError::InvalidFormat;
793        let error = OrderBookError::PriceLevelError(price_level_err);
794        let cloned = error.clone();
795        assert!(matches!(
796            cloned,
797            OrderBookError::PriceLevelError(PriceLevelError::InvalidFormat)
798        ));
799    }
800
801    #[test]
802    fn test_clone_price_level_error_unknown_order_type() {
803        let price_level_err = PriceLevelError::UnknownOrderType("CUSTOM".to_string());
804        let error = OrderBookError::PriceLevelError(price_level_err);
805        let cloned = error.clone();
806        assert!(matches!(
807            cloned,
808            OrderBookError::PriceLevelError(PriceLevelError::UnknownOrderType(ref s))
809            if s == "CUSTOM"
810        ));
811    }
812
813    #[test]
814    fn test_clone_price_level_error_checksum_mismatch() {
815        let price_level_err = PriceLevelError::ChecksumMismatch {
816            expected: "hash1".to_string(),
817            actual: "hash2".to_string(),
818        };
819        let error = OrderBookError::PriceLevelError(price_level_err);
820        let cloned = error.clone();
821        assert!(matches!(
822            cloned,
823            OrderBookError::PriceLevelError(PriceLevelError::ChecksumMismatch {
824                ref expected,
825                ref actual
826            }) if expected == "hash1" && actual == "hash2"
827        ));
828    }
829}