1use crate::{
2 AccountEventKind, 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 Stale {
53 instruments: Vec<InstrumentNameExchange>,
54 },
55 Lagged { missed: u64 },
60}
61
62pub struct MockExchange {
81 pub venue: SimulatedVenue,
83 pub latency_ms: u64,
85 pub request_rx: mpsc::UnboundedReceiver<MockExchangeRequest>,
86 pub event_tx: broadcast::Sender<UnindexedAccountEvent>,
87 market: Option<MarketDrive>,
89}
90
91impl Debug for MockExchange {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 f.debug_struct("MockExchange")
95 .field("venue", &self.venue)
96 .field("latency_ms", &self.latency_ms)
97 .field("request_rx", &self.request_rx)
98 .field("event_tx", &self.event_tx)
99 .field("market_driven", &self.market.is_some())
100 .finish()
101 }
102}
103
104struct MarketDrive {
106 feed: BoxStream<'static, MarketFeedEvent>,
107 clock: MockExchangeClock,
108}
109
110impl MockExchange {
111 pub fn new(
116 config: MockExecutionConfig,
117 request_rx: mpsc::UnboundedReceiver<MockExchangeRequest>,
118 event_tx: broadcast::Sender<UnindexedAccountEvent>,
119 instruments: FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>>,
120 ) -> Self {
121 Self {
122 venue: SimulatedVenue::new(&config, instruments),
123 latency_ms: config.latency_ms,
124 request_rx,
125 event_tx,
126 market: None,
127 }
128 }
129
130 pub fn new_market_driven(
173 venue: SimulatedVenue,
174 latency_ms: u64,
175 request_rx: mpsc::UnboundedReceiver<MockExchangeRequest>,
176 event_tx: broadcast::Sender<UnindexedAccountEvent>,
177 feed: BoxStream<'static, MarketFeedEvent>,
178 clock: MockExchangeClock,
179 ) -> Self {
180 assert_eq!(
181 venue.regime(),
182 VenueRegime::MarketDriven,
183 "MockExchange::new_market_driven given a request-priced venue: build it with \
184 SimulatedVenue::new_market_driven"
185 );
186 Self {
187 venue,
188 latency_ms,
189 request_rx,
190 event_tx,
191 market: Some(MarketDrive { feed, clock }),
192 }
193 }
194
195 pub async fn run(mut self) {
203 let (emit_tx, emit_rx) = mpsc::unbounded_channel();
204 let emitter = tokio::spawn(Self::emit_queued_output(
205 emit_rx,
206 self.event_tx.clone(),
207 self.venue.exchange,
208 ));
209
210 match self.market.take() {
211 None => {
212 let emit = Emit::new(emit_tx, self.venue.exchange, self.latency_ms);
213 self.serve_request_priced(&emit).await;
214 }
215 Some(drive) => {
216 let emit = Emit::new(emit_tx, self.venue.exchange, self.latency_ms / 2);
217 self.serve_market_driven(drive, &emit).await;
218 }
219 }
220
221 if let Err(error) = emitter.await {
225 error!(
226 exchange = %self.venue.exchange,
227 %error,
228 "MockExchange emitter task did not shut down cleanly; queued fills may be lost"
229 );
230 }
231
232 info!(exchange = %self.venue.exchange, "MockExchange shutting down");
233 }
234
235 async fn serve_request_priced(&mut self, emit: &Emit) {
237 while let Some(request) = self.request_rx.recv().await {
238 let expiries = self.advance_venue_time(request.time_request);
242 emit.queue(expiries, None);
243 self.book(request.kind, emit);
244 }
245 }
246
247 async fn serve_market_driven(&mut self, drive: MarketDrive, emit: &Emit) {
253 let MarketDrive { mut feed, clock } = drive;
254 let mut clock = MonotonicClock::new(clock);
255 let to_venue = Duration::from_millis(self.latency_ms / 2);
256 let exchange = self.venue.exchange;
257
258 let mut arriving = VecDeque::<(tokio::time::Instant, MockExchangeRequestKind)>::new();
261 let mut feed_open = true;
262 let mut requests_open = true;
263 let mut stuck_deadline = None;
266
267 let started = self.venue.advance_time(clock.now());
268 emit.queue(started, None);
269
270 while requests_open || !arriving.is_empty() {
271 let arrives_at = arriving.front().map(|(at, _)| *at);
272 let deadline = self
273 .venue
274 .next_deadline()
275 .filter(|deadline| Some(*deadline) != stuck_deadline);
276 let mut capped = false;
279 let deadline_at = deadline.map(|deadline| {
280 let wait = (deadline - clock.peek()).to_std().unwrap_or(Duration::ZERO);
282 capped = wait > MAX_DEADLINE_WAIT;
283 tokio::time::Instant::now() + wait.min(MAX_DEADLINE_WAIT)
284 });
285
286 tokio::select! {
287 biased;
288
289 event = futures::StreamExt::next(&mut feed), if feed_open => match event {
290 Some(MarketFeedEvent::Observation { instrument, observation }) => {
291 let events = self.venue.apply_market(&instrument, observation, clock.now());
292 emit.queue(events, None);
293 }
294 Some(MarketFeedEvent::Stale { instruments }) => {
295 let now = clock.now();
296 for instrument in &instruments {
297 self.venue.mark_market_stale(instrument, now);
298 }
299 }
300 Some(MarketFeedEvent::Lagged { missed }) => {
301 error!(
302 %exchange,
303 missed,
304 "MockExchange market feed lagged: the venue missed market \
305 observations, so resting orders may have missed fills; ending the \
306 account stream"
307 );
308 emit.queue(
309 vec![UnindexedAccountEvent::stream_terminated(
310 exchange,
311 StreamTerminationReason::Error(format!(
312 "mock exchange market feed lagged: {missed} observations missed"
313 )),
314 )],
315 None,
316 );
317 }
318 None => {
319 feed_open = false;
320 warn!(
321 %exchange,
322 "MockExchange market feed ended: serving requests on the last market \
323 seen, marked stale"
324 );
325 let now = clock.now();
326 let instruments =
327 self.venue.instruments().keys().cloned().collect::<Vec<_>>();
328 for instrument in &instruments {
329 self.venue.mark_market_stale(instrument, now);
330 }
331 }
332 },
333
334 () = sleep_until(arrives_at), if arrives_at.is_some() => {
335 if let Some((_, kind)) = arriving.pop_front() {
336 let expiries = self.venue.advance_time(clock.now());
337 emit.queue(expiries, None);
338 self.book(kind, emit);
339 }
340 },
341
342 request = self.request_rx.recv(), if requests_open => match request {
343 Some(request) => {
344 arriving.push_back((tokio::time::Instant::now() + to_venue, request.kind));
345 }
346 None => requests_open = false,
347 },
348
349 () = sleep_until(deadline_at), if deadline_at.is_some() => {
350 if let Some(deadline) = deadline {
353 if capped {
355 continue;
356 }
357 let now = clock.raise_to(deadline);
358 let events = self.venue.advance_time(now);
359 emit.queue(events, None);
360 if self.venue.next_deadline() == Some(deadline) {
361 error!(
362 %exchange,
363 %deadline,
364 "MockExchange venue still reports a deadline it was moved to; \
365 no longer waking for it"
366 );
367 stuck_deadline = Some(deadline);
368 }
369 }
370 },
371 }
372 }
373 }
374
375 fn book(&mut self, kind: MockExchangeRequestKind, emit: &Emit) {
380 match kind {
381 MockExchangeRequestKind::FetchAccountSnapshot { response_tx } => {
382 emit.respond(Vec::new(), response_tx, self.venue.account_snapshot());
383 }
384 MockExchangeRequestKind::FetchBalances {
385 response_tx,
386 assets,
387 } => {
388 emit.respond(Vec::new(), response_tx, self.venue.balances(&assets));
389 }
390 MockExchangeRequestKind::FetchOrdersOpen {
391 response_tx,
392 instruments,
393 } => {
394 emit.respond(
395 Vec::new(),
396 response_tx,
397 self.venue.orders_open(&instruments),
398 );
399 }
400 MockExchangeRequestKind::FetchOrdersEnded {
401 response_tx,
402 orders,
403 } => {
404 emit.respond(Vec::new(), response_tx, self.venue.orders_ended(&orders));
405 }
406 MockExchangeRequestKind::FetchBorrowCapacity {
407 response_tx,
408 instrument,
409 side,
410 } => {
411 emit.respond(
412 Vec::new(),
413 response_tx,
414 self.venue.borrow_capacity(&instrument, side),
415 );
416 }
417 MockExchangeRequestKind::FetchTrades {
418 response_tx,
419 start,
420 end,
421 } => {
422 let mut trades = self.venue.trades(start);
423 trades.retain(|trade| trade.time_exchange <= end);
424 emit.respond(Vec::new(), response_tx, trades);
425 }
426 MockExchangeRequestKind::Checkpoint { response_tx } => {
427 let checkpoint = self.venue.checkpoint();
428 let id = checkpoint.id;
429 let event = UnindexedAccountEvent::new(
430 self.venue.exchange,
431 AccountEventKind::VenueCheckpoint(Box::new(checkpoint)),
432 );
433 emit.respond(vec![event], response_tx, id);
434 }
435 MockExchangeRequestKind::CancelOrder {
436 response_tx,
437 request,
438 } => {
439 let outcome = self.venue.cancel_order(request);
442 emit.respond(outcome.events, response_tx, outcome.response);
443 }
444 MockExchangeRequestKind::OpenOrder {
445 response_tx,
446 request,
447 } => {
448 let outcome = self.venue.open_order(request);
449 emit.respond(outcome.events, response_tx, outcome.response);
450 }
451 }
452 }
453
454 #[must_use]
459 fn advance_venue_time(&mut self, time_request: DateTime<Utc>) -> Vec<UnindexedAccountEvent> {
460 let client_to_exchange_latency = self.latency_ms / 2;
461
462 let time_exchange = time_request
463 .checked_add_signed(TimeDelta::milliseconds(client_to_exchange_latency as i64))
464 .unwrap_or(time_request);
465
466 self.venue.advance_time(time_exchange)
467 }
468
469 async fn emit_queued_output(
476 mut emit_rx: mpsc::UnboundedReceiver<PendingEmission>,
477 event_tx: broadcast::Sender<UnindexedAccountEvent>,
478 exchange: ExchangeId,
479 ) {
480 while let Some(emission) = emit_rx.recv().await {
481 tokio::time::sleep_until(emission.ready_at).await;
482
483 for event in emission.events {
485 if event_tx.send(event).is_err() {
486 error!(
487 %exchange,
488 "MockExchange failed to send AccountEvent notification to client"
489 );
490 }
491 }
492
493 if let Some(respond) = emission.respond {
495 respond(exchange);
496 }
497 }
498 }
499
500 pub fn account_stream(&self) -> BoxStream<'static, UnindexedAccountEvent> {
501 futures::StreamExt::boxed(BroadcastStream::new(self.event_tx.subscribe()).map_while(
502 |result| match result {
503 Ok(event) => Some(event),
504 Err(error) => {
505 error!(
506 ?error,
507 "MockExchange Broadcast AccountStream lagged - terminating"
508 );
509 None
510 }
511 },
512 ))
513 }
514}
515
516async fn sleep_until(at: Option<tokio::time::Instant>) {
519 match at {
520 Some(at) => tokio::time::sleep_until(at).await,
521 None => std::future::pending().await,
522 }
523}
524
525struct MonotonicClock {
527 clock: MockExchangeClock,
528 latest: Option<DateTime<Utc>>,
529}
530
531impl MonotonicClock {
532 fn new(clock: MockExchangeClock) -> Self {
533 Self {
534 clock,
535 latest: None,
536 }
537 }
538
539 fn now(&mut self) -> DateTime<Utc> {
541 let now = (self.clock)();
542 self.raise_to(now)
543 }
544
545 fn peek(&self) -> DateTime<Utc> {
547 let now = (self.clock)();
548 self.latest.map_or(now, |latest| latest.max(now))
549 }
550
551 fn raise_to(&mut self, time: DateTime<Utc>) -> DateTime<Utc> {
553 let latest = self.latest.map_or(time, |latest| latest.max(time));
554 self.latest = Some(latest);
555 latest
556 }
557}
558
559struct Emit {
562 tx: mpsc::UnboundedSender<PendingEmission>,
563 exchange: ExchangeId,
564 delay: Duration,
566}
567
568impl Emit {
569 fn new(
570 tx: mpsc::UnboundedSender<PendingEmission>,
571 exchange: ExchangeId,
572 delay_ms: u64,
573 ) -> Self {
574 Self {
575 tx,
576 exchange,
577 delay: Duration::from_millis(delay_ms),
578 }
579 }
580
581 fn respond<Response>(
583 &self,
584 events: Vec<UnindexedAccountEvent>,
585 response_tx: oneshot::Sender<Response>,
586 response: Response,
587 ) where
588 Response: Send + 'static,
589 {
590 let respond: Respond = Box::new(move |exchange| {
591 if response_tx.send(response).is_err() {
592 error!(
593 %exchange,
594 kind = std::any::type_name::<Response>(),
595 "MockExchange failed to send oneshot response to client"
596 );
597 }
598 });
599 self.queue(events, Some(respond));
600 }
601
602 fn queue(&self, events: Vec<UnindexedAccountEvent>, respond: Option<Respond>) {
618 if events.is_empty() && respond.is_none() {
619 return;
620 }
621
622 let ready_at = tokio::time::Instant::now() + self.delay;
626
627 if self
630 .tx
631 .send(PendingEmission {
632 ready_at,
633 events,
634 respond,
635 })
636 .is_err()
637 {
638 error!(
639 exchange = %self.exchange,
640 "MockExchange could not queue venue output: the emitter has stopped"
641 );
642 }
643 }
644}
645
646type Respond = Box<dyn FnOnce(ExchangeId) + Send>;
648
649struct PendingEmission {
659 ready_at: tokio::time::Instant,
661 events: Vec<UnindexedAccountEvent>,
663 respond: Option<Respond>,
666}
667
668#[cfg(test)]
674#[allow(clippy::unwrap_used, clippy::expect_used)] pub(crate) mod fixtures {
676 use crate::{
677 InstrumentAccountSnapshot, UnindexedAccountSnapshot,
678 balance::{AssetBalance, Balance},
679 client::mock::MockExecutionConfig,
680 fee::{FeeModelConfig, SpotFeeAsset},
681 fill::SimFillConfig,
682 market::MarketSnapshot,
683 order::{
684 Order, OrderEvent, OrderKey, OrderKind, TimeInForce, UnindexedOrder,
685 id::{ClientOrderId, OrderId, StrategyId, VenueOrderId},
686 request::{OrderRequestOpen, RequestOpen},
687 state::{Open, OrderState},
688 },
689 position::PositionReport,
690 };
691 use chrono::{DateTime, Utc};
692 use fnv::FnvHashMap;
693 use rust_decimal::Decimal;
694 use rustrade_instrument::{
695 Side, Underlying,
696 asset::name::AssetNameExchange,
697 exchange::ExchangeId,
698 instrument::{
699 Instrument,
700 kind::InstrumentKind,
701 name::{InstrumentNameExchange, InstrumentNameInternal},
702 quote::InstrumentQuoteAsset,
703 },
704 };
705
706 pub(super) const EXCHANGE: ExchangeId = ExchangeId::BinanceSpot;
707
708 pub(super) fn d(s: &str) -> Decimal {
709 s.parse().unwrap()
710 }
711
712 pub(super) fn base() -> AssetNameExchange {
713 AssetNameExchange::new("BTC")
714 }
715
716 pub(super) fn quote() -> AssetNameExchange {
717 AssetNameExchange::new("USDT")
718 }
719
720 pub(super) fn instrument_name() -> InstrumentNameExchange {
721 InstrumentNameExchange::new("BTCUSDT")
722 }
723
724 pub(super) fn funded(
726 asset: AssetNameExchange,
727 amount: Decimal,
728 ) -> AssetBalance<AssetNameExchange> {
729 AssetBalance {
730 asset,
731 balance: Balance::new(amount, amount),
732 time_exchange: Utc::now(),
733 }
734 }
735
736 pub(super) fn config_from_balances(
738 balances: Vec<AssetBalance<AssetNameExchange>>,
739 fee_model: FeeModelConfig,
740 ) -> MockExecutionConfig {
741 config_from_balances_with_fill(balances, fee_model, SimFillConfig::default())
742 }
743
744 pub(super) fn config_from_balances_with_fill(
745 balances: Vec<AssetBalance<AssetNameExchange>>,
746 fee_model: FeeModelConfig,
747 fill_model: SimFillConfig,
748 ) -> MockExecutionConfig {
749 MockExecutionConfig::new(
750 EXCHANGE,
751 UnindexedAccountSnapshot {
752 exchange: EXCHANGE,
753 balances,
754 instruments: vec![],
755 },
756 0, fee_model,
758 fill_model,
759 SpotFeeAsset::default(),
760 crate::slippage::SlippageConfig::default(),
761 crate::fill::MarketOrderTiming::default(),
762 crate::fill::StaleMarketPolicy::default(),
763 None,
764 None,
765 Default::default(),
766 Default::default(),
767 )
768 }
769
770 pub(super) fn spot_config_holding(
777 btc: &str,
778 usdt: &str,
779 order: UnindexedOrder,
780 ) -> MockExecutionConfig {
781 MockExecutionConfig::new(
782 EXCHANGE,
783 UnindexedAccountSnapshot {
784 exchange: EXCHANGE,
785 balances: vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
786 instruments: vec![InstrumentAccountSnapshot {
787 instrument: instrument_name(),
788 orders: vec![order],
789 orders_complete: true,
790 position: PositionReport::Unreported,
791 isolated: None,
792 }],
793 },
794 0, FeeModelConfig::default(),
796 SimFillConfig::default(),
797 SpotFeeAsset::default(),
798 crate::slippage::SlippageConfig::default(),
799 crate::fill::MarketOrderTiming::default(),
800 crate::fill::StaleMarketPolicy::default(),
801 None,
802 None,
803 Default::default(),
804 Default::default(),
805 )
806 }
807
808 pub(super) fn seeded_gtd(
810 cid: &str,
811 price: &str,
812 opened: DateTime<Utc>,
813 expiry: DateTime<Utc>,
814 ) -> UnindexedOrder {
815 Order {
816 key: OrderKey {
817 exchange: EXCHANGE,
818 instrument: instrument_name(),
819 strategy: StrategyId::new("test"),
820 cid: ClientOrderId::new(cid),
821 },
822 side: Side::Buy,
823 price: Some(d(price)),
824 quantity: d("0.01"),
825 kind: OrderKind::Limit,
826 time_in_force: TimeInForce::GoodTillDate { expiry },
827 state: OrderState::active(Open::new(
828 VenueOrderId::Assigned(OrderId::new(cid)),
829 opened,
830 rust_decimal::Decimal::ZERO,
831 )),
832 }
833 }
834
835 pub(super) fn spot_config(
837 btc: &str,
838 usdt: &str,
839 fee_model: FeeModelConfig,
840 ) -> MockExecutionConfig {
841 config_from_balances(
842 vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
843 fee_model,
844 )
845 }
846
847 pub(super) fn spot_config_with_fill(
848 btc: &str,
849 usdt: &str,
850 fee_model: FeeModelConfig,
851 fill_model: SimFillConfig,
852 ) -> MockExecutionConfig {
853 config_from_balances_with_fill(
854 vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
855 fee_model,
856 fill_model,
857 )
858 }
859
860 pub(super) fn instruments_of(
861 instrument: Instrument<ExchangeId, AssetNameExchange>,
862 ) -> FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
863 let mut instruments = FnvHashMap::default();
864 instruments.insert(instrument.name_exchange.clone(), instrument);
865 instruments
866 }
867
868 pub(super) fn spot_instruments()
869 -> FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
870 instruments_of(Instrument {
871 exchange: EXCHANGE,
872 name_internal: InstrumentNameInternal::new("btcusdt"),
873 name_exchange: instrument_name(),
874 underlying: Underlying {
875 base: base(),
876 quote: quote(),
877 },
878 quote: InstrumentQuoteAsset::UnderlyingQuote,
879 kind: InstrumentKind::Spot,
880 spec: None,
881 data_venue: None,
882 })
883 }
884
885 pub(super) fn request(
891 instrument: InstrumentNameExchange,
892 side: Side,
893 quantity: &str,
894 market: Option<MarketSnapshot>,
895 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
896 OrderEvent {
897 key: OrderKey {
898 exchange: EXCHANGE,
899 instrument,
900 strategy: StrategyId::new("test"),
901 cid: ClientOrderId::new("test-cid"),
902 },
903 state: RequestOpen {
904 market,
905 ..RequestOpen::new(
906 side,
907 None, d(quantity),
909 OrderKind::Market,
910 TimeInForce::ImmediateOrCancel,
911 )
912 },
913 }
914 }
915
916 pub(super) fn with_effect(
918 mut request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
919 effect: crate::margin::MarginEffect,
920 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
921 request.state = request.state.with_margin_effect(effect);
922 request
923 }
924
925 pub(super) fn buy_request(
926 quantity: &str,
927 market: Option<MarketSnapshot>,
928 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
929 request(instrument_name(), Side::Buy, quantity, market)
930 }
931
932 pub(super) fn sell_request(
933 quantity: &str,
934 market: Option<MarketSnapshot>,
935 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
936 request(instrument_name(), Side::Sell, quantity, market)
937 }
938
939 pub(super) fn gtc() -> TimeInForce {
941 TimeInForce::GoodUntilCancelled { post_only: false }
942 }
943
944 pub(super) fn limit_request(
949 cid: &str,
950 side: Side,
951 quantity: &str,
952 price: &str,
953 time_in_force: TimeInForce,
954 ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
955 OrderEvent {
956 key: OrderKey {
957 exchange: EXCHANGE,
958 instrument: instrument_name(),
959 strategy: StrategyId::new("test"),
960 cid: ClientOrderId::new(cid),
961 },
962 state: RequestOpen::new(
963 side,
964 Some(d(price)),
965 d(quantity),
966 OrderKind::Limit,
967 time_in_force,
968 ),
969 }
970 }
971
972 pub(super) fn book(best_bid: &str, best_ask: &str) -> MarketSnapshot {
974 MarketSnapshot {
975 best_bid: Some(d(best_bid)),
976 best_ask: Some(d(best_ask)),
977 last_price: None,
978 }
979 }
980
981 pub(super) fn traded(last_price: &str) -> MarketSnapshot {
983 MarketSnapshot {
984 best_bid: None,
985 best_ask: None,
986 last_price: Some(d(last_price)),
987 }
988 }
989
990 pub(super) fn market_prices(price: &str) -> Option<MarketSnapshot> {
992 let p = Some(d(price));
993 Some(MarketSnapshot {
994 best_bid: p,
995 best_ask: p,
996 last_price: p,
997 })
998 }
999}
1000
1001#[cfg(test)]
1002#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
1004 use super::{fixtures::*, *};
1005 use crate::market::MarketSnapshot;
1006 use crate::{
1007 AccountEventKind,
1008 fee::FeeModelConfig,
1009 order::{
1010 OrderEvent, OrderKey,
1011 id::ClientOrderId,
1012 request::RequestCancel,
1013 state::{InactiveOrderState, OrderState},
1014 },
1015 };
1016 use rust_decimal::Decimal;
1017 use rustrade_instrument::Side;
1018
1019 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1035 async fn open_order_emissions_are_ordered_across_fills() {
1036 const FILLS: usize = 8;
1037 const DEBIT_PER_FILL: &str = "500";
1038
1039 let usdt_start = "10000000";
1040
1041 let (request_tx, request_rx) = mpsc::unbounded_channel();
1042 let (event_tx, mut events) = broadcast::channel(64);
1043
1044 let exchange = MockExchange::new(
1045 spot_config("100", usdt_start, FeeModelConfig::default()),
1046 request_rx,
1047 event_tx,
1048 spot_instruments(),
1049 );
1050
1051 let driver = tokio::spawn(exchange.run());
1052
1053 let mut responses = Vec::with_capacity(FILLS);
1054 for nth in 0..FILLS {
1055 let (response_tx, response_rx) = oneshot::channel();
1056 let mut request = buy_request("0.01", market_prices("50000"));
1057 request.key.cid = ClientOrderId::new(format!("cid-{nth}"));
1058
1059 request_tx
1060 .send(MockExchangeRequest::open_order(
1061 Utc::now(),
1062 response_tx,
1063 request,
1064 ))
1065 .unwrap();
1066 responses.push(response_rx);
1067 }
1068
1069 drop(request_tx);
1071 driver.await.unwrap();
1072
1073 for response in responses {
1074 let response = response.await.expect("every open is answered");
1075 assert!(
1076 matches!(
1077 response.state,
1078 OrderState::Active(_) | OrderState::Inactive(_)
1079 ),
1080 "the account is funded and the instrument priced, so every open fills: {response:?}"
1081 );
1082 }
1083
1084 let balances = std::iter::from_fn(|| events.try_recv().ok())
1086 .filter_map(|event| match event.kind {
1087 AccountEventKind::BalanceSnapshot(snapshot) if snapshot.0.asset == quote() => {
1088 Some(snapshot.0.balance.total)
1089 }
1090 _ => None,
1091 })
1092 .collect::<Vec<_>>();
1093
1094 let expected = (1..=FILLS)
1096 .map(|nth| d(usdt_start) - d(DEBIT_PER_FILL) * Decimal::from(nth))
1097 .collect::<Vec<_>>();
1098
1099 assert_eq!(
1100 balances, expected,
1101 "balance snapshots must reach the client in the order the venue booked them"
1102 );
1103 }
1104
1105 #[tokio::test]
1107 async fn the_driver_fills_at_the_configured_slippage() {
1108 let (request_tx, request_rx) = mpsc::unbounded_channel();
1109 let (event_tx, mut events) = broadcast::channel(16);
1110 let config = MockExecutionConfig {
1111 slippage: crate::slippage::SlippageConfig::fixed(d("0.01")).unwrap(),
1112 ..spot_config("100", "10000000", FeeModelConfig::default())
1113 };
1114 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1115 let driver = tokio::spawn(exchange.run());
1116
1117 let (response_tx, response_rx) = oneshot::channel();
1118 request_tx
1119 .send(MockExchangeRequest::open_order(
1120 Utc::now(),
1121 response_tx,
1122 buy_request("0.01", market_prices("50000")),
1123 ))
1124 .unwrap();
1125 response_rx.await.expect("the open is answered");
1126 drop(request_tx);
1127 driver.await.unwrap();
1128
1129 let prices = std::iter::from_fn(|| events.try_recv().ok())
1130 .filter_map(|event| match event.kind {
1131 AccountEventKind::Trade(trade) => Some(trade.price),
1132 _ => None,
1133 })
1134 .collect::<Vec<_>>();
1135 assert_eq!(prices, vec![d("50500")], "50,000 bought 1% above");
1136 }
1137
1138 #[tokio::test(start_paused = true)]
1153 async fn a_swept_expiry_is_emitted_behind_the_fill_booked_before_it() {
1154 let opened = Utc::now();
1155 let expiry = opened + chrono::TimeDelta::seconds(30);
1156
1157 let (request_tx, request_rx) = mpsc::unbounded_channel();
1158 let (event_tx, mut events) = broadcast::channel(64);
1159
1160 let mut config =
1161 spot_config_holding("100", "10000000", seeded_gtd("gtd", "1", opened, expiry));
1162 config.latency_ms = 50;
1164
1165 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1166 let driver = tokio::spawn(exchange.run());
1167
1168 let (fill_tx, fill_rx) = oneshot::channel();
1170 request_tx
1171 .send(MockExchangeRequest::open_order(
1172 opened,
1173 fill_tx,
1174 buy_request("0.01", market_prices("50000")),
1175 ))
1176 .unwrap();
1177
1178 let (snapshot_tx, snapshot_rx) = oneshot::channel();
1181 request_tx
1182 .send(MockExchangeRequest::fetch_account_snapshot(
1183 expiry + chrono::TimeDelta::seconds(1),
1184 snapshot_tx,
1185 ))
1186 .unwrap();
1187
1188 drop(request_tx);
1189 driver.await.unwrap();
1190 fill_rx.await.expect("the open is answered");
1191 let _ = snapshot_rx.await;
1192
1193 let kinds = std::iter::from_fn(|| events.try_recv().ok())
1194 .map(|event| match event.kind {
1195 AccountEventKind::BalanceSnapshot(_) => "balance",
1196 AccountEventKind::Trade(_) => "trade",
1197 AccountEventKind::OrderSnapshot(_) => "order",
1198 _ => "other",
1199 })
1200 .collect::<Vec<_>>();
1201
1202 assert_eq!(
1203 kinds,
1204 ["balance", "balance", "trade", "order"],
1205 "the fill's events must all precede the sweep, which for a seeded order — held with \
1206 no reservation — owes only its terminal snapshot"
1207 );
1208 }
1209
1210 #[tokio::test]
1211 async fn a_cancel_request_is_answered_rather_than_dropped() {
1212 let (request_tx, request_rx) = mpsc::unbounded_channel();
1213 let (event_tx, _events) = broadcast::channel(16);
1214
1215 let exchange = MockExchange::new(
1216 spot_config("100", "10000", FeeModelConfig::default()),
1217 request_rx,
1218 event_tx,
1219 spot_instruments(),
1220 );
1221 let driver = tokio::spawn(exchange.run());
1222
1223 let (response_tx, response_rx) = oneshot::channel();
1224 let open = buy_request("1", market_prices("50000"));
1225 request_tx
1226 .send(MockExchangeRequest::cancel_order(
1227 Utc::now(),
1228 response_tx,
1229 OrderEvent {
1230 key: open.key,
1231 state: RequestCancel { id: None },
1232 },
1233 ))
1234 .unwrap();
1235
1236 let response = response_rx.await.expect("a cancel must be answered");
1237 assert!(
1238 response.state.is_err(),
1239 "this venue rests no orders, so a cancel must be rejected: {response:?}"
1240 );
1241
1242 drop(request_tx);
1243 driver.await.unwrap();
1244 }
1245
1246 #[tokio::test]
1249 async fn an_order_state_lookup_is_answered_from_the_ledger() {
1250 use crate::client::{OrderStatusClient, mock::MockExecution};
1251
1252 let (request_tx, request_rx) = mpsc::unbounded_channel();
1253 let (event_tx, event_rx) = broadcast::channel(16);
1254 let exchange = MockExchange::new(
1255 spot_config("100", "10000000", FeeModelConfig::default()),
1256 request_rx,
1257 event_tx,
1258 spot_instruments(),
1259 );
1260 let driver = tokio::spawn(exchange.run());
1261
1262 let open = buy_request("1", market_prices("50000"));
1263 let (response_tx, response_rx) = oneshot::channel();
1264 request_tx
1265 .send(MockExchangeRequest::open_order(
1266 Utc::now(),
1267 response_tx,
1268 open.clone(),
1269 ))
1270 .unwrap();
1271 let opened = response_rx.await.expect("an open must be answered");
1272 assert!(matches!(
1273 opened.state,
1274 OrderState::Inactive(InactiveOrderState::FullyFilled(_))
1275 ));
1276
1277 let client = MockExecution {
1278 mocked_exchange: EXCHANGE,
1279 clock: Utc::now,
1280 request_tx: request_tx.clone(),
1281 event_rx,
1282 };
1283 let unknown = OrderKey {
1284 cid: ClientOrderId::new("never-opened"),
1285 ..open.key.clone()
1286 };
1287 let ended = client
1288 .fetch_ended_orders(&[open.key.clone(), unknown])
1289 .await
1290 .expect("the venue is running");
1291
1292 let [order] = ended.as_slice() else {
1293 panic!("only the filled order is reported: {ended:?}");
1294 };
1295 assert_eq!(order.key, open.key);
1296 assert!(matches!(order.state, InactiveOrderState::FullyFilled(_)));
1297
1298 drop(client);
1299 drop(request_tx);
1300 driver.await.unwrap();
1301 }
1302
1303 async fn borrow_capacity_from(
1306 config: crate::client::mock::MockExecutionConfig,
1307 side: Side,
1308 ) -> Result<crate::BorrowCapacity, crate::error::UnindexedClientError> {
1309 use crate::client::{BorrowCapacityClient, mock::MockExecution};
1310
1311 let (request_tx, request_rx) = mpsc::unbounded_channel();
1312 let (event_tx, event_rx) = broadcast::channel(16);
1313 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1314 let driver = tokio::spawn(exchange.run());
1315
1316 let client = MockExecution {
1317 mocked_exchange: EXCHANGE,
1318 clock: Utc::now,
1319 request_tx,
1320 event_rx,
1321 };
1322 let capacity = client.fetch_borrow_capacity(&instrument_name(), side).await;
1323
1324 drop(client);
1325 driver.await.unwrap();
1326 capacity
1327 }
1328
1329 #[tokio::test]
1331 async fn a_borrow_capacity_query_is_answered_by_the_venue() {
1332 use crate::margin::{MarginEffect, SpotMarginConfig};
1333
1334 let config = crate::client::mock::MockExecutionConfig {
1335 spot_margin: Some(
1336 SpotMarginConfig::new(quote(), Decimal::from(3), MarginEffect::Borrow).unwrap(),
1337 ),
1338 ..spot_config("0", "10000", FeeModelConfig::default())
1339 };
1340
1341 let capacity = borrow_capacity_from(config, Side::Buy)
1342 .await
1343 .expect("a margin venue lends against BTCUSDT");
1344
1345 assert_eq!(
1346 capacity,
1347 crate::BorrowCapacity::new(quote()).with_borrowable_now(Decimal::from(20_000))
1348 );
1349 }
1350
1351 #[tokio::test]
1353 async fn a_borrow_capacity_the_venue_refuses_is_an_api_error() {
1354 let error = borrow_capacity_from(
1355 spot_config("0", "10000", FeeModelConfig::default()),
1356 Side::Sell,
1357 )
1358 .await
1359 .expect_err("a cash venue lends against nothing");
1360
1361 assert!(
1362 matches!(
1363 &error,
1364 crate::error::ClientError::Api(crate::error::ApiError::InstrumentInvalid(
1365 instrument,
1366 _,
1367 )) if *instrument == instrument_name()
1368 ),
1369 "{error:?}"
1370 );
1371 }
1372
1373 #[tokio::test(start_paused = true)]
1376 async fn a_fetch_is_answered_after_the_fill_booked_before_it() {
1377 let (request_tx, request_rx) = mpsc::unbounded_channel();
1378 let (event_tx, mut events) = broadcast::channel(64);
1379 let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1380 config.latency_ms = 50;
1381 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1382 let driver = tokio::spawn(exchange.run());
1383
1384 let (open_tx, open_rx) = oneshot::channel();
1385 request_tx
1386 .send(MockExchangeRequest::open_order(
1387 Utc::now(),
1388 open_tx,
1389 buy_request("0.01", market_prices("50000")),
1390 ))
1391 .unwrap();
1392 let (balances_tx, balances_rx) = oneshot::channel();
1393 request_tx
1394 .send(MockExchangeRequest::fetch_balances(
1395 Utc::now(),
1396 vec![quote()],
1397 balances_tx,
1398 ))
1399 .unwrap();
1400
1401 balances_rx.await.expect("the fetch is answered");
1402 let kinds = std::iter::from_fn(|| events.try_recv().ok())
1403 .map(|event| event_name(&event))
1404 .collect::<Vec<_>>();
1405 assert!(
1406 kinds.contains(&"trade"),
1407 "the fill booked before the fetch is emitted before the fetch is answered: {kinds:?}"
1408 );
1409
1410 open_rx.await.expect("the open is answered");
1411 drop(request_tx);
1412 driver.await.unwrap();
1413 }
1414
1415 #[test]
1417 #[should_panic(expected = "request-priced venue")]
1418 fn a_market_driven_driver_refuses_a_request_priced_venue() {
1419 let config = spot_config("0", "10000", FeeModelConfig::default());
1420 let (_request_tx, request_rx) = mpsc::unbounded_channel();
1421 let (event_tx, _events) = broadcast::channel(16);
1422 let _ = MockExchange::new_market_driven(
1423 SimulatedVenue::new(&config, spot_instruments()),
1424 0,
1425 request_rx,
1426 event_tx,
1427 futures::StreamExt::boxed(futures::stream::empty()),
1428 Arc::new(Utc::now),
1429 );
1430 }
1431
1432 fn event_name(event: &UnindexedAccountEvent) -> &'static str {
1433 match &event.kind {
1434 AccountEventKind::BalanceSnapshot(_) => "balance",
1435 AccountEventKind::Trade(_) => "trade",
1436 AccountEventKind::OrderSnapshot(_) => "order",
1437 AccountEventKind::StreamTerminated(_) => "terminated",
1438 _ => "other",
1439 }
1440 }
1441
1442 struct Paper {
1445 request_tx: mpsc::UnboundedSender<MockExchangeRequest>,
1446 market_tx: mpsc::UnboundedSender<MarketFeedEvent>,
1447 events: broadcast::Receiver<UnindexedAccountEvent>,
1448 driver: tokio::task::JoinHandle<()>,
1449 start: DateTime<Utc>,
1451 }
1452
1453 impl Paper {
1454 fn start(config: MockExecutionConfig) -> Self {
1456 let start = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap();
1457 let anchor = tokio::time::Instant::now();
1458 let clock: MockExchangeClock = Arc::new(move || {
1459 start + TimeDelta::from_std(anchor.elapsed()).expect("a test runs for less")
1460 });
1461 Self::start_on(config, clock, start)
1462 }
1463
1464 fn start_on(
1465 config: MockExecutionConfig,
1466 clock: MockExchangeClock,
1467 start: DateTime<Utc>,
1468 ) -> Self {
1469 let (request_tx, request_rx) = mpsc::unbounded_channel();
1470 let (event_tx, events) = broadcast::channel(256);
1471 let (market_tx, market_rx) = mpsc::unbounded_channel();
1472 let exchange = MockExchange::new_market_driven(
1473 SimulatedVenue::new_market_driven(&config, spot_instruments()),
1474 config.latency_ms,
1475 request_rx,
1476 event_tx,
1477 futures::StreamExt::boxed(tokio_stream::wrappers::UnboundedReceiverStream::new(
1478 market_rx,
1479 )),
1480 clock,
1481 );
1482 Self {
1483 request_tx,
1484 market_tx,
1485 events,
1486 driver: tokio::spawn(exchange.run()),
1487 start,
1488 }
1489 }
1490
1491 fn observe(&self, snapshot: MarketSnapshot) {
1492 self.market_tx
1493 .send(MarketFeedEvent::Observation {
1494 instrument: instrument_name(),
1495 observation: MarketObservation::new(snapshot),
1496 })
1497 .unwrap();
1498 }
1499
1500 async fn open(
1501 &self,
1502 request: crate::order::request::OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
1503 ) -> crate::order::Order<
1504 ExchangeId,
1505 InstrumentNameExchange,
1506 crate::order::state::UnindexedOrderState,
1507 > {
1508 let (response_tx, response_rx) = oneshot::channel();
1509 self.request_tx
1510 .send(MockExchangeRequest::open_order(
1511 Utc::now(),
1512 response_tx,
1513 request,
1514 ))
1515 .unwrap();
1516 response_rx.await.expect("an open is answered")
1517 }
1518
1519 async fn next_event(&mut self) -> UnindexedAccountEvent {
1521 tokio::time::timeout(std::time::Duration::from_secs(3600), self.events.recv())
1522 .await
1523 .expect("an account event arrives")
1524 .expect("the account channel is open")
1525 }
1526
1527 async fn until_trade(
1529 &mut self,
1530 ) -> crate::trade::Trade<AssetNameExchange, InstrumentNameExchange> {
1531 loop {
1532 if let AccountEventKind::Trade(trade) = self.next_event().await.kind {
1533 return trade;
1534 }
1535 }
1536 }
1537
1538 async fn stop(self) {
1539 drop(self.request_tx);
1540 self.driver.await.unwrap();
1541 }
1542 }
1543
1544 fn priced(price: &str) -> MarketSnapshot {
1545 market_prices(price).unwrap()
1546 }
1547
1548 #[tokio::test(start_paused = true)]
1552 async fn a_paper_venue_refuses_an_immediate_order_once_its_feed_is_reported_down() {
1553 let ioc = |cid: &str| {
1554 let mut request = request(instrument_name(), Side::Buy, "1", None);
1555 request.key.cid = ClientOrderId::new(cid);
1556 request.state.time_in_force = crate::order::TimeInForce::ImmediateOrCancel;
1557 request
1558 };
1559 let refused = |state: &crate::order::state::UnindexedOrderState| {
1560 matches!(
1561 state,
1562 OrderState::Inactive(InactiveOrderState::OpenFailed(
1563 crate::error::OrderError::Rejected(crate::error::ApiError::OrderRejected(reason))
1564 )) if reason.contains("feed was reported down")
1565 )
1566 };
1567
1568 for end_feed in [false, true] {
1569 let mut config = spot_config("0", "100000", FeeModelConfig::default());
1570 config.stale_market = crate::fill::StaleMarketPolicy::Wait {
1571 max_age: std::time::Duration::from_secs(3600),
1572 };
1573 let mut paper = Paper::start(config);
1574 paper.observe(priced("50000"));
1575
1576 let before = paper.open(ioc("before")).await;
1577 assert!(!refused(&before.state), "{:?}", before.state);
1578
1579 if end_feed {
1580 paper.market_tx = mpsc::unbounded_channel().0;
1582 } else {
1583 paper
1584 .market_tx
1585 .send(MarketFeedEvent::Stale {
1586 instruments: vec![instrument_name()],
1587 })
1588 .unwrap();
1589 }
1590
1591 let after = paper.open(ioc("after")).await;
1592 assert!(
1593 refused(&after.state),
1594 "end_feed {end_feed}: {:?}",
1595 after.state
1596 );
1597 paper.stop().await;
1598 }
1599 }
1600
1601 #[tokio::test(start_paused = true)]
1605 async fn a_paper_limit_rests_then_fills_when_the_market_crosses_it() {
1606 let mut config = spot_config("0", "100000", FeeModelConfig::default());
1607 config.latency_ms = 100;
1608 let mut paper = Paper::start(config);
1609 paper.observe(priced("50000"));
1610
1611 let sent = tokio::time::Instant::now();
1612 let response = paper
1613 .open(limit_request("rest", Side::Buy, "1", "49000", gtc()))
1614 .await;
1615 assert!(
1616 matches!(response.state, OrderState::Active(_)),
1617 "a limit below the market rests: {response:?}"
1618 );
1619 assert_eq!(
1620 sent.elapsed(),
1621 std::time::Duration::from_millis(100),
1622 "half the latency to the venue, half back"
1623 );
1624
1625 paper.observe(priced("48000"));
1626 let trade = paper.until_trade().await;
1627 assert_eq!(trade.quantity, d("1"));
1628 assert!(
1629 trade.price <= d("49000"),
1630 "a buy limit never pays more: {trade:?}"
1631 );
1632 paper.stop().await;
1633 }
1634
1635 #[tokio::test(start_paused = true)]
1638 async fn a_paper_request_is_booked_against_the_market_when_it_arrives() {
1639 let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1640 config.latency_ms = 100;
1641 let mut paper = Paper::start(config);
1642 paper.observe(priced("50000"));
1643
1644 let (response_tx, response_rx) = oneshot::channel();
1645 paper
1646 .request_tx
1647 .send(MockExchangeRequest::open_order(
1648 Utc::now(),
1649 response_tx,
1650 buy_request("0.01", market_prices("1")),
1652 ))
1653 .unwrap();
1654 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1656 paper.observe(priced("51000"));
1657
1658 let trade = paper.until_trade().await;
1659 assert_eq!(
1660 trade.price,
1661 d("51000"),
1662 "booked against the market as it arrived"
1663 );
1664 assert_eq!(
1665 trade.time_exchange,
1666 paper.start + TimeDelta::milliseconds(50),
1667 "stamped when it reached the venue"
1668 );
1669 response_rx.await.expect("the open is answered");
1670 paper.stop().await;
1671 }
1672
1673 #[tokio::test(start_paused = true)]
1676 async fn a_paper_order_expires_at_its_deadline_in_a_quiet_market() {
1677 let config = spot_config("0", "100000", FeeModelConfig::default());
1678 let mut paper = Paper::start(config);
1679 paper.observe(priced("50000"));
1680 let expiry = paper.start + TimeDelta::seconds(10);
1681 let started = tokio::time::Instant::now();
1682
1683 let response = paper
1684 .open(limit_request(
1685 "gtd",
1686 Side::Buy,
1687 "1",
1688 "49000",
1689 crate::order::TimeInForce::GoodTillDate { expiry },
1690 ))
1691 .await;
1692 assert!(matches!(response.state, OrderState::Active(_)));
1693
1694 let expired = loop {
1695 let event = paper.next_event().await;
1696 if let AccountEventKind::OrderSnapshot(order) = event.kind
1697 && matches!(
1698 order.0.state,
1699 OrderState::Inactive(InactiveOrderState::Expired(_))
1700 )
1701 {
1702 break order.0;
1703 }
1704 };
1705 let OrderState::Inactive(InactiveOrderState::Expired(expired)) = expired.state else {
1706 unreachable!("matched above")
1707 };
1708 assert_eq!(expired.time_exchange, expiry, "retired at its own deadline");
1709 assert!(
1710 started.elapsed() >= std::time::Duration::from_secs(10),
1711 "and not before that time has passed: {:?}",
1712 started.elapsed()
1713 );
1714 paper.stop().await;
1715 }
1716
1717 #[tokio::test(start_paused = true)]
1720 async fn a_lagged_paper_feed_ends_the_account_stream_after_the_fills_before_it() {
1721 let config = spot_config("0", "10000000", FeeModelConfig::default());
1722 let mut paper = Paper::start(config);
1723 paper.observe(priced("50000"));
1724 paper.open(buy_request("0.01", None)).await;
1725 paper
1726 .market_tx
1727 .send(MarketFeedEvent::Lagged { missed: 3 })
1728 .unwrap();
1729
1730 let mut kinds = Vec::new();
1731 while kinds.last() != Some(&"terminated") {
1732 kinds.push(event_name(&paper.next_event().await));
1733 }
1734 assert!(
1735 kinds.contains(&"trade"),
1736 "the fill booked before the lag precedes the termination: {kinds:?}"
1737 );
1738
1739 let response = paper.open(buy_request("0.01", None)).await;
1740 assert!(
1741 matches!(
1742 response.state,
1743 OrderState::Inactive(InactiveOrderState::FullyFilled(_))
1744 ),
1745 "the venue keeps trading after the lag: {response:?}"
1746 );
1747 paper.stop().await;
1748 }
1749
1750 #[tokio::test(start_paused = true)]
1752 async fn a_paper_driver_keeps_trading_once_its_feed_ends() {
1753 let config = spot_config("0", "10000000", FeeModelConfig::default());
1754 let mut paper = Paper::start(config);
1755 paper.observe(priced("50000"));
1756 let (replacement, _) = mpsc::unbounded_channel();
1758 drop(std::mem::replace(&mut paper.market_tx, replacement));
1759
1760 paper.open(buy_request("0.01", None)).await;
1761 assert_eq!(paper.until_trade().await.price, d("50000"));
1762 paper.stop().await;
1763 }
1764
1765 #[tokio::test(start_paused = true)]
1767 async fn a_paper_clock_that_steps_back_does_not_move_the_venue_back() {
1768 let later = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap();
1769 let reading = Arc::new(std::sync::Mutex::new(later));
1770 let clock: MockExchangeClock = {
1771 let reading = Arc::clone(&reading);
1772 Arc::new(move || *reading.lock().unwrap())
1773 };
1774 let config = spot_config("0", "10000000", FeeModelConfig::default());
1775 let mut paper = Paper::start_on(config, clock, later);
1776 paper.observe(priced("50000"));
1777 tokio::time::sleep(std::time::Duration::from_millis(1)).await;
1779 *reading.lock().unwrap() = later - TimeDelta::seconds(100);
1780
1781 paper.open(buy_request("0.01", None)).await;
1782 assert_eq!(paper.until_trade().await.time_exchange, later);
1783 paper.stop().await;
1784 }
1785
1786 #[tokio::test]
1789 async fn a_request_priced_request_is_booked_half_the_latency_after_it_was_made() {
1790 let (request_tx, request_rx) = mpsc::unbounded_channel();
1791 let (event_tx, _events) = broadcast::channel(16);
1792 let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1793 config.latency_ms = 100;
1794 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1795 let driver = tokio::spawn(exchange.run());
1796
1797 let made = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap();
1798 let (open_tx, open_rx) = oneshot::channel();
1799 request_tx
1800 .send(MockExchangeRequest::open_order(
1801 made,
1802 open_tx,
1803 buy_request("0.01", market_prices("50000")),
1804 ))
1805 .unwrap();
1806 open_rx.await.expect("the open is answered");
1807
1808 let booked = made + TimeDelta::milliseconds(50);
1809 let (trades_tx, trades_rx) = oneshot::channel();
1810 request_tx
1811 .send(MockExchangeRequest::fetch_trades(
1812 made, trades_tx, made, booked,
1813 ))
1814 .unwrap();
1815 let trades = trades_rx.await.expect("the fetch is answered");
1816 assert_eq!(
1817 trades
1818 .iter()
1819 .map(|trade| trade.time_exchange)
1820 .collect::<Vec<_>>(),
1821 vec![booked],
1822 "booked at half the latency, and a bound at that instant includes it"
1823 );
1824 drop(request_tx);
1825 driver.await.unwrap();
1826 }
1827
1828 #[tokio::test(start_paused = true)]
1830 async fn a_paper_request_in_flight_is_answered_after_the_request_channel_closes() {
1831 let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1832 config.latency_ms = 100;
1833 let paper = Paper::start(config);
1834 paper.observe(priced("50000"));
1835
1836 let (response_tx, response_rx) = oneshot::channel();
1837 paper
1838 .request_tx
1839 .send(MockExchangeRequest::open_order(
1840 Utc::now(),
1841 response_tx,
1842 buy_request("0.01", None),
1843 ))
1844 .unwrap();
1845 drop(paper.request_tx);
1846
1847 let response = response_rx.await.expect("the open is still answered");
1848 assert!(matches!(response.state, OrderState::Inactive(_)));
1849 paper.driver.await.unwrap();
1850 }
1851
1852 #[tokio::test]
1854 async fn a_driver_debug_names_the_driver() {
1855 let (_request_tx, request_rx) = mpsc::unbounded_channel();
1856 let (event_tx, _events) = broadcast::channel(16);
1857 let config = spot_config("0", "10000000", FeeModelConfig::default());
1858 let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1859 assert!(format!("{exchange:?}").contains("MockExchange"));
1860 }
1861
1862 #[tokio::test(start_paused = true)]
1865 async fn a_paper_venue_refuses_a_market_order_before_the_first_observation_and_cancels_a_rest()
1866 {
1867 let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1868 config.latency_ms = 100;
1869 let paper = Paper::start(config);
1870
1871 let refused = paper.open(buy_request("0.01", None)).await;
1872 assert!(
1873 matches!(refused.state, OrderState::Inactive(_)),
1874 "nothing to price a market order from: {refused:?}"
1875 );
1876
1877 let rest = limit_request("rest", Side::Buy, "1", "49000", gtc());
1878 let key = rest.key.clone();
1879 let resting = paper.open(rest).await;
1880 assert!(matches!(resting.state, OrderState::Active(_)));
1881
1882 let (response_tx, response_rx) = oneshot::channel();
1883 paper
1884 .request_tx
1885 .send(MockExchangeRequest::cancel_order(
1886 Utc::now(),
1887 response_tx,
1888 OrderEvent {
1889 key,
1890 state: RequestCancel { id: None },
1891 },
1892 ))
1893 .unwrap();
1894 let cancelled = response_rx.await.expect("the cancel is answered");
1895 assert!(cancelled.state.is_ok(), "{cancelled:?}");
1896 paper.stop().await;
1897 }
1898
1899 #[tokio::test(start_paused = true)]
1901 async fn a_paper_deadline_beyond_the_longest_sleep_is_retired_on_time() {
1902 let config = spot_config("0", "100000", FeeModelConfig::default());
1903 let mut paper = Paper::start(config);
1904 paper.observe(priced("50000"));
1905 let expiry = paper.start + TimeDelta::days(3 * 365);
1906 let started = tokio::time::Instant::now();
1907
1908 paper
1909 .open(limit_request(
1910 "far",
1911 Side::Buy,
1912 "1",
1913 "49000",
1914 crate::order::TimeInForce::GoodTillDate { expiry },
1915 ))
1916 .await;
1917
1918 let expired = loop {
1919 let event = paper
1920 .events
1921 .recv()
1922 .await
1923 .expect("the account channel is open");
1924 if let AccountEventKind::OrderSnapshot(order) = event.kind
1925 && let OrderState::Inactive(InactiveOrderState::Expired(expired)) = order.0.state
1926 {
1927 break expired;
1928 }
1929 };
1930 assert_eq!(expired.time_exchange, expiry);
1931 assert!(started.elapsed() >= std::time::Duration::from_secs(3 * 365 * 24 * 60 * 60));
1932 paper.stop().await;
1933 }
1934}