1use crate::error::EMSError;
11use rusty_common::SmartString;
12use rusty_common::collections::FxHashMap;
13use rusty_model::trading_order::Order;
14use smallvec::SmallVec;
15use thiserror::Error;
16
17#[derive(Error, Debug, Clone)]
19pub enum BatchOrderError {
20 #[error("Transport error affecting entire batch: {error}")]
23 TransportError {
24 error: EMSError,
26 affected_orders: usize,
28 is_retryable: bool,
30 },
31
32 #[error("Partial batch failure: {successful_orders} succeeded, {failed_orders} failed")]
35 PartialFailure {
36 successful_orders: usize,
38 failed_orders: usize,
40 order_results: OrderResultMap,
42 },
43
44 #[error("All orders failed validation: {total_orders} orders")]
47 AllOrdersFailed {
48 total_orders: usize,
50 order_results: OrderResultMap,
52 },
53
54 #[error("Empty batch: no orders to process")]
56 EmptyBatch,
57
58 #[error("Batch size {actual} exceeds maximum limit {max_allowed}")]
60 BatchSizeExceeded {
61 actual: usize,
63 max_allowed: usize,
65 },
66}
67
68#[derive(Debug, Clone)]
70pub enum OrderResult<T> {
71 Success(T),
73 Failed {
75 error: EMSError,
77 order: Box<Order>,
79 is_retryable: bool,
81 },
82}
83
84pub type OrderResultMap<T = ()> = FxHashMap<SmartString, OrderResult<T>>;
86
87#[derive(Debug, Clone)]
89pub struct BatchResult<T> {
90 pub status: BatchStatus,
92 pub order_results: OrderResultMap<T>,
94 pub summary: BatchSummary,
96 pub transport_error: Option<EMSError>,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub enum BatchStatus {
103 AllSucceeded,
105 PartialSuccess,
107 AllFailed,
109 TransportFailure,
111}
112
113#[derive(Debug, Clone, Default)]
115pub struct BatchSummary {
116 pub total_orders: usize,
118 pub successful_orders: usize,
120 pub failed_orders: usize,
122 pub retryable_orders: usize,
124 pub processing_time_ns: u64,
126}
127
128impl<T> BatchResult<T> {
129 #[must_use]
131 pub fn success(order_results: OrderResultMap<T>, processing_time_ns: u64) -> Self {
132 let total_orders = order_results.len();
133 Self {
134 status: BatchStatus::AllSucceeded,
135 order_results,
136 summary: BatchSummary {
137 total_orders,
138 successful_orders: total_orders,
139 failed_orders: 0,
140 retryable_orders: 0,
141 processing_time_ns,
142 },
143 transport_error: None,
144 }
145 }
146
147 #[must_use]
149 pub fn partial_success(order_results: OrderResultMap<T>, processing_time_ns: u64) -> Self {
150 let total_orders = order_results.len();
151 let successful_orders = order_results
152 .values()
153 .filter(|result| matches!(result, OrderResult::Success(_)))
154 .count();
155 let failed_orders = total_orders - successful_orders;
156 let retryable_orders = order_results
157 .values()
158 .filter(|result| {
159 matches!(
160 result,
161 OrderResult::Failed {
162 is_retryable: true,
163 ..
164 }
165 )
166 })
167 .count();
168
169 Self {
170 status: BatchStatus::PartialSuccess,
171 order_results,
172 summary: BatchSummary {
173 total_orders,
174 successful_orders,
175 failed_orders,
176 retryable_orders,
177 processing_time_ns,
178 },
179 transport_error: None,
180 }
181 }
182
183 #[must_use]
185 pub fn all_failed(order_results: OrderResultMap<T>, processing_time_ns: u64) -> Self {
186 let total_orders = order_results.len();
187 let retryable_orders = order_results
188 .values()
189 .filter(|result| {
190 matches!(
191 result,
192 OrderResult::Failed {
193 is_retryable: true,
194 ..
195 }
196 )
197 })
198 .count();
199
200 Self {
201 status: BatchStatus::AllFailed,
202 order_results,
203 summary: BatchSummary {
204 total_orders,
205 successful_orders: 0,
206 failed_orders: total_orders,
207 retryable_orders,
208 processing_time_ns,
209 },
210 transport_error: None,
211 }
212 }
213
214 #[must_use]
216 pub fn transport_failure(
217 error: EMSError,
218 total_orders: usize,
219 processing_time_ns: u64,
220 ) -> Self {
221 Self {
222 status: BatchStatus::TransportFailure,
223 order_results: FxHashMap::default(),
224 summary: BatchSummary {
225 total_orders,
226 successful_orders: 0,
227 failed_orders: total_orders,
228 retryable_orders: if error.is_recoverable() {
229 total_orders
230 } else {
231 0
232 },
233 processing_time_ns,
234 },
235 transport_error: Some(error),
236 }
237 }
238
239 #[must_use]
241 pub const fn has_successes(&self) -> bool {
242 self.summary.successful_orders > 0
243 }
244
245 #[must_use]
247 pub const fn has_failures(&self) -> bool {
248 self.summary.failed_orders > 0
249 }
250
251 #[must_use]
253 pub const fn has_retryable_orders(&self) -> bool {
254 self.summary.retryable_orders > 0
255 }
256
257 #[must_use]
259 pub fn success_rate(&self) -> f64 {
260 if self.summary.total_orders == 0 {
261 0.0
262 } else {
263 (self.summary.successful_orders as f64 / self.summary.total_orders as f64) * 100.0
264 }
265 }
266
267 #[must_use]
269 pub fn get_retryable_orders(&self) -> SmallVec<[Order; 8]> {
270 self.order_results
271 .values()
272 .filter_map(|result| {
273 if let OrderResult::Failed {
274 order,
275 is_retryable: true,
276 ..
277 } = result
278 {
279 Some((**order).clone())
280 } else {
281 None
282 }
283 })
284 .collect()
285 }
286
287 #[must_use]
289 pub fn get_failures_by_error_type(&self) -> FxHashMap<String, SmallVec<[Order; 4]>> {
290 let mut failures: FxHashMap<String, SmallVec<[Order; 4]>> = FxHashMap::default();
291
292 for result in self.order_results.values() {
293 if let OrderResult::Failed { error, order, .. } = result {
294 let error_type = match error {
295 EMSError::InvalidOrderParameters(_) => "Invalid Parameters",
296 EMSError::InsufficientBalance(_) => "Insufficient Balance",
297 EMSError::InstrumentNotFound(_) => "Instrument Not Found",
298 EMSError::RateLimitExceeded { .. } => "Rate Limit",
299 _ => "Other",
300 };
301 failures
302 .entry(error_type.to_string())
303 .or_default()
304 .push((**order).clone());
305 }
306 }
307
308 failures
309 }
310}
311
312impl BatchOrderError {
313 #[must_use]
315 pub const fn transport(error: EMSError, affected_orders: usize) -> Self {
316 let is_retryable = error.is_recoverable();
317 Self::TransportError {
318 error,
319 affected_orders,
320 is_retryable,
321 }
322 }
323
324 #[must_use]
326 pub fn partial_failure(order_results: OrderResultMap) -> Self {
327 let successful_orders = order_results
328 .values()
329 .filter(|result| matches!(result, OrderResult::Success(())))
330 .count();
331 let failed_orders = order_results.len() - successful_orders;
332
333 Self::PartialFailure {
334 successful_orders,
335 failed_orders,
336 order_results,
337 }
338 }
339
340 #[must_use]
342 pub fn all_failed(order_results: OrderResultMap) -> Self {
343 let total_orders = order_results.len();
344 Self::AllOrdersFailed {
345 total_orders,
346 order_results,
347 }
348 }
349
350 #[must_use]
352 pub const fn size_exceeded(actual: usize, max_allowed: usize) -> Self {
353 Self::BatchSizeExceeded {
354 actual,
355 max_allowed,
356 }
357 }
358
359 #[must_use]
361 pub fn is_retryable(&self) -> bool {
362 match self {
363 Self::TransportError { is_retryable, .. } => *is_retryable,
364 Self::PartialFailure { order_results, .. } => {
365 order_results.values().any(|result| {
367 matches!(
368 result,
369 OrderResult::Failed {
370 is_retryable: true,
371 ..
372 }
373 )
374 })
375 }
376 Self::AllOrdersFailed { order_results, .. } => {
377 order_results.values().any(|result| {
379 matches!(
380 result,
381 OrderResult::Failed {
382 is_retryable: true,
383 ..
384 }
385 )
386 })
387 }
388 Self::EmptyBatch | Self::BatchSizeExceeded { .. } => false,
389 }
390 }
391
392 #[must_use]
394 pub fn get_retryable_orders(&self) -> SmallVec<[Order; 8]> {
395 match self {
396 Self::TransportError { .. } => {
397 SmallVec::new()
400 }
401 Self::PartialFailure { order_results, .. }
402 | Self::AllOrdersFailed { order_results, .. } => order_results
403 .values()
404 .filter_map(|result| {
405 if let OrderResult::Failed {
406 order,
407 is_retryable: true,
408 ..
409 } = result
410 {
411 Some((**order).clone())
412 } else {
413 None
414 }
415 })
416 .collect(),
417 Self::EmptyBatch | Self::BatchSizeExceeded { .. } => SmallVec::new(),
418 }
419 }
420
421 #[must_use]
423 pub fn get_error_stats(&self) -> BatchErrorStats {
424 match self {
425 Self::TransportError {
426 affected_orders,
427 is_retryable,
428 ..
429 } => BatchErrorStats {
430 total_orders: *affected_orders,
431 transport_failures: 1,
432 retryable_failures: usize::from(*is_retryable),
433 ..Default::default()
434 },
435 Self::PartialFailure {
436 successful_orders,
437 failed_orders,
438 order_results,
439 ..
440 } => {
441 let retryable_failures = order_results
442 .values()
443 .filter(|result| {
444 matches!(
445 result,
446 OrderResult::Failed {
447 is_retryable: true,
448 ..
449 }
450 )
451 })
452 .count();
453
454 BatchErrorStats {
455 total_orders: successful_orders + failed_orders,
456 successful_orders: *successful_orders,
457 per_order_failures: *failed_orders,
458 retryable_failures,
459 ..Default::default()
460 }
461 }
462 Self::AllOrdersFailed {
463 total_orders,
464 order_results,
465 ..
466 } => {
467 let retryable_failures = order_results
468 .values()
469 .filter(|result| {
470 matches!(
471 result,
472 OrderResult::Failed {
473 is_retryable: true,
474 ..
475 }
476 )
477 })
478 .count();
479
480 BatchErrorStats {
481 total_orders: *total_orders,
482 per_order_failures: *total_orders,
483 retryable_failures,
484 ..Default::default()
485 }
486 }
487 Self::EmptyBatch => BatchErrorStats::default(),
488 Self::BatchSizeExceeded { actual, .. } => BatchErrorStats {
489 total_orders: *actual,
490 validation_failures: 1,
491 ..Default::default()
492 },
493 }
494 }
495}
496
497#[derive(Debug, Clone, Default)]
499pub struct BatchErrorStats {
500 pub total_orders: usize,
502 pub successful_orders: usize,
504 pub transport_failures: usize,
506 pub per_order_failures: usize,
508 pub validation_failures: usize,
510 pub retryable_failures: usize,
512}
513
514impl<T> OrderResult<T> {
515 pub const fn success(value: T) -> Self {
517 Self::Success(value)
518 }
519
520 #[must_use]
522 pub fn failed(error: EMSError, order: Order) -> Self {
523 let is_retryable = error.is_recoverable();
524 Self::Failed {
525 error,
526 order: Box::new(order),
527 is_retryable,
528 }
529 }
530
531 pub const fn is_success(&self) -> bool {
533 matches!(self, Self::Success(_))
534 }
535
536 pub const fn is_retryable(&self) -> bool {
538 matches!(
539 self,
540 Self::Failed {
541 is_retryable: true,
542 ..
543 }
544 )
545 }
546
547 pub const fn error(&self) -> Option<&EMSError> {
549 match self {
550 Self::Failed { error, .. } => Some(error),
551 _ => None,
552 }
553 }
554
555 pub fn failed_order(&self) -> Option<&Order> {
557 match self {
558 Self::Failed { order, .. } => Some(order),
559 _ => None,
560 }
561 }
562}
563
564pub trait ErrorClassification {
566 fn is_per_order_error(&self) -> bool;
568
569 fn is_transport_error(&self) -> bool;
571}
572
573impl ErrorClassification for EMSError {
574 fn is_per_order_error(&self) -> bool {
575 matches!(
576 self,
577 Self::InvalidOrderParameters(_)
578 | Self::InsufficientBalance(_)
579 | Self::InstrumentNotFound(_)
580 | Self::OrderSubmissionError(_)
581 | Self::OrderCancellationError(_)
582 | Self::OrderModificationError(_)
583 )
584 }
585
586 fn is_transport_error(&self) -> bool {
587 matches!(
588 self,
589 Self::ConnectionError(_)
590 | Self::AuthenticationError(_)
591 | Self::RateLimitExceeded { .. }
592 | Self::NetworkError(_)
593 | Self::Timeout { .. }
594 | Self::WebSocketError(_)
595 | Self::WebSocketFrameError(_)
596 )
597 }
598}
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603 use rust_decimal_macros::dec;
604 use rusty_model::{
605 enums::{OrderSide, OrderType},
606 trading_order::Order,
607 types::ClientId,
608 venues::Venue,
609 };
610
611 fn create_test_order(symbol: &str) -> Order {
612 Order::new(
613 Venue::Binance,
614 symbol,
615 OrderSide::Buy,
616 OrderType::Limit,
617 dec!(100),
618 Some(dec!(50000)),
619 ClientId::from("test_client"),
620 )
621 }
622
623 #[test]
624 fn test_batch_result_creation() {
625 let mut order_results = OrderResultMap::default();
626 order_results.insert("order1".into(), OrderResult::success(()));
627 order_results.insert("order2".into(), OrderResult::success(()));
628
629 let result = BatchResult::success(order_results, 1000000);
630
631 assert_eq!(result.status, BatchStatus::AllSucceeded);
632 assert_eq!(result.summary.total_orders, 2);
633 assert_eq!(result.summary.successful_orders, 2);
634 assert_eq!(result.summary.failed_orders, 0);
635 assert_eq!(result.success_rate(), 100.0);
636 }
637
638 #[test]
639 fn test_partial_failure_result() {
640 let mut order_results = OrderResultMap::default();
641 order_results.insert("order1".into(), OrderResult::success(()));
642 order_results.insert(
643 "order2".into(),
644 OrderResult::failed(
645 EMSError::invalid_params("Invalid price"),
646 create_test_order("BTCUSDT"),
647 ),
648 );
649
650 let result = BatchResult::partial_success(order_results, 1500000);
651
652 assert_eq!(result.status, BatchStatus::PartialSuccess);
653 assert_eq!(result.summary.successful_orders, 1);
654 assert_eq!(result.summary.failed_orders, 1);
655 assert_eq!(result.success_rate(), 50.0);
656 assert!(result.has_successes());
657 assert!(result.has_failures());
658 }
659
660 #[test]
661 fn test_transport_error_classification() {
662 let connection_error = EMSError::connection("Network down");
663 let validation_error = EMSError::invalid_params("Bad price");
664
665 assert!(connection_error.is_transport_error());
666 assert!(!connection_error.is_per_order_error());
667
668 assert!(!validation_error.is_transport_error());
669 assert!(validation_error.is_per_order_error());
670 }
671
672 #[test]
673 fn test_retryable_orders_extraction() {
674 let mut order_results = OrderResultMap::default();
675
676 order_results.insert(
678 "order1".into(),
679 OrderResult::<()>::failed(
680 EMSError::invalid_params("Invalid symbol"),
681 create_test_order("INVALID"),
682 ),
683 );
684
685 order_results.insert(
687 "order2".into(),
688 OrderResult::failed(
689 EMSError::connection("Temporary network issue"),
690 create_test_order("BTCUSDT"),
691 ),
692 );
693
694 let result = BatchResult::partial_success(order_results, 1000000);
695 let retryable = result.get_retryable_orders();
696
697 assert_eq!(retryable.len(), 1);
698 assert_eq!(retryable[0].symbol, "BTCUSDT");
699 }
700
701 #[test]
702 fn test_error_grouping() {
703 let mut order_results = OrderResultMap::default();
704
705 order_results.insert(
706 "order1".into(),
707 OrderResult::<()>::failed(
708 EMSError::invalid_params("Bad price"),
709 create_test_order("BTCUSDT"),
710 ),
711 );
712
713 order_results.insert(
714 "order2".into(),
715 OrderResult::<()>::failed(
716 EMSError::invalid_params("Bad quantity"),
717 create_test_order("ETHUSDT"),
718 ),
719 );
720
721 order_results.insert(
722 "order3".into(),
723 OrderResult::failed(
724 EMSError::insufficient_balance("Not enough USDT"),
725 create_test_order("ADAUSDT"),
726 ),
727 );
728
729 let result = BatchResult::all_failed(order_results, 1000000);
730 let failures_by_type = result.get_failures_by_error_type();
731
732 assert_eq!(failures_by_type.get("Invalid Parameters").unwrap().len(), 2);
733 assert_eq!(
734 failures_by_type.get("Insufficient Balance").unwrap().len(),
735 1
736 );
737 }
738
739 #[test]
740 fn test_batch_status_all_succeeded() {
741 let mut order_results = OrderResultMap::default();
742 order_results.insert("order1".into(), OrderResult::success(()));
743 order_results.insert("order2".into(), OrderResult::success(()));
744 order_results.insert("order3".into(), OrderResult::success(()));
745
746 let result = BatchResult::success(order_results, 1000000);
747
748 assert_eq!(result.status, BatchStatus::AllSucceeded);
749 assert_eq!(result.summary.total_orders, 3);
750 assert_eq!(result.summary.successful_orders, 3);
751 assert_eq!(result.summary.failed_orders, 0);
752 assert_eq!(result.summary.retryable_orders, 0);
753 assert!(result.transport_error.is_none());
754 assert!(result.has_successes());
755 assert!(!result.has_failures());
756 assert!(!result.has_retryable_orders());
757 assert_eq!(result.success_rate(), 100.0);
758 }
759
760 #[test]
761 fn test_batch_status_partial_success() {
762 let mut order_results = OrderResultMap::default();
763 order_results.insert("order1".into(), OrderResult::success(()));
764 order_results.insert("order2".into(), OrderResult::success(()));
765 order_results.insert(
766 "order3".into(),
767 OrderResult::failed(
768 EMSError::invalid_params("Invalid price"),
769 create_test_order("BTCUSDT"),
770 ),
771 );
772 order_results.insert(
773 "order4".into(),
774 OrderResult::failed(
775 EMSError::connection("Network issue"), create_test_order("ETHUSDT"),
777 ),
778 );
779
780 let result = BatchResult::partial_success(order_results, 2000000);
781
782 assert_eq!(result.status, BatchStatus::PartialSuccess);
783 assert_eq!(result.summary.total_orders, 4);
784 assert_eq!(result.summary.successful_orders, 2);
785 assert_eq!(result.summary.failed_orders, 2);
786 assert_eq!(result.summary.retryable_orders, 1); assert!(result.transport_error.is_none());
788 assert!(result.has_successes());
789 assert!(result.has_failures());
790 assert!(result.has_retryable_orders());
791 assert_eq!(result.success_rate(), 50.0);
792
793 let retryable = result.get_retryable_orders();
795 assert_eq!(retryable.len(), 1);
796 assert_eq!(retryable[0].symbol, "ETHUSDT");
797 }
798
799 #[test]
800 fn test_batch_status_all_failed() {
801 let mut order_results = OrderResultMap::default();
802 order_results.insert(
803 "order1".into(),
804 OrderResult::<()>::failed(
805 EMSError::invalid_params("Bad price"),
806 create_test_order("BTCUSDT"),
807 ),
808 );
809 order_results.insert(
810 "order2".into(),
811 OrderResult::<()>::failed(
812 EMSError::insufficient_balance("Not enough funds"),
813 create_test_order("ETHUSDT"),
814 ),
815 );
816 order_results.insert(
817 "order3".into(),
818 OrderResult::failed(
819 EMSError::connection("Temporary network issue"), create_test_order("ADAUSDT"),
821 ),
822 );
823
824 let result = BatchResult::all_failed(order_results, 3000000);
825
826 assert_eq!(result.status, BatchStatus::AllFailed);
827 assert_eq!(result.summary.total_orders, 3);
828 assert_eq!(result.summary.successful_orders, 0);
829 assert_eq!(result.summary.failed_orders, 3);
830 assert_eq!(result.summary.retryable_orders, 1); assert!(result.transport_error.is_none());
832 assert!(!result.has_successes());
833 assert!(result.has_failures());
834 assert!(result.has_retryable_orders());
835 assert_eq!(result.success_rate(), 0.0);
836
837 let failures_by_type = result.get_failures_by_error_type();
839 assert_eq!(failures_by_type.get("Invalid Parameters").unwrap().len(), 1);
840 assert_eq!(
841 failures_by_type.get("Insufficient Balance").unwrap().len(),
842 1
843 );
844 assert_eq!(failures_by_type.get("Other").unwrap().len(), 1); }
846
847 #[test]
848 fn test_batch_status_transport_failure() {
849 let transport_error = EMSError::connection("Exchange API down");
850 let result = BatchResult::<()>::transport_failure(
851 transport_error.clone(),
852 5, 500000,
854 );
855
856 assert_eq!(result.status, BatchStatus::TransportFailure);
857 assert_eq!(result.summary.total_orders, 5);
858 assert_eq!(result.summary.successful_orders, 0);
859 assert_eq!(result.summary.failed_orders, 5);
860 assert_eq!(result.summary.retryable_orders, 5); assert!(result.transport_error.is_some());
862 assert_eq!(
863 result.transport_error.as_ref().unwrap().to_string(),
864 transport_error.to_string()
865 );
866 assert!(result.order_results.is_empty()); assert!(!result.has_successes());
868 assert!(result.has_failures());
869 assert!(result.has_retryable_orders());
870 assert_eq!(result.success_rate(), 0.0);
871 }
872
873 #[test]
874 fn test_batch_status_transport_failure_non_retryable() {
875 let transport_error = EMSError::auth("Invalid API key");
876 let result = BatchResult::<()>::transport_failure(transport_error, 3, 750000);
877
878 assert_eq!(result.status, BatchStatus::TransportFailure);
879 assert_eq!(result.summary.total_orders, 3);
880 assert_eq!(result.summary.successful_orders, 0);
881 assert_eq!(result.summary.failed_orders, 3);
882 assert_eq!(result.summary.retryable_orders, 0); assert!(result.transport_error.is_some());
884 assert!(!result.has_retryable_orders());
885 }
886
887 #[test]
888 fn test_empty_batch_result() {
889 let order_results: OrderResultMap = FxHashMap::default();
890 let result = BatchResult::success(order_results, 100000);
891
892 assert_eq!(result.status, BatchStatus::AllSucceeded);
893 assert_eq!(result.summary.total_orders, 0);
894 assert_eq!(result.summary.successful_orders, 0);
895 assert_eq!(result.summary.failed_orders, 0);
896 assert_eq!(result.success_rate(), 0.0); assert!(!result.has_successes());
898 assert!(!result.has_failures());
899 }
900}