1use crate::{
2 UnindexedAccountEvent,
3 client::mock::MockExecutionConfig,
4 error::StreamTerminationReason,
5 exchange::mock::{
6 request::{MockExchangeRequest, MockExchangeRequestKind},
7 venue::VenueRegime,
8 },
9 market::MarketObservation,
10};
11use chrono::{DateTime, TimeDelta, Utc};
12use fnv::FnvHashMap;
13use futures::stream::BoxStream;
14use rustrade_instrument::{
15 asset::name::AssetNameExchange,
16 exchange::ExchangeId,
17 instrument::{Instrument, name::InstrumentNameExchange},
18};
19use std::{collections::VecDeque, fmt::Debug, sync::Arc, time::Duration};
20
21const MAX_DEADLINE_WAIT: Duration = Duration::from_secs(86_400);
24use tokio::sync::{broadcast, mpsc, oneshot};
25use tokio_stream::{StreamExt, wrappers::BroadcastStream};
26use tracing::{error, info, warn};
27
28pub mod account;
29pub mod orders;
30pub mod request;
31pub mod venue;
32
33pub use venue::{CancelOutcome, OpenOutcome, SimulatedVenue, VenueInstrumentMarket, VenueOutcome};
34
35pub type MockExchangeClock = Arc<dyn Fn() -> DateTime<Utc> + Send + Sync>;
38
39#[derive(Debug, Clone, PartialEq, Eq)]
42#[non_exhaustive]
43pub enum MarketFeedEvent {
44 Observation {
46 instrument: InstrumentNameExchange,
47 observation: MarketObservation,
48 },
49 Lagged { missed: u64 },
54}
55
56pub struct MockExchange {
75 pub venue: SimulatedVenue,
77 pub latency_ms: u64,
79 pub request_rx: mpsc::UnboundedReceiver<MockExchangeRequest>,
80 pub event_tx: broadcast::Sender<UnindexedAccountEvent>,
81 market: Option<MarketDrive>,
83}
84
85impl Debug for MockExchange {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 f.debug_struct("MockExchange")
89 .field("venue", &self.venue)
90 .field("latency_ms", &self.latency_ms)
91 .field("request_rx", &self.request_rx)
92 .field("event_tx", &self.event_tx)
93 .field("market_driven", &self.market.is_some())
94 .finish()
95 }
96}
97
98struct MarketDrive {
100 feed: BoxStream<'static, MarketFeedEvent>,
101 clock: MockExchangeClock,
102}
103
104impl MockExchange {
105 pub fn new(
110 config: MockExecutionConfig,
111 request_rx: mpsc::UnboundedReceiver<MockExchangeRequest>,
112 event_tx: broadcast::Sender<UnindexedAccountEvent>,
113 instruments: FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>>,
114 ) -> Self {
115 Self {
116 venue: SimulatedVenue::new(&config, instruments),
117 latency_ms: config.latency_ms,
118 request_rx,
119 event_tx,
120 market: None,
121 }
122 }
123
124 pub fn new_market_driven(
164 venue: SimulatedVenue,
165 latency_ms: u64,
166 request_rx: mpsc::UnboundedReceiver<MockExchangeRequest>,
167 event_tx: broadcast::Sender<UnindexedAccountEvent>,
168 feed: BoxStream<'static, MarketFeedEvent>,
169 clock: MockExchangeClock,
170 ) -> Self {
171 assert_eq!(
172 venue.regime(),
173 VenueRegime::MarketDriven,
174 "MockExchange::new_market_driven given a request-priced venue: build it with \
175 SimulatedVenue::new_market_driven"
176 );
177 Self {
178 venue,
179 latency_ms,
180 request_rx,
181 event_tx,
182 market: Some(MarketDrive { feed, clock }),
183 }
184 }
185
186 pub async fn run(mut self) {
194 let (emit_tx, emit_rx) = mpsc::unbounded_channel();
195 let emitter = tokio::spawn(Self::emit_queued_output(
196 emit_rx,
197 self.event_tx.clone(),
198 self.venue.exchange,
199 ));
200
201 match self.market.take() {
202 None => {
203 let emit = Emit::new(emit_tx, self.venue.exchange, self.latency_ms);
204 self.serve_request_priced(&emit).await;
205 }
206 Some(drive) => {
207 let emit = Emit::new(emit_tx, self.venue.exchange, self.latency_ms / 2);
208 self.serve_market_driven(drive, &emit).await;
209 }
210 }
211
212 if let Err(error) = emitter.await {
216 error!(
217 exchange = %self.venue.exchange,
218 %error,
219 "MockExchange emitter task did not shut down cleanly; queued fills may be lost"
220 );
221 }
222
223 info!(exchange = %self.venue.exchange, "MockExchange shutting down");
224 }
225
226 async fn serve_request_priced(&mut self, emit: &Emit) {
228 while let Some(request) = self.request_rx.recv().await {
229 let expiries = self.advance_venue_time(request.time_request);
233 emit.queue(expiries, None);
234 self.book(request.kind, emit);
235 }
236 }
237
238 async fn serve_market_driven(&mut self, drive: MarketDrive, emit: &Emit) {
244 let MarketDrive { mut feed, clock } = drive;
245 let mut clock = MonotonicClock::new(clock);
246 let to_venue = Duration::from_millis(self.latency_ms / 2);
247 let exchange = self.venue.exchange;
248
249 let mut arriving = VecDeque::<(tokio::time::Instant, MockExchangeRequestKind)>::new();
252 let mut feed_open = true;
253 let mut requests_open = true;
254 let mut stuck_deadline = None;
257
258 let started = self.venue.advance_time(clock.now());
259 emit.queue(started, None);
260
261 while requests_open || !arriving.is_empty() {
262 let arrives_at = arriving.front().map(|(at, _)| *at);
263 let deadline = self
264 .venue
265 .next_deadline()
266 .filter(|deadline| Some(*deadline) != stuck_deadline);
267 let mut capped = false;
270 let deadline_at = deadline.map(|deadline| {
271 let wait = (deadline - clock.peek()).to_std().unwrap_or(Duration::ZERO);
273 capped = wait > MAX_DEADLINE_WAIT;
274 tokio::time::Instant::now() + wait.min(MAX_DEADLINE_WAIT)
275 });
276
277 tokio::select! {
278 biased;
279
280 event = futures::StreamExt::next(&mut feed), if feed_open => match event {
281 Some(MarketFeedEvent::Observation { instrument, observation }) => {
282 let events = self.venue.apply_market(&instrument, observation, clock.now());
283 emit.queue(events, None);
284 }
285 Some(MarketFeedEvent::Lagged { missed }) => {
286 error!(
287 %exchange,
288 missed,
289 "MockExchange market feed lagged: the venue missed market \
290 observations, so resting orders may have missed fills; ending the \
291 account stream"
292 );
293 emit.queue(
294 vec![UnindexedAccountEvent::stream_terminated(
295 exchange,
296 StreamTerminationReason::Error(format!(
297 "mock exchange market feed lagged: {missed} observations missed"
298 )),
299 )],
300 None,
301 );
302 }
303 None => {
304 feed_open = false;
305 warn!(
306 %exchange,
307 "MockExchange market feed ended: serving requests on the last market seen"
308 );
309 }
310 },
311
312 () = sleep_until(arrives_at), if arrives_at.is_some() => {
313 if let Some((_, kind)) = arriving.pop_front() {
314 let expiries = self.venue.advance_time(clock.now());
315 emit.queue(expiries, None);
316 self.book(kind, emit);
317 }
318 },
319
320 request = self.request_rx.recv(), if requests_open => match request {
321 Some(request) => {
322 arriving.push_back((tokio::time::Instant::now() + to_venue, request.kind));
323 }
324 None => requests_open = false,
325 },
326
327 () = sleep_until(deadline_at), if deadline_at.is_some() => {
328 if let Some(deadline) = deadline {
331 if capped {
333 continue;
334 }
335 let now = clock.raise_to(deadline);
336 let events = self.venue.advance_time(now);
337 emit.queue(events, None);
338 if self.venue.next_deadline() == Some(deadline) {
339 error!(
340 %exchange,
341 %deadline,
342 "MockExchange venue still reports a deadline it was moved to; \
343 no longer waking for it"
344 );
345 stuck_deadline = Some(deadline);
346 }
347 }
348 },
349 }
350 }
351 }
352
353 fn book(&mut self, kind: MockExchangeRequestKind, emit: &Emit) {
358 match kind {
359 MockExchangeRequestKind::FetchAccountSnapshot { response_tx } => {
360 emit.respond(Vec::new(), response_tx, self.venue.account_snapshot());
361 }
362 MockExchangeRequestKind::FetchBalances {
363 response_tx,
364 assets,
365 } => {
366 emit.respond(Vec::new(), response_tx, self.venue.balances(&assets));
367 }
368 MockExchangeRequestKind::FetchOrdersOpen {
369 response_tx,
370 instruments,
371 } => {
372 emit.respond(
373 Vec::new(),
374 response_tx,
375 self.venue.orders_open(&instruments),
376 );
377 }
378 MockExchangeRequestKind::FetchOrdersEnded {
379 response_tx,
380 orders,
381 } => {
382 emit.respond(Vec::new(), response_tx, self.venue.orders_ended(&orders));
383 }
384 MockExchangeRequestKind::FetchTrades {
385 response_tx,
386 start,
387 end,
388 } => {
389 let mut trades = self.venue.trades(start);
390 trades.retain(|trade| trade.time_exchange <= end);
391 emit.respond(Vec::new(), response_tx, trades);
392 }
393 MockExchangeRequestKind::CancelOrder {
394 response_tx,
395 request,
396 } => {
397 let outcome = self.venue.cancel_order(request);
400 emit.respond(outcome.events, response_tx, outcome.response);
401 }
402 MockExchangeRequestKind::OpenOrder {
403 response_tx,
404 request,
405 } => {
406 let outcome = self.venue.open_order(request);
407 emit.respond(outcome.events, response_tx, outcome.response);
408 }
409 }
410 }
411
412 #[must_use]
417 fn advance_venue_time(&mut self, time_request: DateTime<Utc>) -> Vec<UnindexedAccountEvent> {
418 let client_to_exchange_latency = self.latency_ms / 2;
419
420 let time_exchange = time_request
421 .checked_add_signed(TimeDelta::milliseconds(client_to_exchange_latency as i64))
422 .unwrap_or(time_request);
423
424 self.venue.advance_time(time_exchange)
425 }
426
427 async fn emit_queued_output(
434 mut emit_rx: mpsc::UnboundedReceiver<PendingEmission>,
435 event_tx: broadcast::Sender<UnindexedAccountEvent>,
436 exchange: ExchangeId,
437 ) {
438 while let Some(emission) = emit_rx.recv().await {
439 tokio::time::sleep_until(emission.ready_at).await;
440
441 for event in emission.events {
443 if event_tx.send(event).is_err() {
444 error!(
445 %exchange,
446 "MockExchange failed to send AccountEvent notification to client"
447 );
448 }
449 }
450
451 if let Some(respond) = emission.respond {
453 respond(exchange);
454 }
455 }
456 }
457
458 pub fn account_stream(&self) -> BoxStream<'static, UnindexedAccountEvent> {
459 futures::StreamExt::boxed(BroadcastStream::new(self.event_tx.subscribe()).map_while(
460 |result| match result {
461 Ok(event) => Some(event),
462 Err(error) => {
463 error!(
464 ?error,
465 "MockExchange Broadcast AccountStream lagged - terminating"
466 );
467 None
468 }
469 },
470 ))
471 }
472}
473
474async fn sleep_until(at: Option<tokio::time::Instant>) {
477 match at {
478 Some(at) => tokio::time::sleep_until(at).await,
479 None => std::future::pending().await,
480 }
481}
482
483struct MonotonicClock {
485 clock: MockExchangeClock,
486 latest: Option<DateTime<Utc>>,
487}
488
489impl MonotonicClock {
490 fn new(clock: MockExchangeClock) -> Self {
491 Self {
492 clock,
493 latest: None,
494 }
495 }
496
497 fn now(&mut self) -> DateTime<Utc> {
499 let now = (self.clock)();
500 self.raise_to(now)
501 }
502
503 fn peek(&self) -> DateTime<Utc> {
505 let now = (self.clock)();
506 self.latest.map_or(now, |latest| latest.max(now))
507 }
508
509 fn raise_to(&mut self, time: DateTime<Utc>) -> DateTime<Utc> {
511 let latest = self.latest.map_or(time, |latest| latest.max(time));
512 self.latest = Some(latest);
513 latest
514 }
515}
516
517struct Emit {
520 tx: mpsc::UnboundedSender<PendingEmission>,
521 exchange: ExchangeId,
522 delay: Duration,
524}
525
526impl Emit {
527 fn new(
528 tx: mpsc::UnboundedSender<PendingEmission>,
529 exchange: ExchangeId,
530 delay_ms: u64,
531 ) -> Self {
532 Self {
533 tx,
534 exchange,
535 delay: Duration::from_millis(delay_ms),
536 }
537 }
538
539 fn respond<Response>(
541 &self,
542 events: Vec<UnindexedAccountEvent>,
543 response_tx: oneshot::Sender<Response>,
544 response: Response,
545 ) where
546 Response: Send + 'static,
547 {
548 let respond: Respond = Box::new(move |exchange| {
549 if response_tx.send(response).is_err() {
550 error!(
551 %exchange,
552 kind = std::any::type_name::<Response>(),
553 "MockExchange failed to send oneshot response to client"
554 );
555 }
556 });
557 self.queue(events, Some(respond));
558 }
559
560 fn queue(&self, events: Vec<UnindexedAccountEvent>, respond: Option<Respond>) {
576 if events.is_empty() && respond.is_none() {
577 return;
578 }
579
580 let ready_at = tokio::time::Instant::now() + self.delay;
584
585 if self
588 .tx
589 .send(PendingEmission {
590 ready_at,
591 events,
592 respond,
593 })
594 .is_err()
595 {
596 error!(
597 exchange = %self.exchange,
598 "MockExchange could not queue venue output: the emitter has stopped"
599 );
600 }
601 }
602}
603
604type Respond = Box<dyn FnOnce(ExchangeId) + Send>;
606
607struct PendingEmission {
617 ready_at: tokio::time::Instant,
619 events: Vec<UnindexedAccountEvent>,
621 respond: Option<Respond>,
624}
625
626#[cfg(test)]
632#[allow(clippy::unwrap_used, clippy::expect_used)] pub(crate) mod fixtures {
634 use crate::{
635 InstrumentAccountSnapshot, UnindexedAccountSnapshot,
636 balance::{AssetBalance, Balance},
637 client::mock::MockExecutionConfig,
638 fee::{FeeModelConfig, SpotFeeAsset},
639 fill::SimFillConfig,
640 market::MarketSnapshot,
641 order::{
642 Order, OrderEvent, OrderKey, OrderKind, TimeInForce, UnindexedOrder,
643 id::{ClientOrderId, OrderId, StrategyId, VenueOrderId},
644 request::{OrderRequestOpen, RequestOpen},
645 state::{Open, OrderState},
646 },
647 position::PositionReport,
648 };
649 use chrono::{DateTime, Utc};
650 use fnv::FnvHashMap;
651 use rust_decimal::Decimal;
652 use rustrade_instrument::{
653 Side, Underlying,
654 asset::name::AssetNameExchange,
655 exchange::ExchangeId,
656 instrument::{
657 Instrument,
658 kind::InstrumentKind,
659 name::{InstrumentNameExchange, InstrumentNameInternal},
660 quote::InstrumentQuoteAsset,
661 },
662 };
663
664 pub(super) const EXCHANGE: ExchangeId = ExchangeId::BinanceSpot;
665
666 pub(super) fn d(s: &str) -> Decimal {
667 s.parse().unwrap()
668 }
669
670 pub(super) fn base() -> AssetNameExchange {
671 AssetNameExchange::new("BTC")
672 }
673
674 pub(super) fn quote() -> AssetNameExchange {
675 AssetNameExchange::new("USDT")
676 }
677
678 pub(super) fn instrument_name() -> InstrumentNameExchange {
679 InstrumentNameExchange::new("BTCUSDT")
680 }
681
682 pub(super) fn funded(
684 asset: AssetNameExchange,
685 amount: Decimal,
686 ) -> AssetBalance<AssetNameExchange> {
687 AssetBalance {
688 asset,
689 balance: Balance::new(amount, amount),
690 time_exchange: Utc::now(),
691 }
692 }
693
694 pub(super) fn config_from_balances(
696 balances: Vec<AssetBalance<AssetNameExchange>>,
697 fee_model: FeeModelConfig,
698 ) -> MockExecutionConfig {
699 config_from_balances_with_fill(balances, fee_model, SimFillConfig::default())
700 }
701
702 pub(super) fn config_from_balances_with_fill(
703 balances: Vec<AssetBalance<AssetNameExchange>>,
704 fee_model: FeeModelConfig,
705 fill_model: SimFillConfig,
706 ) -> MockExecutionConfig {
707 MockExecutionConfig::new(
708 EXCHANGE,
709 UnindexedAccountSnapshot {
710 exchange: EXCHANGE,
711 balances,
712 instruments: vec![],
713 },
714 0, fee_model,
716 fill_model,
717 SpotFeeAsset::default(),
718 crate::slippage::SlippageConfig::default(),
719 crate::fill::MarketOrderTiming::default(),
720 crate::fill::StaleMarketPolicy::default(),
721 None,
722 None,
723 )
724 }
725
726 pub(super) fn spot_config_holding(
733 btc: &str,
734 usdt: &str,
735 order: UnindexedOrder,
736 ) -> MockExecutionConfig {
737 MockExecutionConfig::new(
738 EXCHANGE,
739 UnindexedAccountSnapshot {
740 exchange: EXCHANGE,
741 balances: vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
742 instruments: vec![InstrumentAccountSnapshot {
743 instrument: instrument_name(),
744 orders: vec![order],
745 orders_complete: true,
746 position: PositionReport::Unreported,
747 isolated: None,
748 }],
749 },
750 0, FeeModelConfig::default(),
752 SimFillConfig::default(),
753 SpotFeeAsset::default(),
754 crate::slippage::SlippageConfig::default(),
755 crate::fill::MarketOrderTiming::default(),
756 crate::fill::StaleMarketPolicy::default(),
757 None,
758 None,
759 )
760 }
761
762 pub(super) fn seeded_gtd(
764 cid: &str,
765 price: &str,
766 opened: DateTime<Utc>,
767 expiry: DateTime<Utc>,
768 ) -> UnindexedOrder {
769 Order {
770 key: OrderKey {
771 exchange: EXCHANGE,
772 instrument: instrument_name(),
773 strategy: StrategyId::new("test"),
774 cid: ClientOrderId::new(cid),
775 },
776 side: Side::Buy,
777 price: Some(d(price)),
778 quantity: d("0.01"),
779 kind: OrderKind::Limit,
780 time_in_force: TimeInForce::GoodTillDate { expiry },
781 state: OrderState::active(Open::new(
782 VenueOrderId::Assigned(OrderId::new(cid)),
783 opened,
784 rust_decimal::Decimal::ZERO,
785 )),
786 }
787 }
788
789 pub(super) fn spot_config(
791 btc: &str,
792 usdt: &str,
793 fee_model: FeeModelConfig,
794 ) -> MockExecutionConfig {
795 config_from_balances(
796 vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
797 fee_model,
798 )
799 }
800
801 pub(super) fn spot_config_with_fill(
802 btc: &str,
803 usdt: &str,
804 fee_model: FeeModelConfig,
805 fill_model: SimFillConfig,
806 ) -> MockExecutionConfig {
807 config_from_balances_with_fill(
808 vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
809 fee_model,
810 fill_model,
811 )
812 }
813
814 pub(super) fn instruments_of(
815 instrument: Instrument<ExchangeId, AssetNameExchange>,
816 ) -> FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
817 let mut instruments = FnvHashMap::default();
818 instruments.insert(instrument.name_exchange.clone(), instrument);
819 instruments
820 }
821
822 pub(super) fn spot_instruments()
823 -> FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
824 instruments_of(Instrument {
825 exchange: EXCHANGE,
826 name_internal: InstrumentNameInternal::new("btcusdt"),
827 name_exchange: instrument_name(),
828 underlying: Underlying {
829 base: base(),
830 quote: quote(),
831 },
832 quote: InstrumentQuoteAsset::UnderlyingQuote,
833 kind: InstrumentKind::Spot,
834 spec: None,
835 data_venue: None,
836 })
837 }
838
839 pub(super) fn request(
845 instrument: InstrumentNameExchange,
846 side: Side,
847 quantity: &str,
848 market: Option<MarketSnapshot>,
849 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
850 OrderEvent {
851 key: OrderKey {
852 exchange: EXCHANGE,
853 instrument,
854 strategy: StrategyId::new("test"),
855 cid: ClientOrderId::new("test-cid"),
856 },
857 state: RequestOpen {
858 market,
859 ..RequestOpen::new(
860 side,
861 None, d(quantity),
863 OrderKind::Market,
864 TimeInForce::ImmediateOrCancel,
865 )
866 },
867 }
868 }
869
870 pub(super) fn with_effect(
872 mut request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
873 effect: crate::margin::MarginEffect,
874 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
875 request.state = request.state.with_margin_effect(effect);
876 request
877 }
878
879 pub(super) fn buy_request(
880 quantity: &str,
881 market: Option<MarketSnapshot>,
882 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
883 request(instrument_name(), Side::Buy, quantity, market)
884 }
885
886 pub(super) fn sell_request(
887 quantity: &str,
888 market: Option<MarketSnapshot>,
889 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
890 request(instrument_name(), Side::Sell, quantity, market)
891 }
892
893 pub(super) fn gtc() -> TimeInForce {
895 TimeInForce::GoodUntilCancelled { post_only: false }
896 }
897
898 pub(super) fn limit_request(
903 cid: &str,
904 side: Side,
905 quantity: &str,
906 price: &str,
907 time_in_force: TimeInForce,
908 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
909 OrderEvent {
910 key: OrderKey {
911 exchange: EXCHANGE,
912 instrument: instrument_name(),
913 strategy: StrategyId::new("test"),
914 cid: ClientOrderId::new(cid),
915 },
916 state: RequestOpen::new(
917 side,
918 Some(d(price)),
919 d(quantity),
920 OrderKind::Limit,
921 time_in_force,
922 ),
923 }
924 }
925
926 pub(super) fn book(best_bid: &str, best_ask: &str) -> MarketSnapshot {
928 MarketSnapshot {
929 best_bid: Some(d(best_bid)),
930 best_ask: Some(d(best_ask)),
931 last_price: None,
932 }
933 }
934
935 pub(super) fn traded(last_price: &str) -> MarketSnapshot {
937 MarketSnapshot {
938 best_bid: None,
939 best_ask: None,
940 last_price: Some(d(last_price)),
941 }
942 }
943
944 pub(super) fn market_prices(price: &str) -> Option<MarketSnapshot> {
946 let p = Some(d(price));
947 Some(MarketSnapshot {
948 best_bid: p,
949 best_ask: p,
950 last_price: p,
951 })
952 }
953}
954
955#[cfg(test)]
956#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
958 use super::{fixtures::*, *};
959 use crate::market::MarketSnapshot;
960 use crate::{
961 AccountEventKind,
962 fee::FeeModelConfig,
963 order::{
964 OrderEvent, OrderKey,
965 id::ClientOrderId,
966 request::RequestCancel,
967 state::{InactiveOrderState, OrderState},
968 },
969 };
970 use rust_decimal::Decimal;
971 use rustrade_instrument::Side;
972
973 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
989 async fn open_order_emissions_are_ordered_across_fills() {
990 const FILLS: usize = 8;
991 const DEBIT_PER_FILL: &str = "500";
992
993 let usdt_start = "10000000";
994
995 let (request_tx, request_rx) = mpsc::unbounded_channel();
996 let (event_tx, mut events) = broadcast::channel(64);
997
998 let exchange = MockExchange::new(
999 spot_config("100", usdt_start, FeeModelConfig::default()),
1000 request_rx,
1001 event_tx,
1002 spot_instruments(),
1003 );
1004
1005 let driver = tokio::spawn(exchange.run());
1006
1007 let mut responses = Vec::with_capacity(FILLS);
1008 for nth in 0..FILLS {
1009 let (response_tx, response_rx) = oneshot::channel();
1010 let mut request = buy_request("0.01", market_prices("50000"));
1011 request.key.cid = ClientOrderId::new(format!("cid-{nth}"));
1012
1013 request_tx
1014 .send(MockExchangeRequest::open_order(
1015 Utc::now(),
1016 response_tx,
1017 request,
1018 ))
1019 .unwrap();
1020 responses.push(response_rx);
1021 }
1022
1023 drop(request_tx);
1025 driver.await.unwrap();
1026
1027 for response in responses {
1028 let response = response.await.expect("every open is answered");
1029 assert!(
1030 matches!(
1031 response.state,
1032 OrderState::Active(_) | OrderState::Inactive(_)
1033 ),
1034 "the account is funded and the instrument priced, so every open fills: {response:?}"
1035 );
1036 }
1037
1038 let balances = std::iter::from_fn(|| events.try_recv().ok())
1040 .filter_map(|event| match event.kind {
1041 AccountEventKind::BalanceSnapshot(snapshot) if snapshot.0.asset == quote() => {
1042 Some(snapshot.0.balance.total)
1043 }
1044 _ => None,
1045 })
1046 .collect::<Vec<_>>();
1047
1048 let expected = (1..=FILLS)
1050 .map(|nth| d(usdt_start) - d(DEBIT_PER_FILL) * Decimal::from(nth))
1051 .collect::<Vec<_>>();
1052
1053 assert_eq!(
1054 balances, expected,
1055 "balance snapshots must reach the client in the order the venue booked them"
1056 );
1057 }
1058
1059 #[tokio::test]
1061 async fn the_driver_fills_at_the_configured_slippage() {
1062 let (request_tx, request_rx) = mpsc::unbounded_channel();
1063 let (event_tx, mut events) = broadcast::channel(16);
1064 let config = MockExecutionConfig {
1065 slippage: crate::slippage::SlippageConfig::fixed(d("0.01")).unwrap(),
1066 ..spot_config("100", "10000000", FeeModelConfig::default())
1067 };
1068 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1069 let driver = tokio::spawn(exchange.run());
1070
1071 let (response_tx, response_rx) = oneshot::channel();
1072 request_tx
1073 .send(MockExchangeRequest::open_order(
1074 Utc::now(),
1075 response_tx,
1076 buy_request("0.01", market_prices("50000")),
1077 ))
1078 .unwrap();
1079 response_rx.await.expect("the open is answered");
1080 drop(request_tx);
1081 driver.await.unwrap();
1082
1083 let prices = std::iter::from_fn(|| events.try_recv().ok())
1084 .filter_map(|event| match event.kind {
1085 AccountEventKind::Trade(trade) => Some(trade.price),
1086 _ => None,
1087 })
1088 .collect::<Vec<_>>();
1089 assert_eq!(prices, vec![d("50500")], "50,000 bought 1% above");
1090 }
1091
1092 #[tokio::test(start_paused = true)]
1107 async fn a_swept_expiry_is_emitted_behind_the_fill_booked_before_it() {
1108 let opened = Utc::now();
1109 let expiry = opened + chrono::TimeDelta::seconds(30);
1110
1111 let (request_tx, request_rx) = mpsc::unbounded_channel();
1112 let (event_tx, mut events) = broadcast::channel(64);
1113
1114 let mut config =
1115 spot_config_holding("100", "10000000", seeded_gtd("gtd", "1", opened, expiry));
1116 config.latency_ms = 50;
1118
1119 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1120 let driver = tokio::spawn(exchange.run());
1121
1122 let (fill_tx, fill_rx) = oneshot::channel();
1124 request_tx
1125 .send(MockExchangeRequest::open_order(
1126 opened,
1127 fill_tx,
1128 buy_request("0.01", market_prices("50000")),
1129 ))
1130 .unwrap();
1131
1132 let (snapshot_tx, snapshot_rx) = oneshot::channel();
1135 request_tx
1136 .send(MockExchangeRequest::fetch_account_snapshot(
1137 expiry + chrono::TimeDelta::seconds(1),
1138 snapshot_tx,
1139 ))
1140 .unwrap();
1141
1142 drop(request_tx);
1143 driver.await.unwrap();
1144 fill_rx.await.expect("the open is answered");
1145 let _ = snapshot_rx.await;
1146
1147 let kinds = std::iter::from_fn(|| events.try_recv().ok())
1148 .map(|event| match event.kind {
1149 AccountEventKind::BalanceSnapshot(_) => "balance",
1150 AccountEventKind::Trade(_) => "trade",
1151 AccountEventKind::OrderSnapshot(_) => "order",
1152 _ => "other",
1153 })
1154 .collect::<Vec<_>>();
1155
1156 assert_eq!(
1157 kinds,
1158 ["balance", "balance", "trade", "order"],
1159 "the fill's events must all precede the sweep, which for a seeded order — held with \
1160 no reservation — owes only its terminal snapshot"
1161 );
1162 }
1163
1164 #[tokio::test]
1165 async fn a_cancel_request_is_answered_rather_than_dropped() {
1166 let (request_tx, request_rx) = mpsc::unbounded_channel();
1167 let (event_tx, _events) = broadcast::channel(16);
1168
1169 let exchange = MockExchange::new(
1170 spot_config("100", "10000", FeeModelConfig::default()),
1171 request_rx,
1172 event_tx,
1173 spot_instruments(),
1174 );
1175 let driver = tokio::spawn(exchange.run());
1176
1177 let (response_tx, response_rx) = oneshot::channel();
1178 let open = buy_request("1", market_prices("50000"));
1179 request_tx
1180 .send(MockExchangeRequest::cancel_order(
1181 Utc::now(),
1182 response_tx,
1183 OrderEvent {
1184 key: open.key,
1185 state: RequestCancel { id: None },
1186 },
1187 ))
1188 .unwrap();
1189
1190 let response = response_rx.await.expect("a cancel must be answered");
1191 assert!(
1192 response.state.is_err(),
1193 "this venue rests no orders, so a cancel must be rejected: {response:?}"
1194 );
1195
1196 drop(request_tx);
1197 driver.await.unwrap();
1198 }
1199
1200 #[tokio::test]
1203 async fn an_order_state_lookup_is_answered_from_the_ledger() {
1204 use crate::client::{OrderStatusClient, mock::MockExecution};
1205
1206 let (request_tx, request_rx) = mpsc::unbounded_channel();
1207 let (event_tx, event_rx) = broadcast::channel(16);
1208 let exchange = MockExchange::new(
1209 spot_config("100", "10000000", FeeModelConfig::default()),
1210 request_rx,
1211 event_tx,
1212 spot_instruments(),
1213 );
1214 let driver = tokio::spawn(exchange.run());
1215
1216 let open = buy_request("1", market_prices("50000"));
1217 let (response_tx, response_rx) = oneshot::channel();
1218 request_tx
1219 .send(MockExchangeRequest::open_order(
1220 Utc::now(),
1221 response_tx,
1222 open.clone(),
1223 ))
1224 .unwrap();
1225 let opened = response_rx.await.expect("an open must be answered");
1226 assert!(matches!(
1227 opened.state,
1228 OrderState::Inactive(InactiveOrderState::FullyFilled(_))
1229 ));
1230
1231 let client = MockExecution {
1232 mocked_exchange: EXCHANGE,
1233 clock: Utc::now,
1234 request_tx: request_tx.clone(),
1235 event_rx,
1236 };
1237 let unknown = OrderKey {
1238 cid: ClientOrderId::new("never-opened"),
1239 ..open.key.clone()
1240 };
1241 let ended = client
1242 .fetch_ended_orders(&[open.key.clone(), unknown])
1243 .await
1244 .expect("the venue is running");
1245
1246 let [order] = ended.as_slice() else {
1247 panic!("only the filled order is reported: {ended:?}");
1248 };
1249 assert_eq!(order.key, open.key);
1250 assert!(matches!(order.state, InactiveOrderState::FullyFilled(_)));
1251
1252 drop(client);
1253 drop(request_tx);
1254 driver.await.unwrap();
1255 }
1256
1257 #[tokio::test(start_paused = true)]
1260 async fn a_fetch_is_answered_after_the_fill_booked_before_it() {
1261 let (request_tx, request_rx) = mpsc::unbounded_channel();
1262 let (event_tx, mut events) = broadcast::channel(64);
1263 let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1264 config.latency_ms = 50;
1265 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1266 let driver = tokio::spawn(exchange.run());
1267
1268 let (open_tx, open_rx) = oneshot::channel();
1269 request_tx
1270 .send(MockExchangeRequest::open_order(
1271 Utc::now(),
1272 open_tx,
1273 buy_request("0.01", market_prices("50000")),
1274 ))
1275 .unwrap();
1276 let (balances_tx, balances_rx) = oneshot::channel();
1277 request_tx
1278 .send(MockExchangeRequest::fetch_balances(
1279 Utc::now(),
1280 vec![quote()],
1281 balances_tx,
1282 ))
1283 .unwrap();
1284
1285 balances_rx.await.expect("the fetch is answered");
1286 let kinds = std::iter::from_fn(|| events.try_recv().ok())
1287 .map(|event| event_name(&event))
1288 .collect::<Vec<_>>();
1289 assert!(
1290 kinds.contains(&"trade"),
1291 "the fill booked before the fetch is emitted before the fetch is answered: {kinds:?}"
1292 );
1293
1294 open_rx.await.expect("the open is answered");
1295 drop(request_tx);
1296 driver.await.unwrap();
1297 }
1298
1299 #[test]
1301 #[should_panic(expected = "request-priced venue")]
1302 fn a_market_driven_driver_refuses_a_request_priced_venue() {
1303 let config = spot_config("0", "10000", FeeModelConfig::default());
1304 let (_request_tx, request_rx) = mpsc::unbounded_channel();
1305 let (event_tx, _events) = broadcast::channel(16);
1306 let _ = MockExchange::new_market_driven(
1307 SimulatedVenue::new(&config, spot_instruments()),
1308 0,
1309 request_rx,
1310 event_tx,
1311 futures::StreamExt::boxed(futures::stream::empty()),
1312 Arc::new(Utc::now),
1313 );
1314 }
1315
1316 fn event_name(event: &UnindexedAccountEvent) -> &'static str {
1317 match &event.kind {
1318 AccountEventKind::BalanceSnapshot(_) => "balance",
1319 AccountEventKind::Trade(_) => "trade",
1320 AccountEventKind::OrderSnapshot(_) => "order",
1321 AccountEventKind::StreamTerminated(_) => "terminated",
1322 _ => "other",
1323 }
1324 }
1325
1326 struct Paper {
1329 request_tx: mpsc::UnboundedSender<MockExchangeRequest>,
1330 market_tx: mpsc::UnboundedSender<MarketFeedEvent>,
1331 events: broadcast::Receiver<UnindexedAccountEvent>,
1332 driver: tokio::task::JoinHandle<()>,
1333 start: DateTime<Utc>,
1335 }
1336
1337 impl Paper {
1338 fn start(config: MockExecutionConfig) -> Self {
1340 let start = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap();
1341 let anchor = tokio::time::Instant::now();
1342 let clock: MockExchangeClock = Arc::new(move || {
1343 start + TimeDelta::from_std(anchor.elapsed()).expect("a test runs for less")
1344 });
1345 Self::start_on(config, clock, start)
1346 }
1347
1348 fn start_on(
1349 config: MockExecutionConfig,
1350 clock: MockExchangeClock,
1351 start: DateTime<Utc>,
1352 ) -> Self {
1353 let (request_tx, request_rx) = mpsc::unbounded_channel();
1354 let (event_tx, events) = broadcast::channel(256);
1355 let (market_tx, market_rx) = mpsc::unbounded_channel();
1356 let exchange = MockExchange::new_market_driven(
1357 SimulatedVenue::new_market_driven(&config, spot_instruments()),
1358 config.latency_ms,
1359 request_rx,
1360 event_tx,
1361 futures::StreamExt::boxed(tokio_stream::wrappers::UnboundedReceiverStream::new(
1362 market_rx,
1363 )),
1364 clock,
1365 );
1366 Self {
1367 request_tx,
1368 market_tx,
1369 events,
1370 driver: tokio::spawn(exchange.run()),
1371 start,
1372 }
1373 }
1374
1375 fn observe(&self, snapshot: MarketSnapshot) {
1376 self.market_tx
1377 .send(MarketFeedEvent::Observation {
1378 instrument: instrument_name(),
1379 observation: MarketObservation::new(snapshot),
1380 })
1381 .unwrap();
1382 }
1383
1384 async fn open(
1385 &self,
1386 request: crate::order::request::OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
1387 ) -> crate::order::Order<
1388 ExchangeId,
1389 InstrumentNameExchange,
1390 crate::order::state::UnindexedOrderState,
1391 > {
1392 let (response_tx, response_rx) = oneshot::channel();
1393 self.request_tx
1394 .send(MockExchangeRequest::open_order(
1395 Utc::now(),
1396 response_tx,
1397 request,
1398 ))
1399 .unwrap();
1400 response_rx.await.expect("an open is answered")
1401 }
1402
1403 async fn next_event(&mut self) -> UnindexedAccountEvent {
1405 tokio::time::timeout(std::time::Duration::from_secs(3600), self.events.recv())
1406 .await
1407 .expect("an account event arrives")
1408 .expect("the account channel is open")
1409 }
1410
1411 async fn until_trade(
1413 &mut self,
1414 ) -> crate::trade::Trade<AssetNameExchange, InstrumentNameExchange> {
1415 loop {
1416 if let AccountEventKind::Trade(trade) = self.next_event().await.kind {
1417 return trade;
1418 }
1419 }
1420 }
1421
1422 async fn stop(self) {
1423 drop(self.request_tx);
1424 self.driver.await.unwrap();
1425 }
1426 }
1427
1428 fn priced(price: &str) -> MarketSnapshot {
1429 market_prices(price).unwrap()
1430 }
1431
1432 #[tokio::test(start_paused = true)]
1436 async fn a_paper_limit_rests_then_fills_when_the_market_crosses_it() {
1437 let mut config = spot_config("0", "100000", FeeModelConfig::default());
1438 config.latency_ms = 100;
1439 let mut paper = Paper::start(config);
1440 paper.observe(priced("50000"));
1441
1442 let sent = tokio::time::Instant::now();
1443 let response = paper
1444 .open(limit_request("rest", Side::Buy, "1", "49000", gtc()))
1445 .await;
1446 assert!(
1447 matches!(response.state, OrderState::Active(_)),
1448 "a limit below the market rests: {response:?}"
1449 );
1450 assert_eq!(
1451 sent.elapsed(),
1452 std::time::Duration::from_millis(100),
1453 "half the latency to the venue, half back"
1454 );
1455
1456 paper.observe(priced("48000"));
1457 let trade = paper.until_trade().await;
1458 assert_eq!(trade.quantity, d("1"));
1459 assert!(
1460 trade.price <= d("49000"),
1461 "a buy limit never pays more: {trade:?}"
1462 );
1463 paper.stop().await;
1464 }
1465
1466 #[tokio::test(start_paused = true)]
1469 async fn a_paper_request_is_booked_against_the_market_when_it_arrives() {
1470 let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1471 config.latency_ms = 100;
1472 let mut paper = Paper::start(config);
1473 paper.observe(priced("50000"));
1474
1475 let (response_tx, response_rx) = oneshot::channel();
1476 paper
1477 .request_tx
1478 .send(MockExchangeRequest::open_order(
1479 Utc::now(),
1480 response_tx,
1481 buy_request("0.01", market_prices("1")),
1483 ))
1484 .unwrap();
1485 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1487 paper.observe(priced("51000"));
1488
1489 let trade = paper.until_trade().await;
1490 assert_eq!(
1491 trade.price,
1492 d("51000"),
1493 "booked against the market as it arrived"
1494 );
1495 assert_eq!(
1496 trade.time_exchange,
1497 paper.start + TimeDelta::milliseconds(50),
1498 "stamped when it reached the venue"
1499 );
1500 response_rx.await.expect("the open is answered");
1501 paper.stop().await;
1502 }
1503
1504 #[tokio::test(start_paused = true)]
1507 async fn a_paper_order_expires_at_its_deadline_in_a_quiet_market() {
1508 let config = spot_config("0", "100000", FeeModelConfig::default());
1509 let mut paper = Paper::start(config);
1510 paper.observe(priced("50000"));
1511 let expiry = paper.start + TimeDelta::seconds(10);
1512 let started = tokio::time::Instant::now();
1513
1514 let response = paper
1515 .open(limit_request(
1516 "gtd",
1517 Side::Buy,
1518 "1",
1519 "49000",
1520 crate::order::TimeInForce::GoodTillDate { expiry },
1521 ))
1522 .await;
1523 assert!(matches!(response.state, OrderState::Active(_)));
1524
1525 let expired = loop {
1526 let event = paper.next_event().await;
1527 if let AccountEventKind::OrderSnapshot(order) = event.kind
1528 && matches!(
1529 order.0.state,
1530 OrderState::Inactive(InactiveOrderState::Expired(_))
1531 )
1532 {
1533 break order.0;
1534 }
1535 };
1536 let OrderState::Inactive(InactiveOrderState::Expired(expired)) = expired.state else {
1537 unreachable!("matched above")
1538 };
1539 assert_eq!(expired.time_exchange, expiry, "retired at its own deadline");
1540 assert!(
1541 started.elapsed() >= std::time::Duration::from_secs(10),
1542 "and not before that time has passed: {:?}",
1543 started.elapsed()
1544 );
1545 paper.stop().await;
1546 }
1547
1548 #[tokio::test(start_paused = true)]
1551 async fn a_lagged_paper_feed_ends_the_account_stream_after_the_fills_before_it() {
1552 let config = spot_config("0", "10000000", FeeModelConfig::default());
1553 let mut paper = Paper::start(config);
1554 paper.observe(priced("50000"));
1555 paper.open(buy_request("0.01", None)).await;
1556 paper
1557 .market_tx
1558 .send(MarketFeedEvent::Lagged { missed: 3 })
1559 .unwrap();
1560
1561 let mut kinds = Vec::new();
1562 while kinds.last() != Some(&"terminated") {
1563 kinds.push(event_name(&paper.next_event().await));
1564 }
1565 assert!(
1566 kinds.contains(&"trade"),
1567 "the fill booked before the lag precedes the termination: {kinds:?}"
1568 );
1569
1570 let response = paper.open(buy_request("0.01", None)).await;
1571 assert!(
1572 matches!(
1573 response.state,
1574 OrderState::Inactive(InactiveOrderState::FullyFilled(_))
1575 ),
1576 "the venue keeps trading after the lag: {response:?}"
1577 );
1578 paper.stop().await;
1579 }
1580
1581 #[tokio::test(start_paused = true)]
1583 async fn a_paper_driver_keeps_trading_once_its_feed_ends() {
1584 let config = spot_config("0", "10000000", FeeModelConfig::default());
1585 let mut paper = Paper::start(config);
1586 paper.observe(priced("50000"));
1587 let (replacement, _) = mpsc::unbounded_channel();
1589 drop(std::mem::replace(&mut paper.market_tx, replacement));
1590
1591 paper.open(buy_request("0.01", None)).await;
1592 assert_eq!(paper.until_trade().await.price, d("50000"));
1593 paper.stop().await;
1594 }
1595
1596 #[tokio::test(start_paused = true)]
1598 async fn a_paper_clock_that_steps_back_does_not_move_the_venue_back() {
1599 let later = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap();
1600 let reading = Arc::new(std::sync::Mutex::new(later));
1601 let clock: MockExchangeClock = {
1602 let reading = Arc::clone(&reading);
1603 Arc::new(move || *reading.lock().unwrap())
1604 };
1605 let config = spot_config("0", "10000000", FeeModelConfig::default());
1606 let mut paper = Paper::start_on(config, clock, later);
1607 paper.observe(priced("50000"));
1608 tokio::time::sleep(std::time::Duration::from_millis(1)).await;
1610 *reading.lock().unwrap() = later - TimeDelta::seconds(100);
1611
1612 paper.open(buy_request("0.01", None)).await;
1613 assert_eq!(paper.until_trade().await.time_exchange, later);
1614 paper.stop().await;
1615 }
1616
1617 #[tokio::test]
1620 async fn a_request_priced_request_is_booked_half_the_latency_after_it_was_made() {
1621 let (request_tx, request_rx) = mpsc::unbounded_channel();
1622 let (event_tx, _events) = broadcast::channel(16);
1623 let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1624 config.latency_ms = 100;
1625 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1626 let driver = tokio::spawn(exchange.run());
1627
1628 let made = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap();
1629 let (open_tx, open_rx) = oneshot::channel();
1630 request_tx
1631 .send(MockExchangeRequest::open_order(
1632 made,
1633 open_tx,
1634 buy_request("0.01", market_prices("50000")),
1635 ))
1636 .unwrap();
1637 open_rx.await.expect("the open is answered");
1638
1639 let booked = made + TimeDelta::milliseconds(50);
1640 let (trades_tx, trades_rx) = oneshot::channel();
1641 request_tx
1642 .send(MockExchangeRequest::fetch_trades(
1643 made, trades_tx, made, booked,
1644 ))
1645 .unwrap();
1646 let trades = trades_rx.await.expect("the fetch is answered");
1647 assert_eq!(
1648 trades
1649 .iter()
1650 .map(|trade| trade.time_exchange)
1651 .collect::<Vec<_>>(),
1652 vec![booked],
1653 "booked at half the latency, and a bound at that instant includes it"
1654 );
1655 drop(request_tx);
1656 driver.await.unwrap();
1657 }
1658
1659 #[tokio::test(start_paused = true)]
1661 async fn a_paper_request_in_flight_is_answered_after_the_request_channel_closes() {
1662 let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1663 config.latency_ms = 100;
1664 let paper = Paper::start(config);
1665 paper.observe(priced("50000"));
1666
1667 let (response_tx, response_rx) = oneshot::channel();
1668 paper
1669 .request_tx
1670 .send(MockExchangeRequest::open_order(
1671 Utc::now(),
1672 response_tx,
1673 buy_request("0.01", None),
1674 ))
1675 .unwrap();
1676 drop(paper.request_tx);
1677
1678 let response = response_rx.await.expect("the open is still answered");
1679 assert!(matches!(response.state, OrderState::Inactive(_)));
1680 paper.driver.await.unwrap();
1681 }
1682
1683 #[tokio::test]
1685 async fn a_driver_debug_names_the_driver() {
1686 let (_request_tx, request_rx) = mpsc::unbounded_channel();
1687 let (event_tx, _events) = broadcast::channel(16);
1688 let config = spot_config("0", "10000000", FeeModelConfig::default());
1689 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1690 assert!(format!("{exchange:?}").contains("MockExchange"));
1691 }
1692
1693 #[tokio::test(start_paused = true)]
1696 async fn a_paper_venue_refuses_a_market_order_before_the_first_observation_and_cancels_a_rest()
1697 {
1698 let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1699 config.latency_ms = 100;
1700 let paper = Paper::start(config);
1701
1702 let refused = paper.open(buy_request("0.01", None)).await;
1703 assert!(
1704 matches!(refused.state, OrderState::Inactive(_)),
1705 "nothing to price a market order from: {refused:?}"
1706 );
1707
1708 let rest = limit_request("rest", Side::Buy, "1", "49000", gtc());
1709 let key = rest.key.clone();
1710 let resting = paper.open(rest).await;
1711 assert!(matches!(resting.state, OrderState::Active(_)));
1712
1713 let (response_tx, response_rx) = oneshot::channel();
1714 paper
1715 .request_tx
1716 .send(MockExchangeRequest::cancel_order(
1717 Utc::now(),
1718 response_tx,
1719 OrderEvent {
1720 key,
1721 state: RequestCancel { id: None },
1722 },
1723 ))
1724 .unwrap();
1725 let cancelled = response_rx.await.expect("the cancel is answered");
1726 assert!(cancelled.state.is_ok(), "{cancelled:?}");
1727 paper.stop().await;
1728 }
1729
1730 #[tokio::test(start_paused = true)]
1732 async fn a_paper_deadline_beyond_the_longest_sleep_is_retired_on_time() {
1733 let config = spot_config("0", "100000", FeeModelConfig::default());
1734 let mut paper = Paper::start(config);
1735 paper.observe(priced("50000"));
1736 let expiry = paper.start + TimeDelta::days(3 * 365);
1737 let started = tokio::time::Instant::now();
1738
1739 paper
1740 .open(limit_request(
1741 "far",
1742 Side::Buy,
1743 "1",
1744 "49000",
1745 crate::order::TimeInForce::GoodTillDate { expiry },
1746 ))
1747 .await;
1748
1749 let expired = loop {
1750 let event = paper
1751 .events
1752 .recv()
1753 .await
1754 .expect("the account channel is open");
1755 if let AccountEventKind::OrderSnapshot(order) = event.kind
1756 && let OrderState::Inactive(InactiveOrderState::Expired(expired)) = order.0.state
1757 {
1758 break expired;
1759 }
1760 };
1761 assert_eq!(expired.time_exchange, expiry);
1762 assert!(started.elapsed() >= std::time::Duration::from_secs(3 * 365 * 24 * 60 * 60));
1763 paper.stop().await;
1764 }
1765}