1use crate::{
2 UnindexedAccountEvent,
3 client::mock::MockExecutionConfig,
4 exchange::mock::request::{MockExchangeRequest, MockExchangeRequestKind},
5 order::{Order, state::UnindexedOrderState},
6};
7use chrono::{DateTime, TimeDelta, Utc};
8use fnv::FnvHashMap;
9use futures::stream::BoxStream;
10use rustrade_instrument::{
11 asset::name::AssetNameExchange,
12 exchange::ExchangeId,
13 instrument::{Instrument, name::InstrumentNameExchange},
14};
15use std::fmt::Debug;
16use tokio::sync::{broadcast, mpsc, oneshot};
17use tokio_stream::{StreamExt, wrappers::BroadcastStream};
18use tracing::{error, info};
19
20pub mod account;
21pub mod orders;
22pub mod request;
23pub mod venue;
24
25pub use venue::{CancelOutcome, OpenOutcome, SimulatedVenue, VenueInstrumentMarket, VenueOutcome};
26
27#[derive(Debug)]
46pub struct MockExchange {
47 pub venue: SimulatedVenue,
49 pub latency_ms: u64,
51 pub request_rx: mpsc::UnboundedReceiver<MockExchangeRequest>,
52 pub event_tx: broadcast::Sender<UnindexedAccountEvent>,
53}
54
55impl MockExchange {
56 pub fn new(
57 config: MockExecutionConfig,
58 request_rx: mpsc::UnboundedReceiver<MockExchangeRequest>,
59 event_tx: broadcast::Sender<UnindexedAccountEvent>,
60 instruments: FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>>,
61 ) -> Self {
62 Self {
63 venue: SimulatedVenue::new(&config, instruments),
64 latency_ms: config.latency_ms,
65 request_rx,
66 event_tx,
67 }
68 }
69
70 pub async fn run(mut self) {
77 let (emit_tx, emit_rx) = mpsc::unbounded_channel();
78 let emitter = tokio::spawn(Self::emit_queued_output(
79 emit_rx,
80 self.event_tx.clone(),
81 self.venue.exchange,
82 ));
83
84 while let Some(request) = self.request_rx.recv().await {
85 let expiries = self.advance_venue_time(request.time_request);
89 self.queue_emission(&emit_tx, expiries, None);
90
91 match request.kind {
92 MockExchangeRequestKind::FetchAccountSnapshot { response_tx } => {
93 let snapshot = self.venue.account_snapshot();
94 self.respond_with_latency(response_tx, snapshot);
95 }
96 MockExchangeRequestKind::FetchBalances {
97 response_tx,
98 assets,
99 } => {
100 let balances = self.venue.balances(&assets);
101 self.respond_with_latency(response_tx, balances);
102 }
103 MockExchangeRequestKind::FetchOrdersOpen {
104 response_tx,
105 instruments,
106 } => {
107 let orders_open = self.venue.orders_open(&instruments);
108 self.respond_with_latency(response_tx, orders_open);
109 }
110 MockExchangeRequestKind::FetchOrdersEnded {
111 response_tx,
112 orders,
113 } => {
114 let orders_ended = self.venue.orders_ended(&orders);
115 self.respond_with_latency(response_tx, orders_ended);
116 }
117 MockExchangeRequestKind::FetchTrades {
118 response_tx,
119 start,
120 end,
121 } => {
122 let mut trades = self.venue.trades(start);
123 trades.retain(|trade| trade.time_exchange <= end);
124 self.respond_with_latency(response_tx, trades);
125 }
126 MockExchangeRequestKind::CancelOrder {
127 response_tx,
128 request,
129 } => {
130 let outcome = self.venue.cancel_order(request);
136 self.queue_emission(&emit_tx, outcome.events, None);
137 let _ = response_tx.send(outcome.response);
138 }
139 MockExchangeRequestKind::OpenOrder {
140 response_tx,
141 request,
142 } => {
143 let outcome = self.venue.open_order(request);
146 self.respond_open_with_latency(&emit_tx, response_tx, outcome);
147 }
148 }
149 }
150
151 drop(emit_tx);
155 if let Err(error) = emitter.await {
156 error!(
157 exchange = %self.venue.exchange,
158 %error,
159 "MockExchange emitter task did not shut down cleanly; queued fills may be lost"
160 );
161 }
162
163 info!(exchange = %self.venue.exchange, "MockExchange shutting down");
164 }
165
166 #[must_use]
171 fn advance_venue_time(&mut self, time_request: DateTime<Utc>) -> Vec<UnindexedAccountEvent> {
172 let client_to_exchange_latency = self.latency_ms / 2;
173
174 let time_exchange = time_request
175 .checked_add_signed(TimeDelta::milliseconds(client_to_exchange_latency as i64))
176 .unwrap_or(time_request);
177
178 self.venue.advance_time(time_exchange)
179 }
180
181 fn respond_with_latency<Response>(
186 &self,
187 response_tx: oneshot::Sender<Response>,
188 response: Response,
189 ) where
190 Response: Send + 'static,
191 {
192 let exchange = self.venue.exchange;
193 let latency = std::time::Duration::from_millis(self.latency_ms);
194
195 tokio::spawn(async move {
196 tokio::time::sleep(latency).await;
197 if response_tx.send(response).is_err() {
198 error!(
199 %exchange,
200 kind = std::any::type_name::<Response>(),
201 "MockExchange failed to send oneshot response to client"
202 );
203 }
204 });
205 }
206
207 fn respond_open_with_latency(
223 &self,
224 emit_tx: &mpsc::UnboundedSender<PendingEmission>,
225 response_tx: oneshot::Sender<
226 Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
227 >,
228 outcome: OpenOutcome,
229 ) {
230 self.queue_emission(
231 emit_tx,
232 outcome.events,
233 Some(PendingOpenResponse {
234 response_tx,
235 response: outcome.response,
236 }),
237 );
238 }
239
240 fn queue_emission(
248 &self,
249 emit_tx: &mpsc::UnboundedSender<PendingEmission>,
250 events: Vec<UnindexedAccountEvent>,
251 response: Option<PendingOpenResponse>,
252 ) {
253 if events.is_empty() && response.is_none() {
254 return;
255 }
256
257 let ready_at =
261 tokio::time::Instant::now() + std::time::Duration::from_millis(self.latency_ms);
262
263 if emit_tx
266 .send(PendingEmission {
267 ready_at,
268 events,
269 response,
270 })
271 .is_err()
272 {
273 error!(
274 exchange = %self.venue.exchange,
275 "MockExchange could not queue venue output: the emitter has stopped"
276 );
277 }
278 }
279
280 async fn emit_queued_output(
287 mut emit_rx: mpsc::UnboundedReceiver<PendingEmission>,
288 event_tx: broadcast::Sender<UnindexedAccountEvent>,
289 exchange: ExchangeId,
290 ) {
291 while let Some(emission) = emit_rx.recv().await {
292 tokio::time::sleep_until(emission.ready_at).await;
293
294 for event in emission.events {
296 if event_tx.send(event).is_err() {
297 error!(
298 %exchange,
299 "MockExchange failed to send AccountEvent notification to client"
300 );
301 }
302 }
303
304 let Some(PendingOpenResponse {
306 response_tx,
307 response,
308 }) = emission.response
309 else {
310 continue;
311 };
312
313 if response_tx.send(response).is_err() {
314 error!(
315 %exchange,
316 kind = "OrderResponseOpen",
317 "MockExchange failed to send oneshot response to client"
318 );
319 }
320 }
321 }
322
323 pub fn account_stream(&self) -> BoxStream<'static, UnindexedAccountEvent> {
324 futures::StreamExt::boxed(BroadcastStream::new(self.event_tx.subscribe()).map_while(
325 |result| match result {
326 Ok(event) => Some(event),
327 Err(error) => {
328 error!(
329 ?error,
330 "MockExchange Broadcast AccountStream lagged - terminating"
331 );
332 None
333 }
334 },
335 ))
336 }
337}
338
339#[derive(Debug)]
350struct PendingEmission {
351 ready_at: tokio::time::Instant,
353 events: Vec<UnindexedAccountEvent>,
355 response: Option<PendingOpenResponse>,
358}
359
360#[derive(Debug)]
362struct PendingOpenResponse {
363 response_tx: oneshot::Sender<Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>>,
364 response: Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
365}
366
367#[cfg(test)]
373#[allow(clippy::unwrap_used, clippy::expect_used)] pub(crate) mod fixtures {
375 use crate::{
376 InstrumentAccountSnapshot, UnindexedAccountSnapshot,
377 balance::{AssetBalance, Balance},
378 client::mock::MockExecutionConfig,
379 fee::{FeeModelConfig, SpotFeeAsset},
380 fill::SimFillConfig,
381 market::MarketSnapshot,
382 order::{
383 Order, OrderEvent, OrderKey, OrderKind, TimeInForce, UnindexedOrder,
384 id::{ClientOrderId, OrderId, StrategyId, VenueOrderId},
385 request::{OrderRequestOpen, RequestOpen},
386 state::{Open, OrderState},
387 },
388 position::PositionReport,
389 };
390 use chrono::{DateTime, Utc};
391 use fnv::FnvHashMap;
392 use rust_decimal::Decimal;
393 use rustrade_instrument::{
394 Side, Underlying,
395 asset::name::AssetNameExchange,
396 exchange::ExchangeId,
397 instrument::{
398 Instrument,
399 kind::InstrumentKind,
400 name::{InstrumentNameExchange, InstrumentNameInternal},
401 quote::InstrumentQuoteAsset,
402 },
403 };
404
405 pub(super) const EXCHANGE: ExchangeId = ExchangeId::BinanceSpot;
406
407 pub(super) fn d(s: &str) -> Decimal {
408 s.parse().unwrap()
409 }
410
411 pub(super) fn base() -> AssetNameExchange {
412 AssetNameExchange::new("BTC")
413 }
414
415 pub(super) fn quote() -> AssetNameExchange {
416 AssetNameExchange::new("USDT")
417 }
418
419 pub(super) fn instrument_name() -> InstrumentNameExchange {
420 InstrumentNameExchange::new("BTCUSDT")
421 }
422
423 pub(super) fn funded(
425 asset: AssetNameExchange,
426 amount: Decimal,
427 ) -> AssetBalance<AssetNameExchange> {
428 AssetBalance {
429 asset,
430 balance: Balance::new(amount, amount),
431 time_exchange: Utc::now(),
432 }
433 }
434
435 pub(super) fn config_from_balances(
437 balances: Vec<AssetBalance<AssetNameExchange>>,
438 fee_model: FeeModelConfig,
439 ) -> MockExecutionConfig {
440 config_from_balances_with_fill(balances, fee_model, SimFillConfig::default())
441 }
442
443 pub(super) fn config_from_balances_with_fill(
444 balances: Vec<AssetBalance<AssetNameExchange>>,
445 fee_model: FeeModelConfig,
446 fill_model: SimFillConfig,
447 ) -> MockExecutionConfig {
448 MockExecutionConfig::new(
449 EXCHANGE,
450 UnindexedAccountSnapshot {
451 exchange: EXCHANGE,
452 balances,
453 instruments: vec![],
454 },
455 0, fee_model,
457 fill_model,
458 SpotFeeAsset::default(),
459 crate::slippage::SlippageConfig::default(),
460 )
461 }
462
463 pub(super) fn spot_config_holding(
470 btc: &str,
471 usdt: &str,
472 order: UnindexedOrder,
473 ) -> MockExecutionConfig {
474 MockExecutionConfig::new(
475 EXCHANGE,
476 UnindexedAccountSnapshot {
477 exchange: EXCHANGE,
478 balances: vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
479 instruments: vec![InstrumentAccountSnapshot {
480 instrument: instrument_name(),
481 orders: vec![order],
482 orders_complete: true,
483 position: PositionReport::Unreported,
484 isolated: None,
485 }],
486 },
487 0, FeeModelConfig::default(),
489 SimFillConfig::default(),
490 SpotFeeAsset::default(),
491 crate::slippage::SlippageConfig::default(),
492 )
493 }
494
495 pub(super) fn seeded_gtd(
497 cid: &str,
498 price: &str,
499 opened: DateTime<Utc>,
500 expiry: DateTime<Utc>,
501 ) -> UnindexedOrder {
502 Order {
503 key: OrderKey {
504 exchange: EXCHANGE,
505 instrument: instrument_name(),
506 strategy: StrategyId::new("test"),
507 cid: ClientOrderId::new(cid),
508 },
509 side: Side::Buy,
510 price: Some(d(price)),
511 quantity: d("0.01"),
512 kind: OrderKind::Limit,
513 time_in_force: TimeInForce::GoodTillDate { expiry },
514 state: OrderState::active(Open::new(
515 VenueOrderId::Assigned(OrderId::new(cid)),
516 opened,
517 rust_decimal::Decimal::ZERO,
518 )),
519 }
520 }
521
522 pub(super) fn spot_config(
524 btc: &str,
525 usdt: &str,
526 fee_model: FeeModelConfig,
527 ) -> MockExecutionConfig {
528 config_from_balances(
529 vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
530 fee_model,
531 )
532 }
533
534 pub(super) fn spot_config_with_fill(
535 btc: &str,
536 usdt: &str,
537 fee_model: FeeModelConfig,
538 fill_model: SimFillConfig,
539 ) -> MockExecutionConfig {
540 config_from_balances_with_fill(
541 vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
542 fee_model,
543 fill_model,
544 )
545 }
546
547 pub(super) fn instruments_of(
548 instrument: Instrument<ExchangeId, AssetNameExchange>,
549 ) -> FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
550 let mut instruments = FnvHashMap::default();
551 instruments.insert(instrument.name_exchange.clone(), instrument);
552 instruments
553 }
554
555 pub(super) fn spot_instruments()
556 -> FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
557 instruments_of(Instrument {
558 exchange: EXCHANGE,
559 name_internal: InstrumentNameInternal::new("btcusdt"),
560 name_exchange: instrument_name(),
561 underlying: Underlying {
562 base: base(),
563 quote: quote(),
564 },
565 quote: InstrumentQuoteAsset::UnderlyingQuote,
566 kind: InstrumentKind::Spot,
567 spec: None,
568 data_venue: None,
569 })
570 }
571
572 pub(super) fn request(
578 instrument: InstrumentNameExchange,
579 side: Side,
580 quantity: &str,
581 market: Option<MarketSnapshot>,
582 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
583 OrderEvent {
584 key: OrderKey {
585 exchange: EXCHANGE,
586 instrument,
587 strategy: StrategyId::new("test"),
588 cid: ClientOrderId::new("test-cid"),
589 },
590 state: RequestOpen {
591 side,
592 price: None, quantity: d(quantity),
594 kind: OrderKind::Market,
595 time_in_force: TimeInForce::ImmediateOrCancel,
596 position_id: None,
597 reduce_only: false,
598 market,
599 },
600 }
601 }
602
603 pub(super) fn buy_request(
604 quantity: &str,
605 market: Option<MarketSnapshot>,
606 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
607 request(instrument_name(), Side::Buy, quantity, market)
608 }
609
610 pub(super) fn sell_request(
611 quantity: &str,
612 market: Option<MarketSnapshot>,
613 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
614 request(instrument_name(), Side::Sell, quantity, market)
615 }
616
617 pub(super) fn gtc() -> TimeInForce {
619 TimeInForce::GoodUntilCancelled { post_only: false }
620 }
621
622 pub(super) fn limit_request(
627 cid: &str,
628 side: Side,
629 quantity: &str,
630 price: &str,
631 time_in_force: TimeInForce,
632 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
633 OrderEvent {
634 key: OrderKey {
635 exchange: EXCHANGE,
636 instrument: instrument_name(),
637 strategy: StrategyId::new("test"),
638 cid: ClientOrderId::new(cid),
639 },
640 state: RequestOpen {
641 side,
642 price: Some(d(price)),
643 quantity: d(quantity),
644 kind: OrderKind::Limit,
645 time_in_force,
646 position_id: None,
647 reduce_only: false,
648 market: None,
649 },
650 }
651 }
652
653 pub(super) fn book(best_bid: &str, best_ask: &str) -> MarketSnapshot {
655 MarketSnapshot {
656 best_bid: Some(d(best_bid)),
657 best_ask: Some(d(best_ask)),
658 last_price: None,
659 }
660 }
661
662 pub(super) fn traded(last_price: &str) -> MarketSnapshot {
664 MarketSnapshot {
665 best_bid: None,
666 best_ask: None,
667 last_price: Some(d(last_price)),
668 }
669 }
670
671 pub(super) fn market_prices(price: &str) -> Option<MarketSnapshot> {
673 let p = Some(d(price));
674 Some(MarketSnapshot {
675 best_bid: p,
676 best_ask: p,
677 last_price: p,
678 })
679 }
680}
681
682#[cfg(test)]
683#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
685 use super::{fixtures::*, *};
686 use crate::{
687 AccountEventKind,
688 fee::FeeModelConfig,
689 order::{
690 OrderEvent, OrderKey,
691 id::ClientOrderId,
692 request::RequestCancel,
693 state::{InactiveOrderState, OrderState},
694 },
695 };
696 use rust_decimal::Decimal;
697
698 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
714 async fn open_order_emissions_are_ordered_across_fills() {
715 const FILLS: usize = 8;
716 const DEBIT_PER_FILL: &str = "500";
717
718 let usdt_start = "10000000";
719
720 let (request_tx, request_rx) = mpsc::unbounded_channel();
721 let (event_tx, mut events) = broadcast::channel(64);
722
723 let exchange = MockExchange::new(
724 spot_config("100", usdt_start, FeeModelConfig::default()),
725 request_rx,
726 event_tx,
727 spot_instruments(),
728 );
729
730 let driver = tokio::spawn(exchange.run());
731
732 let mut responses = Vec::with_capacity(FILLS);
733 for nth in 0..FILLS {
734 let (response_tx, response_rx) = oneshot::channel();
735 let mut request = buy_request("0.01", market_prices("50000"));
736 request.key.cid = ClientOrderId::new(format!("cid-{nth}"));
737
738 request_tx
739 .send(MockExchangeRequest::open_order(
740 Utc::now(),
741 response_tx,
742 request,
743 ))
744 .unwrap();
745 responses.push(response_rx);
746 }
747
748 drop(request_tx);
750 driver.await.unwrap();
751
752 for response in responses {
753 let response = response.await.expect("every open is answered");
754 assert!(
755 matches!(
756 response.state,
757 OrderState::Active(_) | OrderState::Inactive(_)
758 ),
759 "the account is funded and the instrument priced, so every open fills: {response:?}"
760 );
761 }
762
763 let balances = std::iter::from_fn(|| events.try_recv().ok())
765 .filter_map(|event| match event.kind {
766 AccountEventKind::BalanceSnapshot(snapshot) if snapshot.0.asset == quote() => {
767 Some(snapshot.0.balance.total)
768 }
769 _ => None,
770 })
771 .collect::<Vec<_>>();
772
773 let expected = (1..=FILLS)
775 .map(|nth| d(usdt_start) - d(DEBIT_PER_FILL) * Decimal::from(nth))
776 .collect::<Vec<_>>();
777
778 assert_eq!(
779 balances, expected,
780 "balance snapshots must reach the client in the order the venue booked them"
781 );
782 }
783
784 #[tokio::test]
786 async fn the_driver_fills_at_the_configured_slippage() {
787 let (request_tx, request_rx) = mpsc::unbounded_channel();
788 let (event_tx, mut events) = broadcast::channel(16);
789 let config = MockExecutionConfig {
790 slippage: crate::slippage::SlippageConfig::fixed(d("0.01")).unwrap(),
791 ..spot_config("100", "10000000", FeeModelConfig::default())
792 };
793 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
794 let driver = tokio::spawn(exchange.run());
795
796 let (response_tx, response_rx) = oneshot::channel();
797 request_tx
798 .send(MockExchangeRequest::open_order(
799 Utc::now(),
800 response_tx,
801 buy_request("0.01", market_prices("50000")),
802 ))
803 .unwrap();
804 response_rx.await.expect("the open is answered");
805 drop(request_tx);
806 driver.await.unwrap();
807
808 let prices = std::iter::from_fn(|| events.try_recv().ok())
809 .filter_map(|event| match event.kind {
810 AccountEventKind::Trade(trade) => Some(trade.price),
811 _ => None,
812 })
813 .collect::<Vec<_>>();
814 assert_eq!(prices, vec![d("50500")], "50,000 bought 1% above");
815 }
816
817 #[tokio::test(start_paused = true)]
832 async fn a_swept_expiry_is_emitted_behind_the_fill_booked_before_it() {
833 let opened = Utc::now();
834 let expiry = opened + chrono::TimeDelta::seconds(30);
835
836 let (request_tx, request_rx) = mpsc::unbounded_channel();
837 let (event_tx, mut events) = broadcast::channel(64);
838
839 let mut config =
840 spot_config_holding("100", "10000000", seeded_gtd("gtd", "1", opened, expiry));
841 config.latency_ms = 50;
843
844 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
845 let driver = tokio::spawn(exchange.run());
846
847 let (fill_tx, fill_rx) = oneshot::channel();
849 request_tx
850 .send(MockExchangeRequest::open_order(
851 opened,
852 fill_tx,
853 buy_request("0.01", market_prices("50000")),
854 ))
855 .unwrap();
856
857 let (snapshot_tx, snapshot_rx) = oneshot::channel();
860 request_tx
861 .send(MockExchangeRequest::fetch_account_snapshot(
862 expiry + chrono::TimeDelta::seconds(1),
863 snapshot_tx,
864 ))
865 .unwrap();
866
867 drop(request_tx);
868 driver.await.unwrap();
869 fill_rx.await.expect("the open is answered");
870 let _ = snapshot_rx.await;
871
872 let kinds = std::iter::from_fn(|| events.try_recv().ok())
873 .map(|event| match event.kind {
874 AccountEventKind::BalanceSnapshot(_) => "balance",
875 AccountEventKind::Trade(_) => "trade",
876 AccountEventKind::OrderSnapshot(_) => "order",
877 _ => "other",
878 })
879 .collect::<Vec<_>>();
880
881 assert_eq!(
882 kinds,
883 ["balance", "balance", "trade", "order"],
884 "the fill's events must all precede the sweep, which for a seeded order — held with \
885 no reservation — owes only its terminal snapshot"
886 );
887 }
888
889 #[tokio::test]
890 async fn a_cancel_request_is_answered_rather_than_dropped() {
891 let (request_tx, request_rx) = mpsc::unbounded_channel();
892 let (event_tx, _events) = broadcast::channel(16);
893
894 let exchange = MockExchange::new(
895 spot_config("100", "10000", FeeModelConfig::default()),
896 request_rx,
897 event_tx,
898 spot_instruments(),
899 );
900 let driver = tokio::spawn(exchange.run());
901
902 let (response_tx, response_rx) = oneshot::channel();
903 let open = buy_request("1", market_prices("50000"));
904 request_tx
905 .send(MockExchangeRequest::cancel_order(
906 Utc::now(),
907 response_tx,
908 OrderEvent {
909 key: open.key,
910 state: RequestCancel { id: None },
911 },
912 ))
913 .unwrap();
914
915 let response = response_rx.await.expect("a cancel must be answered");
916 assert!(
917 response.state.is_err(),
918 "this venue rests no orders, so a cancel must be rejected: {response:?}"
919 );
920
921 drop(request_tx);
922 driver.await.unwrap();
923 }
924
925 #[tokio::test]
928 async fn an_order_state_lookup_is_answered_from_the_ledger() {
929 use crate::client::{OrderStatusClient, mock::MockExecution};
930
931 let (request_tx, request_rx) = mpsc::unbounded_channel();
932 let (event_tx, event_rx) = broadcast::channel(16);
933 let exchange = MockExchange::new(
934 spot_config("100", "10000000", FeeModelConfig::default()),
935 request_rx,
936 event_tx,
937 spot_instruments(),
938 );
939 let driver = tokio::spawn(exchange.run());
940
941 let open = buy_request("1", market_prices("50000"));
942 let (response_tx, response_rx) = oneshot::channel();
943 request_tx
944 .send(MockExchangeRequest::open_order(
945 Utc::now(),
946 response_tx,
947 open.clone(),
948 ))
949 .unwrap();
950 let opened = response_rx.await.expect("an open must be answered");
951 assert!(matches!(
952 opened.state,
953 OrderState::Inactive(InactiveOrderState::FullyFilled(_))
954 ));
955
956 let client = MockExecution {
957 mocked_exchange: EXCHANGE,
958 clock: Utc::now,
959 request_tx: request_tx.clone(),
960 event_rx,
961 };
962 let unknown = OrderKey {
963 cid: ClientOrderId::new("never-opened"),
964 ..open.key.clone()
965 };
966 let ended = client
967 .fetch_ended_orders(&[open.key.clone(), unknown])
968 .await
969 .expect("the venue is running");
970
971 let [order] = ended.as_slice() else {
972 panic!("only the filled order is reported: {ended:?}");
973 };
974 assert_eq!(order.key, open.key);
975 assert!(matches!(order.state, InactiveOrderState::FullyFilled(_)));
976
977 drop(client);
978 drop(request_tx);
979 driver.await.unwrap();
980 }
981}