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 crate::fill::MarketOrderTiming::default(),
461 crate::fill::StaleMarketPolicy::default(),
462 None,
463 None,
464 )
465 }
466
467 pub(super) fn spot_config_holding(
474 btc: &str,
475 usdt: &str,
476 order: UnindexedOrder,
477 ) -> MockExecutionConfig {
478 MockExecutionConfig::new(
479 EXCHANGE,
480 UnindexedAccountSnapshot {
481 exchange: EXCHANGE,
482 balances: vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
483 instruments: vec![InstrumentAccountSnapshot {
484 instrument: instrument_name(),
485 orders: vec![order],
486 orders_complete: true,
487 position: PositionReport::Unreported,
488 isolated: None,
489 }],
490 },
491 0, FeeModelConfig::default(),
493 SimFillConfig::default(),
494 SpotFeeAsset::default(),
495 crate::slippage::SlippageConfig::default(),
496 crate::fill::MarketOrderTiming::default(),
497 crate::fill::StaleMarketPolicy::default(),
498 None,
499 None,
500 )
501 }
502
503 pub(super) fn seeded_gtd(
505 cid: &str,
506 price: &str,
507 opened: DateTime<Utc>,
508 expiry: DateTime<Utc>,
509 ) -> UnindexedOrder {
510 Order {
511 key: OrderKey {
512 exchange: EXCHANGE,
513 instrument: instrument_name(),
514 strategy: StrategyId::new("test"),
515 cid: ClientOrderId::new(cid),
516 },
517 side: Side::Buy,
518 price: Some(d(price)),
519 quantity: d("0.01"),
520 kind: OrderKind::Limit,
521 time_in_force: TimeInForce::GoodTillDate { expiry },
522 state: OrderState::active(Open::new(
523 VenueOrderId::Assigned(OrderId::new(cid)),
524 opened,
525 rust_decimal::Decimal::ZERO,
526 )),
527 }
528 }
529
530 pub(super) fn spot_config(
532 btc: &str,
533 usdt: &str,
534 fee_model: FeeModelConfig,
535 ) -> MockExecutionConfig {
536 config_from_balances(
537 vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
538 fee_model,
539 )
540 }
541
542 pub(super) fn spot_config_with_fill(
543 btc: &str,
544 usdt: &str,
545 fee_model: FeeModelConfig,
546 fill_model: SimFillConfig,
547 ) -> MockExecutionConfig {
548 config_from_balances_with_fill(
549 vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
550 fee_model,
551 fill_model,
552 )
553 }
554
555 pub(super) fn instruments_of(
556 instrument: Instrument<ExchangeId, AssetNameExchange>,
557 ) -> FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
558 let mut instruments = FnvHashMap::default();
559 instruments.insert(instrument.name_exchange.clone(), instrument);
560 instruments
561 }
562
563 pub(super) fn spot_instruments()
564 -> FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
565 instruments_of(Instrument {
566 exchange: EXCHANGE,
567 name_internal: InstrumentNameInternal::new("btcusdt"),
568 name_exchange: instrument_name(),
569 underlying: Underlying {
570 base: base(),
571 quote: quote(),
572 },
573 quote: InstrumentQuoteAsset::UnderlyingQuote,
574 kind: InstrumentKind::Spot,
575 spec: None,
576 data_venue: None,
577 })
578 }
579
580 pub(super) fn request(
586 instrument: InstrumentNameExchange,
587 side: Side,
588 quantity: &str,
589 market: Option<MarketSnapshot>,
590 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
591 OrderEvent {
592 key: OrderKey {
593 exchange: EXCHANGE,
594 instrument,
595 strategy: StrategyId::new("test"),
596 cid: ClientOrderId::new("test-cid"),
597 },
598 state: RequestOpen {
599 side,
600 price: None, quantity: d(quantity),
602 kind: OrderKind::Market,
603 time_in_force: TimeInForce::ImmediateOrCancel,
604 position_id: None,
605 reduce_only: false,
606 market,
607 },
608 }
609 }
610
611 pub(super) fn buy_request(
612 quantity: &str,
613 market: Option<MarketSnapshot>,
614 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
615 request(instrument_name(), Side::Buy, quantity, market)
616 }
617
618 pub(super) fn sell_request(
619 quantity: &str,
620 market: Option<MarketSnapshot>,
621 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
622 request(instrument_name(), Side::Sell, quantity, market)
623 }
624
625 pub(super) fn gtc() -> TimeInForce {
627 TimeInForce::GoodUntilCancelled { post_only: false }
628 }
629
630 pub(super) fn limit_request(
635 cid: &str,
636 side: Side,
637 quantity: &str,
638 price: &str,
639 time_in_force: TimeInForce,
640 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
641 OrderEvent {
642 key: OrderKey {
643 exchange: EXCHANGE,
644 instrument: instrument_name(),
645 strategy: StrategyId::new("test"),
646 cid: ClientOrderId::new(cid),
647 },
648 state: RequestOpen {
649 side,
650 price: Some(d(price)),
651 quantity: d(quantity),
652 kind: OrderKind::Limit,
653 time_in_force,
654 position_id: None,
655 reduce_only: false,
656 market: None,
657 },
658 }
659 }
660
661 pub(super) fn book(best_bid: &str, best_ask: &str) -> MarketSnapshot {
663 MarketSnapshot {
664 best_bid: Some(d(best_bid)),
665 best_ask: Some(d(best_ask)),
666 last_price: None,
667 }
668 }
669
670 pub(super) fn traded(last_price: &str) -> MarketSnapshot {
672 MarketSnapshot {
673 best_bid: None,
674 best_ask: None,
675 last_price: Some(d(last_price)),
676 }
677 }
678
679 pub(super) fn market_prices(price: &str) -> Option<MarketSnapshot> {
681 let p = Some(d(price));
682 Some(MarketSnapshot {
683 best_bid: p,
684 best_ask: p,
685 last_price: p,
686 })
687 }
688}
689
690#[cfg(test)]
691#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
693 use super::{fixtures::*, *};
694 use crate::{
695 AccountEventKind,
696 fee::FeeModelConfig,
697 order::{
698 OrderEvent, OrderKey,
699 id::ClientOrderId,
700 request::RequestCancel,
701 state::{InactiveOrderState, OrderState},
702 },
703 };
704 use rust_decimal::Decimal;
705
706 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
722 async fn open_order_emissions_are_ordered_across_fills() {
723 const FILLS: usize = 8;
724 const DEBIT_PER_FILL: &str = "500";
725
726 let usdt_start = "10000000";
727
728 let (request_tx, request_rx) = mpsc::unbounded_channel();
729 let (event_tx, mut events) = broadcast::channel(64);
730
731 let exchange = MockExchange::new(
732 spot_config("100", usdt_start, FeeModelConfig::default()),
733 request_rx,
734 event_tx,
735 spot_instruments(),
736 );
737
738 let driver = tokio::spawn(exchange.run());
739
740 let mut responses = Vec::with_capacity(FILLS);
741 for nth in 0..FILLS {
742 let (response_tx, response_rx) = oneshot::channel();
743 let mut request = buy_request("0.01", market_prices("50000"));
744 request.key.cid = ClientOrderId::new(format!("cid-{nth}"));
745
746 request_tx
747 .send(MockExchangeRequest::open_order(
748 Utc::now(),
749 response_tx,
750 request,
751 ))
752 .unwrap();
753 responses.push(response_rx);
754 }
755
756 drop(request_tx);
758 driver.await.unwrap();
759
760 for response in responses {
761 let response = response.await.expect("every open is answered");
762 assert!(
763 matches!(
764 response.state,
765 OrderState::Active(_) | OrderState::Inactive(_)
766 ),
767 "the account is funded and the instrument priced, so every open fills: {response:?}"
768 );
769 }
770
771 let balances = std::iter::from_fn(|| events.try_recv().ok())
773 .filter_map(|event| match event.kind {
774 AccountEventKind::BalanceSnapshot(snapshot) if snapshot.0.asset == quote() => {
775 Some(snapshot.0.balance.total)
776 }
777 _ => None,
778 })
779 .collect::<Vec<_>>();
780
781 let expected = (1..=FILLS)
783 .map(|nth| d(usdt_start) - d(DEBIT_PER_FILL) * Decimal::from(nth))
784 .collect::<Vec<_>>();
785
786 assert_eq!(
787 balances, expected,
788 "balance snapshots must reach the client in the order the venue booked them"
789 );
790 }
791
792 #[tokio::test]
794 async fn the_driver_fills_at_the_configured_slippage() {
795 let (request_tx, request_rx) = mpsc::unbounded_channel();
796 let (event_tx, mut events) = broadcast::channel(16);
797 let config = MockExecutionConfig {
798 slippage: crate::slippage::SlippageConfig::fixed(d("0.01")).unwrap(),
799 ..spot_config("100", "10000000", FeeModelConfig::default())
800 };
801 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
802 let driver = tokio::spawn(exchange.run());
803
804 let (response_tx, response_rx) = oneshot::channel();
805 request_tx
806 .send(MockExchangeRequest::open_order(
807 Utc::now(),
808 response_tx,
809 buy_request("0.01", market_prices("50000")),
810 ))
811 .unwrap();
812 response_rx.await.expect("the open is answered");
813 drop(request_tx);
814 driver.await.unwrap();
815
816 let prices = std::iter::from_fn(|| events.try_recv().ok())
817 .filter_map(|event| match event.kind {
818 AccountEventKind::Trade(trade) => Some(trade.price),
819 _ => None,
820 })
821 .collect::<Vec<_>>();
822 assert_eq!(prices, vec![d("50500")], "50,000 bought 1% above");
823 }
824
825 #[tokio::test(start_paused = true)]
840 async fn a_swept_expiry_is_emitted_behind_the_fill_booked_before_it() {
841 let opened = Utc::now();
842 let expiry = opened + chrono::TimeDelta::seconds(30);
843
844 let (request_tx, request_rx) = mpsc::unbounded_channel();
845 let (event_tx, mut events) = broadcast::channel(64);
846
847 let mut config =
848 spot_config_holding("100", "10000000", seeded_gtd("gtd", "1", opened, expiry));
849 config.latency_ms = 50;
851
852 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
853 let driver = tokio::spawn(exchange.run());
854
855 let (fill_tx, fill_rx) = oneshot::channel();
857 request_tx
858 .send(MockExchangeRequest::open_order(
859 opened,
860 fill_tx,
861 buy_request("0.01", market_prices("50000")),
862 ))
863 .unwrap();
864
865 let (snapshot_tx, snapshot_rx) = oneshot::channel();
868 request_tx
869 .send(MockExchangeRequest::fetch_account_snapshot(
870 expiry + chrono::TimeDelta::seconds(1),
871 snapshot_tx,
872 ))
873 .unwrap();
874
875 drop(request_tx);
876 driver.await.unwrap();
877 fill_rx.await.expect("the open is answered");
878 let _ = snapshot_rx.await;
879
880 let kinds = std::iter::from_fn(|| events.try_recv().ok())
881 .map(|event| match event.kind {
882 AccountEventKind::BalanceSnapshot(_) => "balance",
883 AccountEventKind::Trade(_) => "trade",
884 AccountEventKind::OrderSnapshot(_) => "order",
885 _ => "other",
886 })
887 .collect::<Vec<_>>();
888
889 assert_eq!(
890 kinds,
891 ["balance", "balance", "trade", "order"],
892 "the fill's events must all precede the sweep, which for a seeded order — held with \
893 no reservation — owes only its terminal snapshot"
894 );
895 }
896
897 #[tokio::test]
898 async fn a_cancel_request_is_answered_rather_than_dropped() {
899 let (request_tx, request_rx) = mpsc::unbounded_channel();
900 let (event_tx, _events) = broadcast::channel(16);
901
902 let exchange = MockExchange::new(
903 spot_config("100", "10000", FeeModelConfig::default()),
904 request_rx,
905 event_tx,
906 spot_instruments(),
907 );
908 let driver = tokio::spawn(exchange.run());
909
910 let (response_tx, response_rx) = oneshot::channel();
911 let open = buy_request("1", market_prices("50000"));
912 request_tx
913 .send(MockExchangeRequest::cancel_order(
914 Utc::now(),
915 response_tx,
916 OrderEvent {
917 key: open.key,
918 state: RequestCancel { id: None },
919 },
920 ))
921 .unwrap();
922
923 let response = response_rx.await.expect("a cancel must be answered");
924 assert!(
925 response.state.is_err(),
926 "this venue rests no orders, so a cancel must be rejected: {response:?}"
927 );
928
929 drop(request_tx);
930 driver.await.unwrap();
931 }
932
933 #[tokio::test]
936 async fn an_order_state_lookup_is_answered_from_the_ledger() {
937 use crate::client::{OrderStatusClient, mock::MockExecution};
938
939 let (request_tx, request_rx) = mpsc::unbounded_channel();
940 let (event_tx, event_rx) = broadcast::channel(16);
941 let exchange = MockExchange::new(
942 spot_config("100", "10000000", FeeModelConfig::default()),
943 request_rx,
944 event_tx,
945 spot_instruments(),
946 );
947 let driver = tokio::spawn(exchange.run());
948
949 let open = buy_request("1", market_prices("50000"));
950 let (response_tx, response_rx) = oneshot::channel();
951 request_tx
952 .send(MockExchangeRequest::open_order(
953 Utc::now(),
954 response_tx,
955 open.clone(),
956 ))
957 .unwrap();
958 let opened = response_rx.await.expect("an open must be answered");
959 assert!(matches!(
960 opened.state,
961 OrderState::Inactive(InactiveOrderState::FullyFilled(_))
962 ));
963
964 let client = MockExecution {
965 mocked_exchange: EXCHANGE,
966 clock: Utc::now,
967 request_tx: request_tx.clone(),
968 event_rx,
969 };
970 let unknown = OrderKey {
971 cid: ClientOrderId::new("never-opened"),
972 ..open.key.clone()
973 };
974 let ended = client
975 .fetch_ended_orders(&[open.key.clone(), unknown])
976 .await
977 .expect("the venue is running");
978
979 let [order] = ended.as_slice() else {
980 panic!("only the filled order is reported: {ended:?}");
981 };
982 assert_eq!(order.key, open.key);
983 assert!(matches!(order.state, InactiveOrderState::FullyFilled(_)));
984
985 drop(client);
986 drop(request_tx);
987 driver.await.unwrap();
988 }
989}