1use pricelevel::{Hash32, PriceLevelError, Side};
4use std::fmt;
5
6#[derive(Debug)]
8#[non_exhaustive]
9pub enum OrderBookError {
10 PriceLevelError(PriceLevelError),
12
13 OrderNotFound(String),
15
16 InvalidPriceLevel(u128),
18
19 PriceCrossing {
21 price: u128,
23 side: Side,
25 opposite_price: u128,
27 },
28
29 InsufficientLiquidity {
31 side: Side,
33 requested: u64,
35 available: u64,
37 },
38
39 InsufficientLiquidityNotional {
45 side: Side,
47 requested: u128,
49 spent: u128,
53 },
54
55 InvalidOperation {
57 message: String,
59 },
60
61 KillSwitchActive,
65
66 SerializationError {
68 message: String,
70 },
71
72 DeserializationError {
74 message: String,
76 },
77
78 ChecksumMismatch {
80 expected: String,
82 actual: String,
84 },
85
86 InvalidTickSize {
88 price: u128,
90 tick_size: u128,
92 },
93
94 InvalidLotSize {
96 quantity: u64,
98 lot_size: u64,
100 },
101
102 OrderSizeOutOfRange {
104 quantity: u64,
106 min: Option<u64>,
108 max: Option<u64>,
110 },
111
112 DuplicateOrderId {
124 order_id: pricelevel::Id,
126 },
127
128 MissingUserId {
132 order_id: pricelevel::Id,
134 },
135
136 SelfTradePrevented {
139 mode: crate::orderbook::stp::STPMode,
141 taker_order_id: pricelevel::Id,
143 user_id: pricelevel::Hash32,
145 },
146
147 RiskMaxOpenOrders {
154 account: Hash32,
156 current: u64,
158 limit: u64,
160 },
161
162 RiskMaxNotional {
168 account: Hash32,
170 current: u128,
172 attempted: u128,
174 limit: u128,
176 },
177
178 RiskPriceBand {
184 submitted: u128,
186 reference: u128,
188 deviation_bps: u32,
190 limit_bps: u32,
192 },
193
194 #[cfg(feature = "nats")]
196 NatsPublishError {
197 message: String,
199 },
200
201 #[cfg(feature = "nats")]
203 NatsSerializationError {
204 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 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 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#[derive(Debug, Clone)]
543#[non_exhaustive]
544pub enum ManagerError {
545 ProcessorAlreadyStarted,
547
548 BookAlreadyExists {
552 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}