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