1use std::{fmt::Debug, sync::Arc};
65
66use nautilus_common::{
67 live::runner::{replace_data_event_sender, replace_exec_event_sender},
68 messages::{
69 DataEvent, ExecutionEvent, ExecutionReport, data::DataCommand, execution::TradingCommand,
70 },
71 msgbus::{self, MessagingSwitchboard},
72 runner::{
73 DataCommandSender, TimeEventSender, TradingCommandSender, replace_data_cmd_sender,
74 replace_exec_cmd_sender, replace_time_event_sender,
75 },
76 timer::TimeEventHandler,
77};
78use nautilus_model::events::OrderEventAny;
79
80#[derive(Debug)]
82pub struct AsyncDataCommandSender {
83 cmd_tx: tokio::sync::mpsc::UnboundedSender<DataCommand>,
84}
85
86impl AsyncDataCommandSender {
87 #[must_use]
88 pub const fn new(cmd_tx: tokio::sync::mpsc::UnboundedSender<DataCommand>) -> Self {
89 Self { cmd_tx }
90 }
91}
92
93impl DataCommandSender for AsyncDataCommandSender {
94 fn execute(&self, command: DataCommand) {
95 if let Err(e) = self.cmd_tx.send(command) {
96 log::error!("Failed to send data command: {e}");
97 }
98 }
99}
100
101#[derive(Debug, Clone)]
103pub struct AsyncTimeEventSender {
104 time_tx: tokio::sync::mpsc::UnboundedSender<TimeEventHandler>,
105}
106
107impl AsyncTimeEventSender {
108 #[must_use]
109 pub const fn new(time_tx: tokio::sync::mpsc::UnboundedSender<TimeEventHandler>) -> Self {
110 Self { time_tx }
111 }
112}
113
114impl TimeEventSender for AsyncTimeEventSender {
115 fn send(&self, handler: TimeEventHandler) {
116 if let Err(e) = self.time_tx.send(handler) {
117 log::error!("Failed to send time event handler: {e}");
118 }
119 }
120}
121
122#[derive(Debug)]
124pub struct AsyncTradingCommandSender {
125 cmd_tx: tokio::sync::mpsc::UnboundedSender<TradingCommand>,
126}
127
128impl AsyncTradingCommandSender {
129 #[must_use]
130 pub const fn new(cmd_tx: tokio::sync::mpsc::UnboundedSender<TradingCommand>) -> Self {
131 Self { cmd_tx }
132 }
133}
134
135impl TradingCommandSender for AsyncTradingCommandSender {
136 fn execute(&self, command: TradingCommand) {
137 if let Err(e) = self.cmd_tx.send(command) {
138 log::error!("Failed to send trading command: {e}");
139 }
140 }
141}
142
143pub trait Runner {
144 fn run(&mut self);
145}
146
147#[derive(Debug)]
152pub struct AsyncRunnerChannels {
153 pub time_evt_rx: tokio::sync::mpsc::UnboundedReceiver<TimeEventHandler>,
154 pub exec_evt_rx: tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
155 pub exec_cmd_rx: tokio::sync::mpsc::UnboundedReceiver<TradingCommand>,
156 pub data_evt_rx: tokio::sync::mpsc::UnboundedReceiver<DataEvent>,
157 pub data_cmd_rx: tokio::sync::mpsc::UnboundedReceiver<DataCommand>,
158}
159
160pub struct AsyncRunner {
161 channels: AsyncRunnerChannels,
162 time_evt_tx: tokio::sync::mpsc::UnboundedSender<TimeEventHandler>,
163 signal_rx: tokio::sync::mpsc::UnboundedReceiver<()>,
164 signal_tx: tokio::sync::mpsc::UnboundedSender<()>,
165 exec_cmd_tx: tokio::sync::mpsc::UnboundedSender<TradingCommand>,
166 exec_evt_tx: tokio::sync::mpsc::UnboundedSender<ExecutionEvent>,
167 data_cmd_tx: tokio::sync::mpsc::UnboundedSender<DataCommand>,
168 data_evt_tx: tokio::sync::mpsc::UnboundedSender<DataEvent>,
169}
170
171#[derive(Clone, Debug)]
173pub struct AsyncRunnerHandle {
174 signal_tx: tokio::sync::mpsc::UnboundedSender<()>,
175}
176
177impl AsyncRunnerHandle {
178 pub fn stop(&self) {
180 if let Err(e) = self.signal_tx.send(()) {
181 log::error!("Failed to send shutdown signal: {e}");
182 }
183 }
184}
185
186impl Default for AsyncRunner {
187 fn default() -> Self {
188 Self::new()
189 }
190}
191
192impl Debug for AsyncRunner {
193 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194 f.debug_struct(stringify!(AsyncRunner)).finish()
195 }
196}
197
198impl AsyncRunner {
199 #[must_use]
205 pub fn new() -> Self {
206 use tokio::sync::mpsc::unbounded_channel; let (time_evt_tx, time_evt_rx) = unbounded_channel::<TimeEventHandler>();
209 let (signal_tx, signal_rx) = unbounded_channel::<()>();
210 let (exec_cmd_tx, exec_cmd_rx) = unbounded_channel::<TradingCommand>();
211 let (exec_evt_tx, exec_evt_rx) = unbounded_channel::<ExecutionEvent>();
212 let (data_cmd_tx, data_cmd_rx) = unbounded_channel::<DataCommand>();
213 let (data_evt_tx, data_evt_rx) = unbounded_channel::<DataEvent>();
214
215 Self {
216 channels: AsyncRunnerChannels {
217 time_evt_rx,
218 exec_evt_rx,
219 exec_cmd_rx,
220 data_evt_rx,
221 data_cmd_rx,
222 },
223 time_evt_tx,
224 signal_rx,
225 signal_tx,
226 exec_cmd_tx,
227 exec_evt_tx,
228 data_cmd_tx,
229 data_evt_tx,
230 }
231 }
232
233 pub fn bind_senders(&self) {
239 replace_time_event_sender(Arc::new(AsyncTimeEventSender::new(
240 self.time_evt_tx.clone(),
241 )));
242 replace_exec_cmd_sender(Arc::new(AsyncTradingCommandSender::new(
243 self.exec_cmd_tx.clone(),
244 )));
245 replace_exec_event_sender(self.exec_evt_tx.clone());
246 replace_data_cmd_sender(Arc::new(AsyncDataCommandSender::new(
247 self.data_cmd_tx.clone(),
248 )));
249 replace_data_event_sender(self.data_evt_tx.clone());
250 }
251
252 pub fn stop(&self) {
254 if let Err(e) = self.signal_tx.send(()) {
255 log::error!("Failed to send shutdown signal: {e}");
256 }
257 }
258
259 #[must_use]
261 pub fn handle(&self) -> AsyncRunnerHandle {
262 AsyncRunnerHandle {
263 signal_tx: self.signal_tx.clone(),
264 }
265 }
266
267 #[must_use]
272 pub fn take_channels(self) -> AsyncRunnerChannels {
273 self.channels
274 }
275
276 pub fn flush_pending_data(&mut self) {
282 let mut total = 0;
283
284 loop {
285 let mut progressed = false;
286
287 while let Ok(evt) = self.channels.data_evt_rx.try_recv() {
294 Self::handle_data_event(evt);
295 progressed = true;
296 total += 1;
297 }
298
299 while let Ok(cmd) = self.channels.data_cmd_rx.try_recv() {
300 Self::handle_data_command(cmd);
301 progressed = true;
302 total += 1;
303 }
304
305 if !progressed {
306 break;
307 }
308 }
309
310 if total > 0 {
311 log::debug!("Flushed {total} pending data events/commands");
312 }
313 }
314
315 pub async fn run(&mut self) {
320 self.bind_senders();
321
322 log::info!("AsyncRunner starting");
323
324 loop {
325 tokio::select! {
326 biased;
327
328 Some(()) = self.signal_rx.recv() => {
329 log::info!("AsyncRunner received signal, shutting down");
330 return;
331 },
332 Some(handler) = self.channels.time_evt_rx.recv() => {
333 Self::handle_time_event(handler);
334 },
335 Some(cmd) = self.channels.exec_cmd_rx.recv() => {
336 Self::handle_exec_command(cmd);
337 },
338 Some(evt) = self.channels.exec_evt_rx.recv() => {
339 Self::handle_exec_event(evt);
340 },
341 Some(cmd) = self.channels.data_cmd_rx.recv() => {
342 Self::handle_data_command(cmd);
343 },
344 Some(evt) = self.channels.data_evt_rx.recv() => {
345 Self::handle_data_event(evt);
346 },
347 else => {
348 log::debug!("AsyncRunner all channels closed, exiting");
349 return;
350 }
351 };
352 }
353 }
354
355 #[inline]
357 pub fn handle_time_event(handler: TimeEventHandler) {
358 handler.run();
359 }
360
361 #[inline]
363 pub fn handle_data_command(cmd: DataCommand) {
364 msgbus::send_data_command(MessagingSwitchboard::data_engine_execute(), cmd);
365 }
366
367 #[inline]
369 pub fn handle_data_event(event: DataEvent) {
370 match event {
371 DataEvent::Data(data) => {
372 msgbus::send_data(MessagingSwitchboard::data_engine_process_data(), data);
373 }
374 DataEvent::Instrument(data) => {
375 msgbus::send_any(MessagingSwitchboard::data_engine_process(), &data);
376 }
377 DataEvent::Response(resp) => {
378 msgbus::send_data_response(MessagingSwitchboard::data_engine_response(), resp);
379 }
380 DataEvent::FundingRate(funding_rate) => {
381 msgbus::send_any(MessagingSwitchboard::data_engine_process(), &funding_rate);
382 }
383 DataEvent::InstrumentStatus(status) => {
384 msgbus::send_any(MessagingSwitchboard::data_engine_process(), &status);
385 }
386 DataEvent::OptionGreeks(greeks) => {
387 msgbus::send_any(MessagingSwitchboard::data_engine_process(), &greeks);
388 }
389 #[cfg(feature = "defi")]
390 DataEvent::DeFi(data) => {
391 msgbus::send_defi_data(MessagingSwitchboard::data_engine_process_defi_data(), data);
392 }
393 }
394 }
395
396 #[inline]
398 pub fn handle_exec_command(cmd: TradingCommand) {
399 msgbus::send_trading_command(MessagingSwitchboard::exec_engine_execute(), cmd);
400 }
401
402 #[inline]
404 pub fn handle_exec_event(event: ExecutionEvent) {
405 match event {
406 ExecutionEvent::Order(order_event) => {
407 msgbus::send_order_event(MessagingSwitchboard::exec_engine_process(), order_event);
408 }
409 ExecutionEvent::OrderSubmittedBatch(batch) => {
410 for submitted in batch {
411 msgbus::send_order_event(
412 MessagingSwitchboard::exec_engine_process(),
413 OrderEventAny::Submitted(submitted),
414 );
415 }
416 }
417 ExecutionEvent::OrderAcceptedBatch(batch) => {
418 for accepted in batch {
419 msgbus::send_order_event(
420 MessagingSwitchboard::exec_engine_process(),
421 OrderEventAny::Accepted(accepted),
422 );
423 }
424 }
425 ExecutionEvent::OrderCanceledBatch(batch) => {
426 for canceled in batch {
427 msgbus::send_order_event(
428 MessagingSwitchboard::exec_engine_process(),
429 OrderEventAny::Canceled(canceled),
430 );
431 }
432 }
433 ExecutionEvent::Report(report) => {
434 Self::handle_exec_report(report);
435 }
436 ExecutionEvent::Account(ref account) => {
437 msgbus::send_account_state(
438 MessagingSwitchboard::portfolio_update_account(),
439 account,
440 );
441 }
442 }
443 }
444
445 #[inline]
446 pub fn handle_exec_report(report: ExecutionReport) {
447 let endpoint = MessagingSwitchboard::exec_engine_reconcile_execution_report();
448 msgbus::send_execution_report(endpoint, report);
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use std::time::Duration;
455
456 use nautilus_common::{
457 live::runner::{get_data_event_sender, get_exec_event_sender},
458 messages::{
459 ExecutionEvent, ExecutionReport,
460 data::{SubscribeCommand, SubscribeCustomData},
461 execution::{CancelAllOrders, TradingCommand},
462 },
463 runner::{
464 get_data_cmd_sender, get_time_event_sender, get_trading_cmd_sender,
465 try_get_time_event_sender, try_get_trading_cmd_sender,
466 },
467 timer::{TimeEvent, TimeEventCallback, TimeEventHandler},
468 };
469 use nautilus_core::{UUID4, UnixNanos};
470 use nautilus_model::{
471 data::{Data, DataType, quote::QuoteTick},
472 enums::{
473 AccountType, LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSideSpecified,
474 TimeInForce,
475 },
476 events::{
477 OrderAcceptedBatch, OrderCanceledBatch, OrderEvent, OrderEventAny, OrderSubmittedBatch,
478 account::state::AccountState,
479 order::spec::{OrderAcceptedSpec, OrderCanceledSpec, OrderSubmittedSpec},
480 },
481 identifiers::{
482 AccountId, ClientId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId,
483 TraderId, VenueOrderId,
484 },
485 reports::{FillReport, OrderStatusReport, PositionStatusReport},
486 types::{Money, Price, Quantity},
487 };
488 use rstest::rstest;
489 use ustr::Ustr;
490
491 use super::*;
492
493 fn test_quote() -> QuoteTick {
495 QuoteTick {
496 instrument_id: InstrumentId::from("EUR/USD.SIM"),
497 bid_price: Price::from("1.10000"),
498 ask_price: Price::from("1.10001"),
499 bid_size: Quantity::from(1_000_000),
500 ask_size: Quantity::from(1_000_000),
501 ts_event: UnixNanos::default(),
502 ts_init: UnixNanos::default(),
503 }
504 }
505
506 fn create_test_runner(
510 time_evt_rx: tokio::sync::mpsc::UnboundedReceiver<TimeEventHandler>,
511 data_evt_rx: tokio::sync::mpsc::UnboundedReceiver<DataEvent>,
512 data_cmd_rx: tokio::sync::mpsc::UnboundedReceiver<DataCommand>,
513 exec_evt_rx: tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
514 exec_cmd_rx: tokio::sync::mpsc::UnboundedReceiver<TradingCommand>,
515 signal_rx: tokio::sync::mpsc::UnboundedReceiver<()>,
516 signal_tx: tokio::sync::mpsc::UnboundedSender<()>,
517 ) -> AsyncRunner {
518 let (time_evt_tx, _) = tokio::sync::mpsc::unbounded_channel();
519 let (data_cmd_tx, _) = tokio::sync::mpsc::unbounded_channel();
520 let (data_evt_tx, _) = tokio::sync::mpsc::unbounded_channel();
521 let (exec_cmd_tx, _) = tokio::sync::mpsc::unbounded_channel();
522 let (exec_evt_tx, _) = tokio::sync::mpsc::unbounded_channel();
523
524 AsyncRunner {
525 channels: AsyncRunnerChannels {
526 time_evt_rx,
527 exec_evt_rx,
528 exec_cmd_rx,
529 data_evt_rx,
530 data_cmd_rx,
531 },
532 time_evt_tx,
533 exec_cmd_tx,
534 exec_evt_tx,
535 data_cmd_tx,
536 data_evt_tx,
537 signal_rx,
538 signal_tx,
539 }
540 }
541
542 #[rstest]
543 fn test_async_data_command_sender_creation() {
544 let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
545 let sender = AsyncDataCommandSender::new(tx);
546 assert!(format!("{sender:?}").contains("AsyncDataCommandSender"));
547 }
548
549 #[rstest]
550 fn test_async_time_event_sender_creation() {
551 let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
552 let sender = AsyncTimeEventSender::new(tx);
553 assert!(format!("{sender:?}").contains("AsyncTimeEventSender"));
554 }
555
556 #[tokio::test]
557 async fn test_async_data_command_sender_execute() {
558 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
559 let sender = AsyncDataCommandSender::new(tx);
560
561 let command = DataCommand::Subscribe(SubscribeCommand::Data(SubscribeCustomData {
562 client_id: Some(ClientId::from("TEST")),
563 venue: None,
564 data_type: DataType::new("QuoteTick", None, None),
565 command_id: UUID4::new(),
566 ts_init: UnixNanos::default(),
567 correlation_id: None,
568 params: None,
569 }));
570
571 sender.execute(command.clone());
572
573 let received = rx.recv().await.unwrap();
574 match (received, command) {
575 (
576 DataCommand::Subscribe(SubscribeCommand::Data(r)),
577 DataCommand::Subscribe(SubscribeCommand::Data(c)),
578 ) => {
579 assert_eq!(r.client_id, c.client_id);
580 assert_eq!(r.data_type, c.data_type);
581 }
582 _ => panic!("Command mismatch"),
583 }
584 }
585
586 #[tokio::test]
587 async fn test_async_time_event_sender_send() {
588 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
589 let sender = AsyncTimeEventSender::new(tx);
590
591 let event = TimeEvent::new(
592 Ustr::from("test"),
593 UUID4::new(),
594 UnixNanos::from(1),
595 UnixNanos::from(2),
596 );
597 let callback = TimeEventCallback::from(|_: TimeEvent| {});
598 let handler = TimeEventHandler::new(event, callback);
599
600 sender.send(handler);
601
602 assert!(rx.recv().await.is_some());
603 }
604
605 #[tokio::test]
606 async fn test_runner_shutdown_signal() {
607 let (_data_tx, data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
609 let (_cmd_tx, data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
610 let (_time_tx, time_evt_rx) = tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
611 let (_exec_evt_tx, exec_evt_rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
612 let (_exec_cmd_tx, exec_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
613 let (signal_tx, signal_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
614
615 let mut runner = create_test_runner(
616 time_evt_rx,
617 data_evt_rx,
618 data_cmd_rx,
619 exec_evt_rx,
620 exec_cmd_rx,
621 signal_rx,
622 signal_tx.clone(),
623 );
624
625 let runner_handle = tokio::spawn(async move {
627 runner.run().await;
628 });
629
630 signal_tx.send(()).unwrap();
632
633 let result = tokio::time::timeout(Duration::from_millis(100), runner_handle).await;
635 assert!(result.is_ok(), "Runner should stop on signal");
636 }
637
638 #[tokio::test]
639 async fn test_runner_closes_on_channel_drop() {
640 let (data_tx, data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
641 let (_cmd_tx, data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
642 let (_time_tx, time_evt_rx) = tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
643 let (_exec_evt_tx, exec_evt_rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
644 let (_exec_cmd_tx, exec_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
645 let (signal_tx, signal_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
646
647 let mut runner = create_test_runner(
648 time_evt_rx,
649 data_evt_rx,
650 data_cmd_rx,
651 exec_evt_rx,
652 exec_cmd_rx,
653 signal_rx,
654 signal_tx.clone(),
655 );
656
657 let runner_handle = tokio::spawn(async move {
659 runner.run().await;
660 });
661
662 drop(data_tx);
663
664 tokio::task::yield_now().await;
666 signal_tx.send(()).ok();
667
668 let result = tokio::time::timeout(Duration::from_millis(200), runner_handle).await;
670 assert!(
671 result.is_ok(),
672 "Runner should stop when channels close or on signal"
673 );
674 }
675
676 #[tokio::test]
677 async fn test_concurrent_event_sending() {
678 let (data_evt_tx, data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
679 let (_data_cmd_tx, data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
680 let (_time_evt_tx, time_evt_rx) =
681 tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
682 let (_exec_evt_tx, exec_evt_rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
683 let (_exec_cmd_tx, exec_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
684 let (signal_tx, signal_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
685
686 let mut runner = create_test_runner(
688 time_evt_rx,
689 data_evt_rx,
690 data_cmd_rx,
691 exec_evt_rx,
692 exec_cmd_rx,
693 signal_rx,
694 signal_tx.clone(),
695 );
696
697 let mut handles = vec![];
699
700 for _ in 0..5 {
701 let tx_clone = data_evt_tx.clone();
702
703 let handle = tokio::spawn(async move {
704 for _ in 0..20 {
705 let quote = test_quote();
706 tx_clone.send(DataEvent::Data(Data::Quote(quote))).unwrap();
707 tokio::task::yield_now().await;
708 }
709 });
710 handles.push(handle);
711 }
712
713 let runner_handle = tokio::spawn(async move {
715 runner.run().await;
716 });
717
718 for handle in handles {
720 handle.await.unwrap();
721 }
722
723 tokio::task::yield_now().await;
725 signal_tx.send(()).unwrap();
726
727 let _ = tokio::time::timeout(Duration::from_millis(200), runner_handle).await;
728 }
729
730 #[rstest]
731 #[case(10)]
732 #[case(100)]
733 #[case(1000)]
734 fn test_channel_send_performance(#[case] count: usize) {
735 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
736 let quote = test_quote();
737
738 for _ in 0..count {
740 tx.send(DataEvent::Data(Data::Quote(quote))).unwrap();
741 }
742
743 let mut received = 0;
745 while rx.try_recv().is_ok() {
746 received += 1;
747 }
748
749 assert_eq!(received, count);
750 }
751
752 #[rstest]
753 fn test_async_trading_command_sender_creation() {
754 let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
755 let sender = AsyncTradingCommandSender::new(tx);
756 assert!(format!("{sender:?}").contains("AsyncTradingCommandSender"));
757 }
758
759 #[tokio::test]
760 async fn test_async_trading_command_sender_execute() {
761 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
762 let sender = AsyncTradingCommandSender::new(tx);
763
764 let command = TradingCommand::CancelAllOrders(CancelAllOrders::new(
765 TraderId::from("TRADER-001"),
766 None,
767 StrategyId::from("S-001"),
768 InstrumentId::from("EUR/USD.SIM"),
769 OrderSide::Buy,
770 UUID4::new(),
771 UnixNanos::default(),
772 None,
773 None, ));
775
776 sender.execute(command);
777
778 let received = rx.recv().await;
779 assert!(received.is_some());
780 assert!(matches!(
781 received.unwrap(),
782 TradingCommand::CancelAllOrders(_)
783 ));
784 }
785
786 #[tokio::test]
787 async fn test_runner_processes_trading_commands() {
788 let (_data_evt_tx, data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
789 let (_data_cmd_tx, data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
790 let (_time_evt_tx, time_evt_rx) =
791 tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
792 let (_exec_evt_tx, exec_evt_rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
793 let (exec_cmd_tx, exec_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
794 let (signal_tx, signal_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
795
796 let mut runner = create_test_runner(
797 time_evt_rx,
798 data_evt_rx,
799 data_cmd_rx,
800 exec_evt_rx,
801 exec_cmd_rx,
802 signal_rx,
803 signal_tx.clone(),
804 );
805
806 let runner_handle = tokio::spawn(async move {
807 runner.run().await;
808 });
809
810 let command = TradingCommand::CancelAllOrders(CancelAllOrders::new(
811 TraderId::from("TRADER-001"),
812 None,
813 StrategyId::from("S-001"),
814 InstrumentId::from("EUR/USD.SIM"),
815 OrderSide::Buy,
816 UUID4::new(),
817 UnixNanos::default(),
818 None,
819 None, ));
821 exec_cmd_tx.send(command).unwrap();
822
823 tokio::task::yield_now().await;
824 signal_tx.send(()).unwrap();
825
826 let result = tokio::time::timeout(Duration::from_millis(100), runner_handle).await;
827 assert!(result.is_ok(), "Runner should process command and stop");
828 }
829
830 #[tokio::test]
831 async fn test_runner_processes_multiple_trading_commands() {
832 let (_data_evt_tx, data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
833 let (_data_cmd_tx, data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
834 let (_time_evt_tx, time_evt_rx) =
835 tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
836 let (_exec_evt_tx, exec_evt_rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
837 let (exec_cmd_tx, exec_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
838 let (signal_tx, signal_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
839
840 let mut runner = create_test_runner(
841 time_evt_rx,
842 data_evt_rx,
843 data_cmd_rx,
844 exec_evt_rx,
845 exec_cmd_rx,
846 signal_rx,
847 signal_tx.clone(),
848 );
849
850 let runner_handle = tokio::spawn(async move {
851 runner.run().await;
852 });
853
854 for i in 0..10 {
855 let strategy_id = format!("S-{i:03}");
856 let command = TradingCommand::CancelAllOrders(CancelAllOrders::new(
857 TraderId::from("TRADER-001"),
858 None,
859 StrategyId::from(strategy_id.as_str()),
860 InstrumentId::from("EUR/USD.SIM"),
861 OrderSide::Buy,
862 UUID4::new(),
863 UnixNanos::default(),
864 None,
865 None, ));
867 exec_cmd_tx.send(command).unwrap();
868 }
869
870 tokio::task::yield_now().await;
871 signal_tx.send(()).unwrap();
872
873 let result = tokio::time::timeout(Duration::from_millis(100), runner_handle).await;
874 assert!(
875 result.is_ok(),
876 "Runner should process all commands and stop"
877 );
878 }
879
880 #[tokio::test]
881 async fn test_execution_event_order_channel() {
882 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
883
884 let event = OrderSubmittedSpec::builder()
885 .client_order_id(ClientOrderId::from("O-001"))
886 .build();
887
888 tx.send(ExecutionEvent::Order(OrderEventAny::Submitted(event)))
889 .unwrap();
890
891 let received = rx.recv().await.unwrap();
892 match received {
893 ExecutionEvent::Order(OrderEventAny::Submitted(e)) => {
894 assert_eq!(e.client_order_id(), ClientOrderId::from("O-001"));
895 }
896 _ => panic!("Expected OrderSubmitted event"),
897 }
898 }
899
900 #[tokio::test]
901 async fn test_execution_report_order_status_channel() {
902 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
903
904 let report = OrderStatusReport::new(
905 AccountId::from("SIM-001"),
906 InstrumentId::from("EUR/USD.SIM"),
907 Some(ClientOrderId::from("O-001")),
908 VenueOrderId::from("V-001"),
909 OrderSide::Buy,
910 OrderType::Market,
911 TimeInForce::Gtc,
912 OrderStatus::Accepted,
913 Quantity::from(100_000),
914 Quantity::from(100_000),
915 UnixNanos::from(1),
916 UnixNanos::from(2),
917 UnixNanos::from(3),
918 None,
919 );
920
921 tx.send(ExecutionEvent::Report(ExecutionReport::Order(Box::new(
922 report,
923 ))))
924 .unwrap();
925
926 let received = rx.recv().await.unwrap();
927 match received {
928 ExecutionEvent::Report(ExecutionReport::Order(r)) => {
929 assert_eq!(r.venue_order_id.as_str(), "V-001");
930 assert_eq!(r.order_status, OrderStatus::Accepted);
931 }
932 _ => panic!("Expected OrderStatusReport"),
933 }
934 }
935
936 #[tokio::test]
937 async fn test_execution_report_fill() {
938 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
939
940 let report = FillReport::new(
941 AccountId::from("SIM-001"),
942 InstrumentId::from("EUR/USD.SIM"),
943 VenueOrderId::from("V-001"),
944 TradeId::from("T-001"),
945 OrderSide::Buy,
946 Quantity::from(100_000),
947 Price::from("1.10000"),
948 Money::from("10 USD"),
949 LiquiditySide::Taker,
950 Some(ClientOrderId::from("O-001")),
951 None,
952 UnixNanos::from(1),
953 UnixNanos::from(2),
954 None,
955 );
956
957 tx.send(ExecutionEvent::Report(ExecutionReport::Fill(Box::new(
958 report,
959 ))))
960 .unwrap();
961
962 let received = rx.recv().await.unwrap();
963 match received {
964 ExecutionEvent::Report(ExecutionReport::Fill(r)) => {
965 assert_eq!(r.venue_order_id.as_str(), "V-001");
966 assert_eq!(r.trade_id.to_string(), "T-001");
967 }
968 _ => panic!("Expected FillReport"),
969 }
970 }
971
972 #[tokio::test]
973 async fn test_execution_report_position() {
974 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
975
976 let report = PositionStatusReport::new(
977 AccountId::from("SIM-001"),
978 InstrumentId::from("EUR/USD.SIM"),
979 PositionSideSpecified::Long,
980 Quantity::from(100_000),
981 UnixNanos::from(1),
982 UnixNanos::from(2),
983 None,
984 Some(PositionId::from("P-001")),
985 None,
986 );
987
988 tx.send(ExecutionEvent::Report(ExecutionReport::Position(Box::new(
989 report,
990 ))))
991 .unwrap();
992
993 let received = rx.recv().await.unwrap();
994 match received {
995 ExecutionEvent::Report(ExecutionReport::Position(r)) => {
996 assert_eq!(r.venue_position_id.unwrap().as_str(), "P-001");
997 }
998 _ => panic!("Expected PositionStatusReport"),
999 }
1000 }
1001
1002 #[tokio::test]
1003 async fn test_execution_event_account() {
1004 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
1005
1006 let account_state = AccountState::new(
1007 AccountId::from("SIM-001"),
1008 AccountType::Cash,
1009 vec![],
1010 vec![],
1011 true,
1012 UUID4::new(),
1013 UnixNanos::from(1),
1014 UnixNanos::from(2),
1015 None,
1016 );
1017
1018 tx.send(ExecutionEvent::Account(account_state)).unwrap();
1019
1020 let received = rx.recv().await.unwrap();
1021 match received {
1022 ExecutionEvent::Account(r) => {
1023 assert_eq!(r.account_id.as_str(), "SIM-001");
1024 }
1025 _ => panic!("Expected AccountState"),
1026 }
1027 }
1028
1029 #[tokio::test]
1030 async fn test_runner_stop_method() {
1031 let (_data_tx, data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
1032 let (_cmd_tx, data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
1033 let (_time_tx, time_evt_rx) = tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
1034 let (_exec_evt_tx, exec_evt_rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
1035 let (_exec_cmd_tx, exec_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
1036 let (signal_tx, signal_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
1037
1038 let mut runner = create_test_runner(
1039 time_evt_rx,
1040 data_evt_rx,
1041 data_cmd_rx,
1042 exec_evt_rx,
1043 exec_cmd_rx,
1044 signal_rx,
1045 signal_tx.clone(),
1046 );
1047
1048 let runner_handle = tokio::spawn(async move {
1049 runner.run().await;
1050 });
1051
1052 signal_tx.send(()).unwrap();
1054
1055 let result = tokio::time::timeout(Duration::from_millis(100), runner_handle).await;
1056 assert!(result.is_ok(), "Runner should stop when stop() is called");
1057 }
1058
1059 #[tokio::test]
1060 async fn test_all_event_types_integration() {
1061 let (data_evt_tx, data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
1062 let (data_cmd_tx, data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
1063 let (time_evt_tx, time_evt_rx) = tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
1064 let (exec_evt_tx, exec_evt_rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
1065 let (_exec_cmd_tx, exec_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
1066 let (signal_tx, signal_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
1067
1068 let mut runner = create_test_runner(
1069 time_evt_rx,
1070 data_evt_rx,
1071 data_cmd_rx,
1072 exec_evt_rx,
1073 exec_cmd_rx,
1074 signal_rx,
1075 signal_tx.clone(),
1076 );
1077
1078 let runner_handle = tokio::spawn(async move {
1079 runner.run().await;
1080 });
1081
1082 let quote = test_quote();
1084 data_evt_tx
1085 .send(DataEvent::Data(Data::Quote(quote)))
1086 .unwrap();
1087
1088 let command = DataCommand::Subscribe(SubscribeCommand::Data(SubscribeCustomData {
1090 client_id: Some(ClientId::from("TEST")),
1091 venue: None,
1092 data_type: DataType::new("QuoteTick", None, None),
1093 command_id: UUID4::new(),
1094 ts_init: UnixNanos::default(),
1095 correlation_id: None,
1096 params: None,
1097 }));
1098 data_cmd_tx.send(command).unwrap();
1099
1100 let event = TimeEvent::new(
1102 Ustr::from("test"),
1103 UUID4::new(),
1104 UnixNanos::from(1),
1105 UnixNanos::from(2),
1106 );
1107 let callback = TimeEventCallback::from(|_: TimeEvent| {});
1108 let handler = TimeEventHandler::new(event, callback);
1109 time_evt_tx.send(handler).unwrap();
1110
1111 let order_event = OrderSubmittedSpec::builder()
1113 .client_order_id(ClientOrderId::from("O-001"))
1114 .build();
1115 exec_evt_tx
1116 .send(ExecutionEvent::Order(OrderEventAny::Submitted(order_event)))
1117 .unwrap();
1118
1119 let order_status = OrderStatusReport::new(
1121 AccountId::from("SIM-001"),
1122 InstrumentId::from("EUR/USD.SIM"),
1123 Some(ClientOrderId::from("O-001")),
1124 VenueOrderId::from("V-001"),
1125 OrderSide::Buy,
1126 OrderType::Market,
1127 TimeInForce::Gtc,
1128 OrderStatus::Accepted,
1129 Quantity::from(100_000),
1130 Quantity::from(100_000),
1131 UnixNanos::from(1),
1132 UnixNanos::from(2),
1133 UnixNanos::from(3),
1134 None,
1135 );
1136 exec_evt_tx
1137 .send(ExecutionEvent::Report(ExecutionReport::Order(Box::new(
1138 order_status,
1139 ))))
1140 .unwrap();
1141
1142 let fill = FillReport::new(
1144 AccountId::from("SIM-001"),
1145 InstrumentId::from("EUR/USD.SIM"),
1146 VenueOrderId::from("V-001"),
1147 TradeId::from("T-001"),
1148 OrderSide::Buy,
1149 Quantity::from(100_000),
1150 Price::from("1.10000"),
1151 Money::from("10 USD"),
1152 LiquiditySide::Taker,
1153 Some(ClientOrderId::from("O-001")),
1154 None,
1155 UnixNanos::from(1),
1156 UnixNanos::from(2),
1157 None,
1158 );
1159 exec_evt_tx
1160 .send(ExecutionEvent::Report(ExecutionReport::Fill(Box::new(
1161 fill,
1162 ))))
1163 .unwrap();
1164
1165 let position = PositionStatusReport::new(
1167 AccountId::from("SIM-001"),
1168 InstrumentId::from("EUR/USD.SIM"),
1169 PositionSideSpecified::Long,
1170 Quantity::from(100_000),
1171 UnixNanos::from(1),
1172 UnixNanos::from(2),
1173 None,
1174 Some(PositionId::from("P-001")),
1175 None,
1176 );
1177 exec_evt_tx
1178 .send(ExecutionEvent::Report(ExecutionReport::Position(Box::new(
1179 position,
1180 ))))
1181 .unwrap();
1182
1183 let account_state = AccountState::new(
1185 AccountId::from("SIM-001"),
1186 AccountType::Cash,
1187 vec![],
1188 vec![],
1189 true,
1190 UUID4::new(),
1191 UnixNanos::from(1),
1192 UnixNanos::from(2),
1193 None,
1194 );
1195 exec_evt_tx
1196 .send(ExecutionEvent::Account(account_state))
1197 .unwrap();
1198
1199 tokio::task::yield_now().await;
1201 signal_tx.send(()).unwrap();
1202
1203 let result = tokio::time::timeout(Duration::from_millis(200), runner_handle).await;
1204 assert!(
1205 result.is_ok(),
1206 "Runner should process all event types and stop cleanly"
1207 );
1208 }
1209
1210 #[tokio::test]
1211 async fn test_runner_handle_stops_runner() {
1212 let (_data_tx, data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
1213 let (_cmd_tx, data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
1214 let (_time_tx, time_evt_rx) = tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
1215 let (_exec_evt_tx, exec_evt_rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
1216 let (_exec_cmd_tx, exec_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
1217 let (signal_tx, signal_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
1218
1219 let mut runner = create_test_runner(
1220 time_evt_rx,
1221 data_evt_rx,
1222 data_cmd_rx,
1223 exec_evt_rx,
1224 exec_cmd_rx,
1225 signal_rx,
1226 signal_tx.clone(),
1227 );
1228
1229 let handle = runner.handle();
1231
1232 let runner_handle = tokio::spawn(async move {
1233 runner.run().await;
1234 });
1235
1236 handle.stop();
1238
1239 let result = tokio::time::timeout(Duration::from_millis(100), runner_handle).await;
1240 assert!(result.is_ok(), "Runner should stop via handle");
1241 }
1242
1243 #[tokio::test]
1244 async fn test_runner_handle_is_cloneable() {
1245 let (signal_tx, _signal_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
1246 let handle = AsyncRunnerHandle { signal_tx };
1247
1248 let handle2 = handle.clone();
1249
1250 assert!(handle.signal_tx.send(()).is_ok());
1252 assert!(handle2.signal_tx.send(()).is_ok());
1253 }
1254
1255 #[tokio::test]
1256 async fn test_runner_processes_events_before_stop() {
1257 let (data_evt_tx, data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
1258 let (_cmd_tx, data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
1259 let (_time_tx, time_evt_rx) = tokio::sync::mpsc::unbounded_channel::<TimeEventHandler>();
1260 let (_exec_evt_tx, exec_evt_rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
1261 let (_exec_cmd_tx, exec_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<TradingCommand>();
1262 let (signal_tx, signal_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
1263
1264 let mut runner = create_test_runner(
1265 time_evt_rx,
1266 data_evt_rx,
1267 data_cmd_rx,
1268 exec_evt_rx,
1269 exec_cmd_rx,
1270 signal_rx,
1271 signal_tx.clone(),
1272 );
1273
1274 let handle = runner.handle();
1275
1276 for _ in 0..10 {
1278 let quote = test_quote();
1279 data_evt_tx
1280 .send(DataEvent::Data(Data::Quote(quote)))
1281 .unwrap();
1282 }
1283
1284 let runner_handle = tokio::spawn(async move {
1285 runner.run().await;
1286 });
1287
1288 tokio::task::yield_now().await;
1290 handle.stop();
1291
1292 let result = tokio::time::timeout(Duration::from_millis(200), runner_handle).await;
1293 assert!(result.is_ok(), "Runner should process events and stop");
1294 }
1295
1296 #[rstest]
1297 fn test_new_does_not_bind_tls() {
1298 std::thread::spawn(|| {
1299 let _runner = AsyncRunner::new();
1300 assert!(try_get_time_event_sender().is_none());
1301 assert!(try_get_trading_cmd_sender().is_none());
1302 })
1303 .join()
1304 .unwrap();
1305 }
1306
1307 #[rstest]
1308 fn test_bind_senders_routes_to_runner_channels() {
1309 std::thread::spawn(|| {
1310 let mut runner = AsyncRunner::new();
1311 runner.bind_senders();
1312
1313 get_data_cmd_sender().execute(DataCommand::Subscribe(SubscribeCommand::Data(
1314 SubscribeCustomData {
1315 client_id: Some(ClientId::from("TEST")),
1316 venue: None,
1317 data_type: DataType::new("test", None, None),
1318 command_id: UUID4::new(),
1319 ts_init: UnixNanos::default(),
1320 correlation_id: None,
1321 params: None,
1322 },
1323 )));
1324 assert!(runner.channels.data_cmd_rx.try_recv().is_ok());
1325
1326 get_trading_cmd_sender().execute(TradingCommand::CancelAllOrders(
1327 CancelAllOrders::new(
1328 TraderId::from("TRADER-001"),
1329 None,
1330 StrategyId::from("S-001"),
1331 InstrumentId::from("EUR/USD.SIM"),
1332 OrderSide::Buy,
1333 UUID4::new(),
1334 UnixNanos::default(),
1335 None,
1336 None, ),
1338 ));
1339 assert!(runner.channels.exec_cmd_rx.try_recv().is_ok());
1340
1341 let event = TimeEvent::new(
1342 Ustr::from("test"),
1343 UUID4::new(),
1344 UnixNanos::from(1),
1345 UnixNanos::from(2),
1346 );
1347 let callback = TimeEventCallback::from(|_: TimeEvent| {});
1348 get_time_event_sender().send(TimeEventHandler::new(event, callback));
1349 assert!(runner.channels.time_evt_rx.try_recv().is_ok());
1350
1351 get_data_event_sender()
1352 .send(DataEvent::Data(Data::Quote(test_quote())))
1353 .unwrap();
1354 assert!(runner.channels.data_evt_rx.try_recv().is_ok());
1355
1356 let account = AccountState::new(
1357 AccountId::from("SIM-001"),
1358 AccountType::Cash,
1359 vec![],
1360 vec![],
1361 true,
1362 UUID4::new(),
1363 UnixNanos::from(1),
1364 UnixNanos::from(2),
1365 None,
1366 );
1367 get_exec_event_sender()
1368 .send(ExecutionEvent::Account(account))
1369 .unwrap();
1370 assert!(runner.channels.exec_evt_rx.try_recv().is_ok());
1371 })
1372 .join()
1373 .unwrap();
1374 }
1375
1376 #[rstest]
1377 fn test_bind_senders_reclaims_tls_from_previous_runner() {
1378 std::thread::spawn(|| {
1379 let mut runner1 = AsyncRunner::new();
1380 runner1.bind_senders();
1381
1382 let mut runner2 = AsyncRunner::new();
1383 runner2.bind_senders();
1384
1385 get_data_cmd_sender().execute(DataCommand::Subscribe(SubscribeCommand::Data(
1386 SubscribeCustomData {
1387 client_id: Some(ClientId::from("TEST")),
1388 venue: None,
1389 data_type: DataType::new("test", None, None),
1390 command_id: UUID4::new(),
1391 ts_init: UnixNanos::default(),
1392 correlation_id: None,
1393 params: None,
1394 },
1395 )));
1396
1397 assert!(runner2.channels.data_cmd_rx.try_recv().is_ok());
1398 assert!(runner1.channels.data_cmd_rx.try_recv().is_err());
1399 })
1400 .join()
1401 .unwrap();
1402 }
1403
1404 #[tokio::test]
1405 async fn test_execution_event_order_submitted_batch_channel() {
1406 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
1407
1408 let events = vec![
1409 OrderSubmittedSpec::builder()
1410 .client_order_id(ClientOrderId::from("O-001"))
1411 .build(),
1412 OrderSubmittedSpec::builder()
1413 .client_order_id(ClientOrderId::from("O-002"))
1414 .build(),
1415 ];
1416
1417 let batch = OrderSubmittedBatch::new(events);
1418 tx.send(ExecutionEvent::OrderSubmittedBatch(batch)).unwrap();
1419
1420 let received = rx.recv().await.unwrap();
1421 match received {
1422 ExecutionEvent::OrderSubmittedBatch(b) => {
1423 assert_eq!(b.len(), 2);
1424 assert_eq!(b.events[0].client_order_id, ClientOrderId::from("O-001"));
1425 assert_eq!(b.events[1].client_order_id, ClientOrderId::from("O-002"));
1426 }
1427 _ => panic!("Expected OrderSubmittedBatch event"),
1428 }
1429 }
1430
1431 #[tokio::test]
1432 async fn test_execution_event_order_accepted_batch_channel() {
1433 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
1434
1435 let events = vec![
1436 OrderAcceptedSpec::builder()
1437 .client_order_id(ClientOrderId::from("O-001"))
1438 .build(),
1439 OrderAcceptedSpec::builder()
1440 .client_order_id(ClientOrderId::from("O-002"))
1441 .build(),
1442 ];
1443
1444 let batch = OrderAcceptedBatch::new(events);
1445 tx.send(ExecutionEvent::OrderAcceptedBatch(batch)).unwrap();
1446
1447 let received = rx.recv().await.unwrap();
1448 match received {
1449 ExecutionEvent::OrderAcceptedBatch(b) => {
1450 assert_eq!(b.len(), 2);
1451 assert_eq!(b.events[0].client_order_id, ClientOrderId::from("O-001"));
1452 assert_eq!(b.events[1].client_order_id, ClientOrderId::from("O-002"));
1453 }
1454 _ => panic!("Expected OrderAcceptedBatch event"),
1455 }
1456 }
1457
1458 #[tokio::test]
1459 async fn test_execution_event_order_canceled_batch_channel() {
1460 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
1461
1462 let events = vec![
1463 OrderCanceledSpec::builder()
1464 .client_order_id(ClientOrderId::from("O-001"))
1465 .build(),
1466 OrderCanceledSpec::builder()
1467 .client_order_id(ClientOrderId::from("O-002"))
1468 .build(),
1469 ];
1470
1471 let batch = OrderCanceledBatch::new(events);
1472 tx.send(ExecutionEvent::OrderCanceledBatch(batch)).unwrap();
1473
1474 let received = rx.recv().await.unwrap();
1475 match received {
1476 ExecutionEvent::OrderCanceledBatch(b) => {
1477 assert_eq!(b.len(), 2);
1478 assert_eq!(b.events[0].client_order_id, ClientOrderId::from("O-001"));
1479 assert_eq!(b.events[1].client_order_id, ClientOrderId::from("O-002"));
1480 }
1481 _ => panic!("Expected OrderCanceledBatch event"),
1482 }
1483 }
1484}