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::FetchBorrowCapacity {
385 response_tx,
386 instrument,
387 side,
388 } => {
389 emit.respond(
390 Vec::new(),
391 response_tx,
392 self.venue.borrow_capacity(&instrument, side),
393 );
394 }
395 MockExchangeRequestKind::FetchTrades {
396 response_tx,
397 start,
398 end,
399 } => {
400 let mut trades = self.venue.trades(start);
401 trades.retain(|trade| trade.time_exchange <= end);
402 emit.respond(Vec::new(), response_tx, trades);
403 }
404 MockExchangeRequestKind::CancelOrder {
405 response_tx,
406 request,
407 } => {
408 let outcome = self.venue.cancel_order(request);
411 emit.respond(outcome.events, response_tx, outcome.response);
412 }
413 MockExchangeRequestKind::OpenOrder {
414 response_tx,
415 request,
416 } => {
417 let outcome = self.venue.open_order(request);
418 emit.respond(outcome.events, response_tx, outcome.response);
419 }
420 }
421 }
422
423 #[must_use]
428 fn advance_venue_time(&mut self, time_request: DateTime<Utc>) -> Vec<UnindexedAccountEvent> {
429 let client_to_exchange_latency = self.latency_ms / 2;
430
431 let time_exchange = time_request
432 .checked_add_signed(TimeDelta::milliseconds(client_to_exchange_latency as i64))
433 .unwrap_or(time_request);
434
435 self.venue.advance_time(time_exchange)
436 }
437
438 async fn emit_queued_output(
445 mut emit_rx: mpsc::UnboundedReceiver<PendingEmission>,
446 event_tx: broadcast::Sender<UnindexedAccountEvent>,
447 exchange: ExchangeId,
448 ) {
449 while let Some(emission) = emit_rx.recv().await {
450 tokio::time::sleep_until(emission.ready_at).await;
451
452 for event in emission.events {
454 if event_tx.send(event).is_err() {
455 error!(
456 %exchange,
457 "MockExchange failed to send AccountEvent notification to client"
458 );
459 }
460 }
461
462 if let Some(respond) = emission.respond {
464 respond(exchange);
465 }
466 }
467 }
468
469 pub fn account_stream(&self) -> BoxStream<'static, UnindexedAccountEvent> {
470 futures::StreamExt::boxed(BroadcastStream::new(self.event_tx.subscribe()).map_while(
471 |result| match result {
472 Ok(event) => Some(event),
473 Err(error) => {
474 error!(
475 ?error,
476 "MockExchange Broadcast AccountStream lagged - terminating"
477 );
478 None
479 }
480 },
481 ))
482 }
483}
484
485async fn sleep_until(at: Option<tokio::time::Instant>) {
488 match at {
489 Some(at) => tokio::time::sleep_until(at).await,
490 None => std::future::pending().await,
491 }
492}
493
494struct MonotonicClock {
496 clock: MockExchangeClock,
497 latest: Option<DateTime<Utc>>,
498}
499
500impl MonotonicClock {
501 fn new(clock: MockExchangeClock) -> Self {
502 Self {
503 clock,
504 latest: None,
505 }
506 }
507
508 fn now(&mut self) -> DateTime<Utc> {
510 let now = (self.clock)();
511 self.raise_to(now)
512 }
513
514 fn peek(&self) -> DateTime<Utc> {
516 let now = (self.clock)();
517 self.latest.map_or(now, |latest| latest.max(now))
518 }
519
520 fn raise_to(&mut self, time: DateTime<Utc>) -> DateTime<Utc> {
522 let latest = self.latest.map_or(time, |latest| latest.max(time));
523 self.latest = Some(latest);
524 latest
525 }
526}
527
528struct Emit {
531 tx: mpsc::UnboundedSender<PendingEmission>,
532 exchange: ExchangeId,
533 delay: Duration,
535}
536
537impl Emit {
538 fn new(
539 tx: mpsc::UnboundedSender<PendingEmission>,
540 exchange: ExchangeId,
541 delay_ms: u64,
542 ) -> Self {
543 Self {
544 tx,
545 exchange,
546 delay: Duration::from_millis(delay_ms),
547 }
548 }
549
550 fn respond<Response>(
552 &self,
553 events: Vec<UnindexedAccountEvent>,
554 response_tx: oneshot::Sender<Response>,
555 response: Response,
556 ) where
557 Response: Send + 'static,
558 {
559 let respond: Respond = Box::new(move |exchange| {
560 if response_tx.send(response).is_err() {
561 error!(
562 %exchange,
563 kind = std::any::type_name::<Response>(),
564 "MockExchange failed to send oneshot response to client"
565 );
566 }
567 });
568 self.queue(events, Some(respond));
569 }
570
571 fn queue(&self, events: Vec<UnindexedAccountEvent>, respond: Option<Respond>) {
587 if events.is_empty() && respond.is_none() {
588 return;
589 }
590
591 let ready_at = tokio::time::Instant::now() + self.delay;
595
596 if self
599 .tx
600 .send(PendingEmission {
601 ready_at,
602 events,
603 respond,
604 })
605 .is_err()
606 {
607 error!(
608 exchange = %self.exchange,
609 "MockExchange could not queue venue output: the emitter has stopped"
610 );
611 }
612 }
613}
614
615type Respond = Box<dyn FnOnce(ExchangeId) + Send>;
617
618struct PendingEmission {
628 ready_at: tokio::time::Instant,
630 events: Vec<UnindexedAccountEvent>,
632 respond: Option<Respond>,
635}
636
637#[cfg(test)]
643#[allow(clippy::unwrap_used, clippy::expect_used)] pub(crate) mod fixtures {
645 use crate::{
646 InstrumentAccountSnapshot, UnindexedAccountSnapshot,
647 balance::{AssetBalance, Balance},
648 client::mock::MockExecutionConfig,
649 fee::{FeeModelConfig, SpotFeeAsset},
650 fill::SimFillConfig,
651 market::MarketSnapshot,
652 order::{
653 Order, OrderEvent, OrderKey, OrderKind, TimeInForce, UnindexedOrder,
654 id::{ClientOrderId, OrderId, StrategyId, VenueOrderId},
655 request::{OrderRequestOpen, RequestOpen},
656 state::{Open, OrderState},
657 },
658 position::PositionReport,
659 };
660 use chrono::{DateTime, Utc};
661 use fnv::FnvHashMap;
662 use rust_decimal::Decimal;
663 use rustrade_instrument::{
664 Side, Underlying,
665 asset::name::AssetNameExchange,
666 exchange::ExchangeId,
667 instrument::{
668 Instrument,
669 kind::InstrumentKind,
670 name::{InstrumentNameExchange, InstrumentNameInternal},
671 quote::InstrumentQuoteAsset,
672 },
673 };
674
675 pub(super) const EXCHANGE: ExchangeId = ExchangeId::BinanceSpot;
676
677 pub(super) fn d(s: &str) -> Decimal {
678 s.parse().unwrap()
679 }
680
681 pub(super) fn base() -> AssetNameExchange {
682 AssetNameExchange::new("BTC")
683 }
684
685 pub(super) fn quote() -> AssetNameExchange {
686 AssetNameExchange::new("USDT")
687 }
688
689 pub(super) fn instrument_name() -> InstrumentNameExchange {
690 InstrumentNameExchange::new("BTCUSDT")
691 }
692
693 pub(super) fn funded(
695 asset: AssetNameExchange,
696 amount: Decimal,
697 ) -> AssetBalance<AssetNameExchange> {
698 AssetBalance {
699 asset,
700 balance: Balance::new(amount, amount),
701 time_exchange: Utc::now(),
702 }
703 }
704
705 pub(super) fn config_from_balances(
707 balances: Vec<AssetBalance<AssetNameExchange>>,
708 fee_model: FeeModelConfig,
709 ) -> MockExecutionConfig {
710 config_from_balances_with_fill(balances, fee_model, SimFillConfig::default())
711 }
712
713 pub(super) fn config_from_balances_with_fill(
714 balances: Vec<AssetBalance<AssetNameExchange>>,
715 fee_model: FeeModelConfig,
716 fill_model: SimFillConfig,
717 ) -> MockExecutionConfig {
718 MockExecutionConfig::new(
719 EXCHANGE,
720 UnindexedAccountSnapshot {
721 exchange: EXCHANGE,
722 balances,
723 instruments: vec![],
724 },
725 0, fee_model,
727 fill_model,
728 SpotFeeAsset::default(),
729 crate::slippage::SlippageConfig::default(),
730 crate::fill::MarketOrderTiming::default(),
731 crate::fill::StaleMarketPolicy::default(),
732 None,
733 None,
734 )
735 }
736
737 pub(super) fn spot_config_holding(
744 btc: &str,
745 usdt: &str,
746 order: UnindexedOrder,
747 ) -> MockExecutionConfig {
748 MockExecutionConfig::new(
749 EXCHANGE,
750 UnindexedAccountSnapshot {
751 exchange: EXCHANGE,
752 balances: vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
753 instruments: vec![InstrumentAccountSnapshot {
754 instrument: instrument_name(),
755 orders: vec![order],
756 orders_complete: true,
757 position: PositionReport::Unreported,
758 isolated: None,
759 }],
760 },
761 0, FeeModelConfig::default(),
763 SimFillConfig::default(),
764 SpotFeeAsset::default(),
765 crate::slippage::SlippageConfig::default(),
766 crate::fill::MarketOrderTiming::default(),
767 crate::fill::StaleMarketPolicy::default(),
768 None,
769 None,
770 )
771 }
772
773 pub(super) fn seeded_gtd(
775 cid: &str,
776 price: &str,
777 opened: DateTime<Utc>,
778 expiry: DateTime<Utc>,
779 ) -> UnindexedOrder {
780 Order {
781 key: OrderKey {
782 exchange: EXCHANGE,
783 instrument: instrument_name(),
784 strategy: StrategyId::new("test"),
785 cid: ClientOrderId::new(cid),
786 },
787 side: Side::Buy,
788 price: Some(d(price)),
789 quantity: d("0.01"),
790 kind: OrderKind::Limit,
791 time_in_force: TimeInForce::GoodTillDate { expiry },
792 state: OrderState::active(Open::new(
793 VenueOrderId::Assigned(OrderId::new(cid)),
794 opened,
795 rust_decimal::Decimal::ZERO,
796 )),
797 }
798 }
799
800 pub(super) fn spot_config(
802 btc: &str,
803 usdt: &str,
804 fee_model: FeeModelConfig,
805 ) -> MockExecutionConfig {
806 config_from_balances(
807 vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
808 fee_model,
809 )
810 }
811
812 pub(super) fn spot_config_with_fill(
813 btc: &str,
814 usdt: &str,
815 fee_model: FeeModelConfig,
816 fill_model: SimFillConfig,
817 ) -> MockExecutionConfig {
818 config_from_balances_with_fill(
819 vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
820 fee_model,
821 fill_model,
822 )
823 }
824
825 pub(super) fn instruments_of(
826 instrument: Instrument<ExchangeId, AssetNameExchange>,
827 ) -> FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
828 let mut instruments = FnvHashMap::default();
829 instruments.insert(instrument.name_exchange.clone(), instrument);
830 instruments
831 }
832
833 pub(super) fn spot_instruments()
834 -> FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
835 instruments_of(Instrument {
836 exchange: EXCHANGE,
837 name_internal: InstrumentNameInternal::new("btcusdt"),
838 name_exchange: instrument_name(),
839 underlying: Underlying {
840 base: base(),
841 quote: quote(),
842 },
843 quote: InstrumentQuoteAsset::UnderlyingQuote,
844 kind: InstrumentKind::Spot,
845 spec: None,
846 data_venue: None,
847 })
848 }
849
850 pub(super) fn request(
856 instrument: InstrumentNameExchange,
857 side: Side,
858 quantity: &str,
859 market: Option<MarketSnapshot>,
860 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
861 OrderEvent {
862 key: OrderKey {
863 exchange: EXCHANGE,
864 instrument,
865 strategy: StrategyId::new("test"),
866 cid: ClientOrderId::new("test-cid"),
867 },
868 state: RequestOpen {
869 market,
870 ..RequestOpen::new(
871 side,
872 None, d(quantity),
874 OrderKind::Market,
875 TimeInForce::ImmediateOrCancel,
876 )
877 },
878 }
879 }
880
881 pub(super) fn with_effect(
883 mut request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
884 effect: crate::margin::MarginEffect,
885 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
886 request.state = request.state.with_margin_effect(effect);
887 request
888 }
889
890 pub(super) fn buy_request(
891 quantity: &str,
892 market: Option<MarketSnapshot>,
893 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
894 request(instrument_name(), Side::Buy, quantity, market)
895 }
896
897 pub(super) fn sell_request(
898 quantity: &str,
899 market: Option<MarketSnapshot>,
900 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
901 request(instrument_name(), Side::Sell, quantity, market)
902 }
903
904 pub(super) fn gtc() -> TimeInForce {
906 TimeInForce::GoodUntilCancelled { post_only: false }
907 }
908
909 pub(super) fn limit_request(
914 cid: &str,
915 side: Side,
916 quantity: &str,
917 price: &str,
918 time_in_force: TimeInForce,
919 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
920 OrderEvent {
921 key: OrderKey {
922 exchange: EXCHANGE,
923 instrument: instrument_name(),
924 strategy: StrategyId::new("test"),
925 cid: ClientOrderId::new(cid),
926 },
927 state: RequestOpen::new(
928 side,
929 Some(d(price)),
930 d(quantity),
931 OrderKind::Limit,
932 time_in_force,
933 ),
934 }
935 }
936
937 pub(super) fn book(best_bid: &str, best_ask: &str) -> MarketSnapshot {
939 MarketSnapshot {
940 best_bid: Some(d(best_bid)),
941 best_ask: Some(d(best_ask)),
942 last_price: None,
943 }
944 }
945
946 pub(super) fn traded(last_price: &str) -> MarketSnapshot {
948 MarketSnapshot {
949 best_bid: None,
950 best_ask: None,
951 last_price: Some(d(last_price)),
952 }
953 }
954
955 pub(super) fn market_prices(price: &str) -> Option<MarketSnapshot> {
957 let p = Some(d(price));
958 Some(MarketSnapshot {
959 best_bid: p,
960 best_ask: p,
961 last_price: p,
962 })
963 }
964}
965
966#[cfg(test)]
967#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
969 use super::{fixtures::*, *};
970 use crate::market::MarketSnapshot;
971 use crate::{
972 AccountEventKind,
973 fee::FeeModelConfig,
974 order::{
975 OrderEvent, OrderKey,
976 id::ClientOrderId,
977 request::RequestCancel,
978 state::{InactiveOrderState, OrderState},
979 },
980 };
981 use rust_decimal::Decimal;
982 use rustrade_instrument::Side;
983
984 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1000 async fn open_order_emissions_are_ordered_across_fills() {
1001 const FILLS: usize = 8;
1002 const DEBIT_PER_FILL: &str = "500";
1003
1004 let usdt_start = "10000000";
1005
1006 let (request_tx, request_rx) = mpsc::unbounded_channel();
1007 let (event_tx, mut events) = broadcast::channel(64);
1008
1009 let exchange = MockExchange::new(
1010 spot_config("100", usdt_start, FeeModelConfig::default()),
1011 request_rx,
1012 event_tx,
1013 spot_instruments(),
1014 );
1015
1016 let driver = tokio::spawn(exchange.run());
1017
1018 let mut responses = Vec::with_capacity(FILLS);
1019 for nth in 0..FILLS {
1020 let (response_tx, response_rx) = oneshot::channel();
1021 let mut request = buy_request("0.01", market_prices("50000"));
1022 request.key.cid = ClientOrderId::new(format!("cid-{nth}"));
1023
1024 request_tx
1025 .send(MockExchangeRequest::open_order(
1026 Utc::now(),
1027 response_tx,
1028 request,
1029 ))
1030 .unwrap();
1031 responses.push(response_rx);
1032 }
1033
1034 drop(request_tx);
1036 driver.await.unwrap();
1037
1038 for response in responses {
1039 let response = response.await.expect("every open is answered");
1040 assert!(
1041 matches!(
1042 response.state,
1043 OrderState::Active(_) | OrderState::Inactive(_)
1044 ),
1045 "the account is funded and the instrument priced, so every open fills: {response:?}"
1046 );
1047 }
1048
1049 let balances = std::iter::from_fn(|| events.try_recv().ok())
1051 .filter_map(|event| match event.kind {
1052 AccountEventKind::BalanceSnapshot(snapshot) if snapshot.0.asset == quote() => {
1053 Some(snapshot.0.balance.total)
1054 }
1055 _ => None,
1056 })
1057 .collect::<Vec<_>>();
1058
1059 let expected = (1..=FILLS)
1061 .map(|nth| d(usdt_start) - d(DEBIT_PER_FILL) * Decimal::from(nth))
1062 .collect::<Vec<_>>();
1063
1064 assert_eq!(
1065 balances, expected,
1066 "balance snapshots must reach the client in the order the venue booked them"
1067 );
1068 }
1069
1070 #[tokio::test]
1072 async fn the_driver_fills_at_the_configured_slippage() {
1073 let (request_tx, request_rx) = mpsc::unbounded_channel();
1074 let (event_tx, mut events) = broadcast::channel(16);
1075 let config = MockExecutionConfig {
1076 slippage: crate::slippage::SlippageConfig::fixed(d("0.01")).unwrap(),
1077 ..spot_config("100", "10000000", FeeModelConfig::default())
1078 };
1079 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1080 let driver = tokio::spawn(exchange.run());
1081
1082 let (response_tx, response_rx) = oneshot::channel();
1083 request_tx
1084 .send(MockExchangeRequest::open_order(
1085 Utc::now(),
1086 response_tx,
1087 buy_request("0.01", market_prices("50000")),
1088 ))
1089 .unwrap();
1090 response_rx.await.expect("the open is answered");
1091 drop(request_tx);
1092 driver.await.unwrap();
1093
1094 let prices = std::iter::from_fn(|| events.try_recv().ok())
1095 .filter_map(|event| match event.kind {
1096 AccountEventKind::Trade(trade) => Some(trade.price),
1097 _ => None,
1098 })
1099 .collect::<Vec<_>>();
1100 assert_eq!(prices, vec![d("50500")], "50,000 bought 1% above");
1101 }
1102
1103 #[tokio::test(start_paused = true)]
1118 async fn a_swept_expiry_is_emitted_behind_the_fill_booked_before_it() {
1119 let opened = Utc::now();
1120 let expiry = opened + chrono::TimeDelta::seconds(30);
1121
1122 let (request_tx, request_rx) = mpsc::unbounded_channel();
1123 let (event_tx, mut events) = broadcast::channel(64);
1124
1125 let mut config =
1126 spot_config_holding("100", "10000000", seeded_gtd("gtd", "1", opened, expiry));
1127 config.latency_ms = 50;
1129
1130 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1131 let driver = tokio::spawn(exchange.run());
1132
1133 let (fill_tx, fill_rx) = oneshot::channel();
1135 request_tx
1136 .send(MockExchangeRequest::open_order(
1137 opened,
1138 fill_tx,
1139 buy_request("0.01", market_prices("50000")),
1140 ))
1141 .unwrap();
1142
1143 let (snapshot_tx, snapshot_rx) = oneshot::channel();
1146 request_tx
1147 .send(MockExchangeRequest::fetch_account_snapshot(
1148 expiry + chrono::TimeDelta::seconds(1),
1149 snapshot_tx,
1150 ))
1151 .unwrap();
1152
1153 drop(request_tx);
1154 driver.await.unwrap();
1155 fill_rx.await.expect("the open is answered");
1156 let _ = snapshot_rx.await;
1157
1158 let kinds = std::iter::from_fn(|| events.try_recv().ok())
1159 .map(|event| match event.kind {
1160 AccountEventKind::BalanceSnapshot(_) => "balance",
1161 AccountEventKind::Trade(_) => "trade",
1162 AccountEventKind::OrderSnapshot(_) => "order",
1163 _ => "other",
1164 })
1165 .collect::<Vec<_>>();
1166
1167 assert_eq!(
1168 kinds,
1169 ["balance", "balance", "trade", "order"],
1170 "the fill's events must all precede the sweep, which for a seeded order — held with \
1171 no reservation — owes only its terminal snapshot"
1172 );
1173 }
1174
1175 #[tokio::test]
1176 async fn a_cancel_request_is_answered_rather_than_dropped() {
1177 let (request_tx, request_rx) = mpsc::unbounded_channel();
1178 let (event_tx, _events) = broadcast::channel(16);
1179
1180 let exchange = MockExchange::new(
1181 spot_config("100", "10000", FeeModelConfig::default()),
1182 request_rx,
1183 event_tx,
1184 spot_instruments(),
1185 );
1186 let driver = tokio::spawn(exchange.run());
1187
1188 let (response_tx, response_rx) = oneshot::channel();
1189 let open = buy_request("1", market_prices("50000"));
1190 request_tx
1191 .send(MockExchangeRequest::cancel_order(
1192 Utc::now(),
1193 response_tx,
1194 OrderEvent {
1195 key: open.key,
1196 state: RequestCancel { id: None },
1197 },
1198 ))
1199 .unwrap();
1200
1201 let response = response_rx.await.expect("a cancel must be answered");
1202 assert!(
1203 response.state.is_err(),
1204 "this venue rests no orders, so a cancel must be rejected: {response:?}"
1205 );
1206
1207 drop(request_tx);
1208 driver.await.unwrap();
1209 }
1210
1211 #[tokio::test]
1214 async fn an_order_state_lookup_is_answered_from_the_ledger() {
1215 use crate::client::{OrderStatusClient, mock::MockExecution};
1216
1217 let (request_tx, request_rx) = mpsc::unbounded_channel();
1218 let (event_tx, event_rx) = broadcast::channel(16);
1219 let exchange = MockExchange::new(
1220 spot_config("100", "10000000", FeeModelConfig::default()),
1221 request_rx,
1222 event_tx,
1223 spot_instruments(),
1224 );
1225 let driver = tokio::spawn(exchange.run());
1226
1227 let open = buy_request("1", market_prices("50000"));
1228 let (response_tx, response_rx) = oneshot::channel();
1229 request_tx
1230 .send(MockExchangeRequest::open_order(
1231 Utc::now(),
1232 response_tx,
1233 open.clone(),
1234 ))
1235 .unwrap();
1236 let opened = response_rx.await.expect("an open must be answered");
1237 assert!(matches!(
1238 opened.state,
1239 OrderState::Inactive(InactiveOrderState::FullyFilled(_))
1240 ));
1241
1242 let client = MockExecution {
1243 mocked_exchange: EXCHANGE,
1244 clock: Utc::now,
1245 request_tx: request_tx.clone(),
1246 event_rx,
1247 };
1248 let unknown = OrderKey {
1249 cid: ClientOrderId::new("never-opened"),
1250 ..open.key.clone()
1251 };
1252 let ended = client
1253 .fetch_ended_orders(&[open.key.clone(), unknown])
1254 .await
1255 .expect("the venue is running");
1256
1257 let [order] = ended.as_slice() else {
1258 panic!("only the filled order is reported: {ended:?}");
1259 };
1260 assert_eq!(order.key, open.key);
1261 assert!(matches!(order.state, InactiveOrderState::FullyFilled(_)));
1262
1263 drop(client);
1264 drop(request_tx);
1265 driver.await.unwrap();
1266 }
1267
1268 async fn borrow_capacity_from(
1271 config: crate::client::mock::MockExecutionConfig,
1272 side: Side,
1273 ) -> Result<crate::BorrowCapacity, crate::error::UnindexedClientError> {
1274 use crate::client::{BorrowCapacityClient, mock::MockExecution};
1275
1276 let (request_tx, request_rx) = mpsc::unbounded_channel();
1277 let (event_tx, event_rx) = broadcast::channel(16);
1278 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1279 let driver = tokio::spawn(exchange.run());
1280
1281 let client = MockExecution {
1282 mocked_exchange: EXCHANGE,
1283 clock: Utc::now,
1284 request_tx,
1285 event_rx,
1286 };
1287 let capacity = client.fetch_borrow_capacity(&instrument_name(), side).await;
1288
1289 drop(client);
1290 driver.await.unwrap();
1291 capacity
1292 }
1293
1294 #[tokio::test]
1296 async fn a_borrow_capacity_query_is_answered_by_the_venue() {
1297 use crate::margin::{MarginEffect, SpotMarginConfig};
1298
1299 let config = crate::client::mock::MockExecutionConfig {
1300 spot_margin: Some(
1301 SpotMarginConfig::new(quote(), Decimal::from(3), MarginEffect::Borrow).unwrap(),
1302 ),
1303 ..spot_config("0", "10000", FeeModelConfig::default())
1304 };
1305
1306 let capacity = borrow_capacity_from(config, Side::Buy)
1307 .await
1308 .expect("a margin venue lends against BTCUSDT");
1309
1310 assert_eq!(
1311 capacity,
1312 crate::BorrowCapacity::new(quote()).with_borrowable_now(Decimal::from(20_000))
1313 );
1314 }
1315
1316 #[tokio::test]
1318 async fn a_borrow_capacity_the_venue_refuses_is_an_api_error() {
1319 let error = borrow_capacity_from(
1320 spot_config("0", "10000", FeeModelConfig::default()),
1321 Side::Sell,
1322 )
1323 .await
1324 .expect_err("a cash venue lends against nothing");
1325
1326 assert!(
1327 matches!(
1328 &error,
1329 crate::error::ClientError::Api(crate::error::ApiError::InstrumentInvalid(
1330 instrument,
1331 _,
1332 )) if *instrument == instrument_name()
1333 ),
1334 "{error:?}"
1335 );
1336 }
1337
1338 #[tokio::test(start_paused = true)]
1341 async fn a_fetch_is_answered_after_the_fill_booked_before_it() {
1342 let (request_tx, request_rx) = mpsc::unbounded_channel();
1343 let (event_tx, mut events) = broadcast::channel(64);
1344 let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1345 config.latency_ms = 50;
1346 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1347 let driver = tokio::spawn(exchange.run());
1348
1349 let (open_tx, open_rx) = oneshot::channel();
1350 request_tx
1351 .send(MockExchangeRequest::open_order(
1352 Utc::now(),
1353 open_tx,
1354 buy_request("0.01", market_prices("50000")),
1355 ))
1356 .unwrap();
1357 let (balances_tx, balances_rx) = oneshot::channel();
1358 request_tx
1359 .send(MockExchangeRequest::fetch_balances(
1360 Utc::now(),
1361 vec![quote()],
1362 balances_tx,
1363 ))
1364 .unwrap();
1365
1366 balances_rx.await.expect("the fetch is answered");
1367 let kinds = std::iter::from_fn(|| events.try_recv().ok())
1368 .map(|event| event_name(&event))
1369 .collect::<Vec<_>>();
1370 assert!(
1371 kinds.contains(&"trade"),
1372 "the fill booked before the fetch is emitted before the fetch is answered: {kinds:?}"
1373 );
1374
1375 open_rx.await.expect("the open is answered");
1376 drop(request_tx);
1377 driver.await.unwrap();
1378 }
1379
1380 #[test]
1382 #[should_panic(expected = "request-priced venue")]
1383 fn a_market_driven_driver_refuses_a_request_priced_venue() {
1384 let config = spot_config("0", "10000", FeeModelConfig::default());
1385 let (_request_tx, request_rx) = mpsc::unbounded_channel();
1386 let (event_tx, _events) = broadcast::channel(16);
1387 let _ = MockExchange::new_market_driven(
1388 SimulatedVenue::new(&config, spot_instruments()),
1389 0,
1390 request_rx,
1391 event_tx,
1392 futures::StreamExt::boxed(futures::stream::empty()),
1393 Arc::new(Utc::now),
1394 );
1395 }
1396
1397 fn event_name(event: &UnindexedAccountEvent) -> &'static str {
1398 match &event.kind {
1399 AccountEventKind::BalanceSnapshot(_) => "balance",
1400 AccountEventKind::Trade(_) => "trade",
1401 AccountEventKind::OrderSnapshot(_) => "order",
1402 AccountEventKind::StreamTerminated(_) => "terminated",
1403 _ => "other",
1404 }
1405 }
1406
1407 struct Paper {
1410 request_tx: mpsc::UnboundedSender<MockExchangeRequest>,
1411 market_tx: mpsc::UnboundedSender<MarketFeedEvent>,
1412 events: broadcast::Receiver<UnindexedAccountEvent>,
1413 driver: tokio::task::JoinHandle<()>,
1414 start: DateTime<Utc>,
1416 }
1417
1418 impl Paper {
1419 fn start(config: MockExecutionConfig) -> Self {
1421 let start = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap();
1422 let anchor = tokio::time::Instant::now();
1423 let clock: MockExchangeClock = Arc::new(move || {
1424 start + TimeDelta::from_std(anchor.elapsed()).expect("a test runs for less")
1425 });
1426 Self::start_on(config, clock, start)
1427 }
1428
1429 fn start_on(
1430 config: MockExecutionConfig,
1431 clock: MockExchangeClock,
1432 start: DateTime<Utc>,
1433 ) -> Self {
1434 let (request_tx, request_rx) = mpsc::unbounded_channel();
1435 let (event_tx, events) = broadcast::channel(256);
1436 let (market_tx, market_rx) = mpsc::unbounded_channel();
1437 let exchange = MockExchange::new_market_driven(
1438 SimulatedVenue::new_market_driven(&config, spot_instruments()),
1439 config.latency_ms,
1440 request_rx,
1441 event_tx,
1442 futures::StreamExt::boxed(tokio_stream::wrappers::UnboundedReceiverStream::new(
1443 market_rx,
1444 )),
1445 clock,
1446 );
1447 Self {
1448 request_tx,
1449 market_tx,
1450 events,
1451 driver: tokio::spawn(exchange.run()),
1452 start,
1453 }
1454 }
1455
1456 fn observe(&self, snapshot: MarketSnapshot) {
1457 self.market_tx
1458 .send(MarketFeedEvent::Observation {
1459 instrument: instrument_name(),
1460 observation: MarketObservation::new(snapshot),
1461 })
1462 .unwrap();
1463 }
1464
1465 async fn open(
1466 &self,
1467 request: crate::order::request::OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
1468 ) -> crate::order::Order<
1469 ExchangeId,
1470 InstrumentNameExchange,
1471 crate::order::state::UnindexedOrderState,
1472 > {
1473 let (response_tx, response_rx) = oneshot::channel();
1474 self.request_tx
1475 .send(MockExchangeRequest::open_order(
1476 Utc::now(),
1477 response_tx,
1478 request,
1479 ))
1480 .unwrap();
1481 response_rx.await.expect("an open is answered")
1482 }
1483
1484 async fn next_event(&mut self) -> UnindexedAccountEvent {
1486 tokio::time::timeout(std::time::Duration::from_secs(3600), self.events.recv())
1487 .await
1488 .expect("an account event arrives")
1489 .expect("the account channel is open")
1490 }
1491
1492 async fn until_trade(
1494 &mut self,
1495 ) -> crate::trade::Trade<AssetNameExchange, InstrumentNameExchange> {
1496 loop {
1497 if let AccountEventKind::Trade(trade) = self.next_event().await.kind {
1498 return trade;
1499 }
1500 }
1501 }
1502
1503 async fn stop(self) {
1504 drop(self.request_tx);
1505 self.driver.await.unwrap();
1506 }
1507 }
1508
1509 fn priced(price: &str) -> MarketSnapshot {
1510 market_prices(price).unwrap()
1511 }
1512
1513 #[tokio::test(start_paused = true)]
1517 async fn a_paper_limit_rests_then_fills_when_the_market_crosses_it() {
1518 let mut config = spot_config("0", "100000", FeeModelConfig::default());
1519 config.latency_ms = 100;
1520 let mut paper = Paper::start(config);
1521 paper.observe(priced("50000"));
1522
1523 let sent = tokio::time::Instant::now();
1524 let response = paper
1525 .open(limit_request("rest", Side::Buy, "1", "49000", gtc()))
1526 .await;
1527 assert!(
1528 matches!(response.state, OrderState::Active(_)),
1529 "a limit below the market rests: {response:?}"
1530 );
1531 assert_eq!(
1532 sent.elapsed(),
1533 std::time::Duration::from_millis(100),
1534 "half the latency to the venue, half back"
1535 );
1536
1537 paper.observe(priced("48000"));
1538 let trade = paper.until_trade().await;
1539 assert_eq!(trade.quantity, d("1"));
1540 assert!(
1541 trade.price <= d("49000"),
1542 "a buy limit never pays more: {trade:?}"
1543 );
1544 paper.stop().await;
1545 }
1546
1547 #[tokio::test(start_paused = true)]
1550 async fn a_paper_request_is_booked_against_the_market_when_it_arrives() {
1551 let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1552 config.latency_ms = 100;
1553 let mut paper = Paper::start(config);
1554 paper.observe(priced("50000"));
1555
1556 let (response_tx, response_rx) = oneshot::channel();
1557 paper
1558 .request_tx
1559 .send(MockExchangeRequest::open_order(
1560 Utc::now(),
1561 response_tx,
1562 buy_request("0.01", market_prices("1")),
1564 ))
1565 .unwrap();
1566 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1568 paper.observe(priced("51000"));
1569
1570 let trade = paper.until_trade().await;
1571 assert_eq!(
1572 trade.price,
1573 d("51000"),
1574 "booked against the market as it arrived"
1575 );
1576 assert_eq!(
1577 trade.time_exchange,
1578 paper.start + TimeDelta::milliseconds(50),
1579 "stamped when it reached the venue"
1580 );
1581 response_rx.await.expect("the open is answered");
1582 paper.stop().await;
1583 }
1584
1585 #[tokio::test(start_paused = true)]
1588 async fn a_paper_order_expires_at_its_deadline_in_a_quiet_market() {
1589 let config = spot_config("0", "100000", FeeModelConfig::default());
1590 let mut paper = Paper::start(config);
1591 paper.observe(priced("50000"));
1592 let expiry = paper.start + TimeDelta::seconds(10);
1593 let started = tokio::time::Instant::now();
1594
1595 let response = paper
1596 .open(limit_request(
1597 "gtd",
1598 Side::Buy,
1599 "1",
1600 "49000",
1601 crate::order::TimeInForce::GoodTillDate { expiry },
1602 ))
1603 .await;
1604 assert!(matches!(response.state, OrderState::Active(_)));
1605
1606 let expired = loop {
1607 let event = paper.next_event().await;
1608 if let AccountEventKind::OrderSnapshot(order) = event.kind
1609 && matches!(
1610 order.0.state,
1611 OrderState::Inactive(InactiveOrderState::Expired(_))
1612 )
1613 {
1614 break order.0;
1615 }
1616 };
1617 let OrderState::Inactive(InactiveOrderState::Expired(expired)) = expired.state else {
1618 unreachable!("matched above")
1619 };
1620 assert_eq!(expired.time_exchange, expiry, "retired at its own deadline");
1621 assert!(
1622 started.elapsed() >= std::time::Duration::from_secs(10),
1623 "and not before that time has passed: {:?}",
1624 started.elapsed()
1625 );
1626 paper.stop().await;
1627 }
1628
1629 #[tokio::test(start_paused = true)]
1632 async fn a_lagged_paper_feed_ends_the_account_stream_after_the_fills_before_it() {
1633 let config = spot_config("0", "10000000", FeeModelConfig::default());
1634 let mut paper = Paper::start(config);
1635 paper.observe(priced("50000"));
1636 paper.open(buy_request("0.01", None)).await;
1637 paper
1638 .market_tx
1639 .send(MarketFeedEvent::Lagged { missed: 3 })
1640 .unwrap();
1641
1642 let mut kinds = Vec::new();
1643 while kinds.last() != Some(&"terminated") {
1644 kinds.push(event_name(&paper.next_event().await));
1645 }
1646 assert!(
1647 kinds.contains(&"trade"),
1648 "the fill booked before the lag precedes the termination: {kinds:?}"
1649 );
1650
1651 let response = paper.open(buy_request("0.01", None)).await;
1652 assert!(
1653 matches!(
1654 response.state,
1655 OrderState::Inactive(InactiveOrderState::FullyFilled(_))
1656 ),
1657 "the venue keeps trading after the lag: {response:?}"
1658 );
1659 paper.stop().await;
1660 }
1661
1662 #[tokio::test(start_paused = true)]
1664 async fn a_paper_driver_keeps_trading_once_its_feed_ends() {
1665 let config = spot_config("0", "10000000", FeeModelConfig::default());
1666 let mut paper = Paper::start(config);
1667 paper.observe(priced("50000"));
1668 let (replacement, _) = mpsc::unbounded_channel();
1670 drop(std::mem::replace(&mut paper.market_tx, replacement));
1671
1672 paper.open(buy_request("0.01", None)).await;
1673 assert_eq!(paper.until_trade().await.price, d("50000"));
1674 paper.stop().await;
1675 }
1676
1677 #[tokio::test(start_paused = true)]
1679 async fn a_paper_clock_that_steps_back_does_not_move_the_venue_back() {
1680 let later = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap();
1681 let reading = Arc::new(std::sync::Mutex::new(later));
1682 let clock: MockExchangeClock = {
1683 let reading = Arc::clone(&reading);
1684 Arc::new(move || *reading.lock().unwrap())
1685 };
1686 let config = spot_config("0", "10000000", FeeModelConfig::default());
1687 let mut paper = Paper::start_on(config, clock, later);
1688 paper.observe(priced("50000"));
1689 tokio::time::sleep(std::time::Duration::from_millis(1)).await;
1691 *reading.lock().unwrap() = later - TimeDelta::seconds(100);
1692
1693 paper.open(buy_request("0.01", None)).await;
1694 assert_eq!(paper.until_trade().await.time_exchange, later);
1695 paper.stop().await;
1696 }
1697
1698 #[tokio::test]
1701 async fn a_request_priced_request_is_booked_half_the_latency_after_it_was_made() {
1702 let (request_tx, request_rx) = mpsc::unbounded_channel();
1703 let (event_tx, _events) = broadcast::channel(16);
1704 let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1705 config.latency_ms = 100;
1706 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1707 let driver = tokio::spawn(exchange.run());
1708
1709 let made = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap();
1710 let (open_tx, open_rx) = oneshot::channel();
1711 request_tx
1712 .send(MockExchangeRequest::open_order(
1713 made,
1714 open_tx,
1715 buy_request("0.01", market_prices("50000")),
1716 ))
1717 .unwrap();
1718 open_rx.await.expect("the open is answered");
1719
1720 let booked = made + TimeDelta::milliseconds(50);
1721 let (trades_tx, trades_rx) = oneshot::channel();
1722 request_tx
1723 .send(MockExchangeRequest::fetch_trades(
1724 made, trades_tx, made, booked,
1725 ))
1726 .unwrap();
1727 let trades = trades_rx.await.expect("the fetch is answered");
1728 assert_eq!(
1729 trades
1730 .iter()
1731 .map(|trade| trade.time_exchange)
1732 .collect::<Vec<_>>(),
1733 vec![booked],
1734 "booked at half the latency, and a bound at that instant includes it"
1735 );
1736 drop(request_tx);
1737 driver.await.unwrap();
1738 }
1739
1740 #[tokio::test(start_paused = true)]
1742 async fn a_paper_request_in_flight_is_answered_after_the_request_channel_closes() {
1743 let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1744 config.latency_ms = 100;
1745 let paper = Paper::start(config);
1746 paper.observe(priced("50000"));
1747
1748 let (response_tx, response_rx) = oneshot::channel();
1749 paper
1750 .request_tx
1751 .send(MockExchangeRequest::open_order(
1752 Utc::now(),
1753 response_tx,
1754 buy_request("0.01", None),
1755 ))
1756 .unwrap();
1757 drop(paper.request_tx);
1758
1759 let response = response_rx.await.expect("the open is still answered");
1760 assert!(matches!(response.state, OrderState::Inactive(_)));
1761 paper.driver.await.unwrap();
1762 }
1763
1764 #[tokio::test]
1766 async fn a_driver_debug_names_the_driver() {
1767 let (_request_tx, request_rx) = mpsc::unbounded_channel();
1768 let (event_tx, _events) = broadcast::channel(16);
1769 let config = spot_config("0", "10000000", FeeModelConfig::default());
1770 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1771 assert!(format!("{exchange:?}").contains("MockExchange"));
1772 }
1773
1774 #[tokio::test(start_paused = true)]
1777 async fn a_paper_venue_refuses_a_market_order_before_the_first_observation_and_cancels_a_rest()
1778 {
1779 let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1780 config.latency_ms = 100;
1781 let paper = Paper::start(config);
1782
1783 let refused = paper.open(buy_request("0.01", None)).await;
1784 assert!(
1785 matches!(refused.state, OrderState::Inactive(_)),
1786 "nothing to price a market order from: {refused:?}"
1787 );
1788
1789 let rest = limit_request("rest", Side::Buy, "1", "49000", gtc());
1790 let key = rest.key.clone();
1791 let resting = paper.open(rest).await;
1792 assert!(matches!(resting.state, OrderState::Active(_)));
1793
1794 let (response_tx, response_rx) = oneshot::channel();
1795 paper
1796 .request_tx
1797 .send(MockExchangeRequest::cancel_order(
1798 Utc::now(),
1799 response_tx,
1800 OrderEvent {
1801 key,
1802 state: RequestCancel { id: None },
1803 },
1804 ))
1805 .unwrap();
1806 let cancelled = response_rx.await.expect("the cancel is answered");
1807 assert!(cancelled.state.is_ok(), "{cancelled:?}");
1808 paper.stop().await;
1809 }
1810
1811 #[tokio::test(start_paused = true)]
1813 async fn a_paper_deadline_beyond_the_longest_sleep_is_retired_on_time() {
1814 let config = spot_config("0", "100000", FeeModelConfig::default());
1815 let mut paper = Paper::start(config);
1816 paper.observe(priced("50000"));
1817 let expiry = paper.start + TimeDelta::days(3 * 365);
1818 let started = tokio::time::Instant::now();
1819
1820 paper
1821 .open(limit_request(
1822 "far",
1823 Side::Buy,
1824 "1",
1825 "49000",
1826 crate::order::TimeInForce::GoodTillDate { expiry },
1827 ))
1828 .await;
1829
1830 let expired = loop {
1831 let event = paper
1832 .events
1833 .recv()
1834 .await
1835 .expect("the account channel is open");
1836 if let AccountEventKind::OrderSnapshot(order) = event.kind
1837 && let OrderState::Inactive(InactiveOrderState::Expired(expired)) = order.0.state
1838 {
1839 break expired;
1840 }
1841 };
1842 assert_eq!(expired.time_exchange, expiry);
1843 assert!(started.elapsed() >= std::time::Duration::from_secs(3 * 365 * 24 * 60 * 60));
1844 paper.stop().await;
1845 }
1846}