Skip to main content

rustrade_execution/exchange/mock/
mod.rs

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
21/// The longest the market-driven loop sleeps for a venue deadline in one go, well inside the
22/// longest a tokio timer can wait. A deadline beyond it is waited for in steps.
23const 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
35/// The clock a [`market-driven`](MockExchange::new_market_driven) [`MockExchange`] stamps
36/// everything with.
37pub type MockExchangeClock = Arc<dyn Fn() -> DateTime<Utc> + Send + Sync>;
38
39/// One input from the market feed of a [`market-driven`](MockExchange::new_market_driven)
40/// [`MockExchange`].
41#[derive(Debug, Clone, PartialEq, Eq)]
42#[non_exhaustive]
43pub enum MarketFeedEvent {
44    /// The market of `instrument` as it now stands. See [`SimulatedVenue::apply_market`].
45    Observation {
46        instrument: InstrumentNameExchange,
47        observation: MarketObservation,
48    },
49    /// The feed fell behind and dropped `missed` observations before this point.
50    ///
51    /// The venue never sees what they showed, so a price that crossed a resting order only in one
52    /// of them filled nothing. See [`MockExchange::new_market_driven`] for what the driver does.
53    Lagged { missed: u64 },
54}
55
56/// Asynchronous driver for a [`SimulatedVenue`]: channels, simulated latency, emission ordering.
57///
58/// It owns no ledger and prices nothing. What the venue models, what it rejects and what it leaves
59/// unaccounted for are documented on [`SimulatedVenue`]; this type decides only *when* the venue's
60/// output reaches a client.
61///
62/// It drives a venue in one of two [`VenueRegime`]s:
63/// - [`new`](Self::new): request-priced, for a backtest that sends one request at a time. A
64///   market order is priced from the snapshot its request carries, and nothing can rest.
65/// - [`new_market_driven`](Self::new_market_driven): fed a live market, for paper trading. Limit
66///   orders rest and fill as the market crosses them, as in a backtest of the same venue.
67///
68/// # Ordering
69/// Everything the venue produces — account events and every response, fetches included — is queued
70/// in the order the venue produced it and drained by a single task, so emission order equals
71/// booking order. A response is emitted after the account events produced before it, so a client
72/// holding a response has already been sent every event the venue booked ahead of it. Closing
73/// `request_rx` closes that queue in turn, and [`run`](Self::run) returns only once it has drained.
74pub struct MockExchange {
75    /// The venue's state machine. Ledger, pricing and ordering obligations live here.
76    pub venue: SimulatedVenue,
77    /// Simulated round-trip delay. Transport policy, so it lives on the driver, not the venue.
78    pub latency_ms: u64,
79    pub request_rx: mpsc::UnboundedReceiver<MockExchangeRequest>,
80    pub event_tx: broadcast::Sender<UnindexedAccountEvent>,
81    /// The market feed and clock of a market-driven driver, `None` for a request-priced one.
82    market: Option<MarketDrive>,
83}
84
85/// Manual because the market feed is a boxed stream, which has no `Debug`.
86impl 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
98/// What a market-driven driver reads besides its requests.
99struct MarketDrive {
100    feed: BoxStream<'static, MarketFeedEvent>,
101    clock: MockExchangeClock,
102}
103
104impl MockExchange {
105    /// A request-priced driver, building its venue from `config` with [`SimulatedVenue::new`].
106    ///
107    /// Each request is booked when it is received, at its own `time_request` plus half of
108    /// `latency_ms`, and its output is emitted `latency_ms` of real time later.
109    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    /// A driver that runs `venue` on a live market: `feed` supplies the market, and `clock` the
125    /// time everything is stamped with.
126    ///
127    /// # Time
128    /// The venue is stamped from `clock`, held so that it never moves backwards: an instant before
129    /// the latest one already stamped is stamped with that latest one instead. A request's own
130    /// `time_request` is not used. The clock is moved to `clock`'s instant when [`run`](Self::run)
131    /// starts, so interest and holding costs accrue from then, and on each venue deadline (an
132    /// order's expiry, a holding-cost or interest boundary) when it falls, whether or not anything
133    /// else arrives.
134    ///
135    /// # Latency, as in a backtest of the same venue
136    /// A request waits half of `latency_ms` of real time before it is booked, against the market
137    /// held at that moment, and its output is emitted half of `latency_ms` after that. A fill the
138    /// market causes on a resting order pays the second half alone. Integer division drops an odd
139    /// millisecond from each half.
140    ///
141    /// # Market input
142    /// Each [`MarketFeedEvent::Observation`] is applied with [`SimulatedVenue::apply_market`] as it
143    /// arrives, ahead of a request that is due at the same moment. An instrument the feed has not
144    /// yet observed refuses a market order, while a limit order on it rests.
145    ///
146    /// When the feed reports [`MarketFeedEvent::Lagged`], the driver logs an error and emits a
147    /// [`StreamTerminated`](crate::AccountEventKind::StreamTerminated) after everything queued
148    /// before it, which ends the account stream of a [`MockExecution`](crate::client::mock::MockExecution)
149    /// client. The venue keeps running, and nothing it booked is lost: a client that reopens the
150    /// stream can read its fills back. What the dropped observations showed is lost, so a resting
151    /// order may have missed a fill. Fill recovery cannot repair that.
152    ///
153    /// When the feed ends, the driver logs a warning and serves requests on the last market it
154    /// saw. Deadlines keep falling.
155    ///
156    /// The feed is served ahead of requests, so while it has a backlog a request is read, and its
157    /// wait for the venue begins, only after that backlog is applied. The latency above is a lower
158    /// bound. The backlog is bounded by the feed's own buffer.
159    ///
160    /// # Panics
161    /// If `venue` is not [`VenueRegime::MarketDriven`]: a request-priced venue does not price from
162    /// the feed, so feeding it would change nothing.
163    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    /// Serves requests until the request channel closes, then lets queued output finish.
187    ///
188    /// Venue output is not emitted inline: it is queued and drained by a single task, so that
189    /// emission order equals booking order. Closing the request channel closes that queue in turn,
190    /// and this method returns only once the queue has drained, so shutting the venue down cannot
191    /// strand a fill the client is still waiting on. A market-driven driver first books the
192    /// requests still waiting out their latency.
193    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        // `emit` is dropped above, which closes the queue: let the emitter finish draining it. A
213        // fill booked on the final request would otherwise be dropped with the task, which is
214        // precisely the truncation the drain exists to prevent.
215        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    /// The request-priced loop: each request is booked as soon as it is received.
227    async fn serve_request_priced(&mut self, emit: &Emit) {
228        while let Some(request) = self.request_rx.recv().await {
229            // Advancing the clock can retire an order whose deadline has passed. Those events
230            // answer no request, and are queued before this request's own output so that the
231            // client sees the expiry before anything the request goes on to produce.
232            let expiries = self.advance_venue_time(request.time_request);
233            emit.queue(expiries, None);
234            self.book(request.kind, emit);
235        }
236    }
237
238    /// The market-driven loop: market observations, requests once their latency has passed, and
239    /// the venue's own deadlines, in that order of priority at any one moment.
240    ///
241    /// Every branch is cancel-safe: receiving from a channel or a stream loses nothing when another
242    /// branch wins, and both timers are rebuilt on each pass from state held outside them.
243    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        // Requests waiting out their way to the venue. The latency is fixed, so they fall due in
250        // the order they arrived.
251        let mut arriving = VecDeque::<(tokio::time::Instant, MockExchangeRequestKind)>::new();
252        let mut feed_open = true;
253        let mut requests_open = true;
254        // A deadline the venue still reports after being woken for it, which would otherwise wake
255        // this loop without end. See where it is set.
256        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            // Whether the sleep for the deadline is cut short at the cap, so that waking from it
268            // does not mean the deadline is due.
269            let mut capped = false;
270            let deadline_at = deadline.map(|deadline| {
271                // Rebuilt on every pass, so it needs no state of its own and cannot drift.
272                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                    // The timer and `clock` are two sources, so the wake can land a hair before the
329                    // deadline by `clock`. It is due by the timer, so the venue moves to it.
330                    if let Some(deadline) = deadline {
331                        // The cap woke this loop before the deadline: wait again.
332                        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    /// Books one request against the venue, queuing what it produced and the response it owes.
354    ///
355    /// The venue books the request against its own ledger before returning, so a later request
356    /// already sees it.
357    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                // The venue releases what the cancelled order held before returning, so the
409                // restatement it owes precedes the response, exactly as an open's does.
410                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    /// Applies this driver's latency model, then advances the venue to the resulting instant.
424    ///
425    /// Returns whatever the advance retired — see [`SimulatedVenue::advance_time`]. The caller must
426    /// deliver them; dropping them would leave the client holding an order the venue no longer has.
427    #[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    /// Drains queued venue output in order, emitting each batch's account events before its
439    /// response.
440    ///
441    /// Runs until `emit_rx` closes -- which happens when [`MockExchange::run`] returns -- and then
442    /// finishes whatever is still queued, so a shutdown cannot strand a booked fill or a swept
443    /// expiry.
444    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            // Order is the venue's contract; this preserves it verbatim.
453            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            // Absent for events nothing requested, such as an order retired by its own deadline.
463            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
485/// Sleeps until `at`, or forever when there is nothing to wait for; the `select!` branch using it
486/// is disabled in that case, so the forever is never awaited.
487async 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
494/// A driver's clock, held so that it never moves backwards.
495struct 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    /// The instant to stamp now: the clock's, unless it is before the latest already stamped.
509    fn now(&mut self) -> DateTime<Utc> {
510        let now = (self.clock)();
511        self.raise_to(now)
512    }
513
514    /// The instant [`now`](Self::now) would return, without recording it.
515    fn peek(&self) -> DateTime<Utc> {
516        let now = (self.clock)();
517        self.latest.map_or(now, |latest| latest.max(now))
518    }
519
520    /// Moves the clock to at least `time`, returning the instant to stamp.
521    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
528/// The emission queue as the serving loop sees it: what it owes the client, and how long each
529/// batch waits before it is sent.
530struct Emit {
531    tx: mpsc::UnboundedSender<PendingEmission>,
532    exchange: ExchangeId,
533    /// How long each batch waits after the venue produced it.
534    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    /// Queues `events`, followed by `response` on `response_tx`.
551    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    /// Queues account events, and any response they precede, for the emitter to drain in order.
572    ///
573    /// Everything this venue produces goes through here rather than to `event_tx` directly, so
574    /// that emission order equals production order across *all* of it -- fills, expiries, cancel
575    /// restatements and responses alike. Sending any of them inline would let it overtake a batch
576    /// still waiting out its latency, and an absolute balance delivered out of order is the wrong
577    /// balance, not merely a late one.
578    ///
579    /// # Why a queue rather than a task per fill
580    /// A balance is an **absolute snapshot**, not a delta: successive fills report `9_999_500`,
581    /// then `9_999_000`, then `9_998_500`. Applying them out of order therefore does not merely
582    /// reorder history, it yields the wrong balance. Each fill previously got its own
583    /// [`tokio::spawn`]. Those tasks raced -- at `latency_ms: 0` they all become runnable at once
584    /// -- so snapshots reached the client in arbitrary order and the last to *arrive* won. One
585    /// queue, drained by one task, makes emission order equal booking order by construction.
586    fn queue(&self, events: Vec<UnindexedAccountEvent>, respond: Option<Respond>) {
587        if events.is_empty() && respond.is_none() {
588            return;
589        }
590
591        // Stamped at production time, not at emission time, so the delay each batch waits is
592        // measured from when the venue produced *it*. The delay is fixed, so these are
593        // non-decreasing in queue order and the drain never sleeps away a latency twice.
594        let ready_at = tokio::time::Instant::now() + self.delay;
595
596        // Failure means the emitter is gone, which happens only once `run` has returned. There is
597        // no client left to notify, so there is nothing to do but say so.
598        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
615/// Sends one response to the channel owed it, erased so that every response type shares one queue.
616type Respond = Box<dyn FnOnce(ExchangeId) + Send>;
617
618/// One batch of venue output awaiting emission, held in booking order by [`MockExchange`]'s
619/// emitter queue.
620///
621/// # Why a response is optional
622/// Not everything this venue produces answers a request. An order retired by its own deadline is
623/// swept whenever the clock advances, so the events belong to nobody's response. They must still be
624/// delivered in the same ordered stream as everything else: a balance is an absolute restatement,
625/// so an expiry overtaking a fill still queued behind its latency would leave the client holding
626/// the wrong balance, which is the exact failure this queue exists to prevent.
627struct PendingEmission {
628    /// When this batch's latency expires, measured from the instant the venue produced it.
629    ready_at: tokio::time::Instant,
630    /// The account events the venue produced, in the order it requires.
631    events: Vec<UnindexedAccountEvent>,
632    /// Sends the response, once every event above has been emitted, if this batch answers a
633    /// request at all.
634    respond: Option<Respond>,
635}
636
637/// Fixtures shared by this module's driver tests and [`venue`]'s state-machine tests.
638///
639/// They live in the parent rather than beside either set of tests because a private item is visible
640/// to descendant modules: `venue::tests` can reach `mock::fixtures`, whereas `mock::tests` could not
641/// reach anything declared inside `venue`.
642#[cfg(test)]
643#[allow(clippy::unwrap_used, clippy::expect_used)] // Test code: panics on bad input are acceptable
644pub(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    /// A balance with nothing reserved, which is the only shape this venue can model.
694    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    /// Zero latency, so a driver's queue is exercised without any wall-clock sleeping.
706    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, // latency_ms
726            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    /// A spot config whose account already holds one open order, as `initial_state` seeds it.
738    ///
739    /// The only way an order can rest on a venue [`MockExchange`] drives: that venue is
740    /// [`VenueRegime::RequestPriced`], which refuses `OrderKind::Limit` outright.
741    ///
742    /// [`VenueRegime::RequestPriced`]: crate::exchange::mock::venue::VenueRegime::RequestPriced
743    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, // latency_ms
762            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    /// An open buy resting at `price`, retiring itself at `expiry`.
774    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    /// The spot fixture: `btc` of the base asset and `usdt` of the quote, both fully free.
801    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    /// A Market order carrying the snapshot the venue will price it against.
851    ///
852    /// The snapshot is set on the request rather than passed alongside it, because
853    /// [`RequestOpen::market`] is the venue's only price source and a second copy on the same path
854    /// could disagree with it.
855    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, // Market orders don't have a limit price
873                    d(quantity),
874                    OrderKind::Market,
875                    TimeInForce::ImmediateOrCancel,
876                )
877            },
878        }
879    }
880
881    /// `request`, carrying `effect` as its [`RequestOpen::margin_effect`].
882    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    /// The terms a plainly resting order carries: on the book until somebody cancels it.
905    pub(super) fn gtc() -> TimeInForce {
906        TimeInForce::GoodUntilCancelled { post_only: false }
907    }
908
909    /// A Limit order at `price`, carrying its own `cid` so several can rest at once.
910    ///
911    /// `market` is deliberately `None`: a limit order is judged and priced against the venue's own
912    /// market, never against a snapshot its sender stamped.
913    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    /// Both sides of a book and no trade price, which is what an L1-only feed supplies.
938    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    /// A trade price and no book, which is what a trades-only feed supplies.
947    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    /// A snapshot whose three prices are all `price`, wrapped as the venue receives it.
956    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)] // Test code: panics on bad input are acceptable
968mod 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    /// A [`SimulatedVenue`] balance is an absolute restatement, so emission order *is* the answer.
985    ///
986    /// Each fill used to be emitted by its own spawned task. Those tasks raced, so the snapshots
987    /// `9_999_500`, `9_999_000`, `9_998_500`, ... reached the client in arbitrary order and the
988    /// last to arrive won — which is not the same thing as the last to be booked. That went
989    /// unnoticed only because the timestamp each snapshot carries is derived from a clock
990    /// contaminated with wall-clock time, and so happened to be unique and increasing, letting the
991    /// engine discard the out-of-order ones as stale. Correcting that clock removes the accident.
992    ///
993    /// Asserted on the broadcast stream rather than on any downstream balance, because the ordering
994    /// is the venue's contract to keep: the engine cannot repair snapshots it receives out of order.
995    ///
996    /// # Why this must stay multi-threaded
997    /// The race is between spawned tasks. On a `current_thread` runtime they are serialised by the
998    /// scheduler and this test passes against the unfixed code, detecting nothing.
999    #[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        // Closing the request channel ends `run`, which drains the emitter before returning.
1035        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        // Each fill also restates the base it received; this test pins the quote it paid with.
1050        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        // One quote snapshot per fill, each debiting the quote asset by the same notional.
1060        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    /// The async driver builds its venue from the configuration, slippage included.
1071    #[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    /// A cancel is rejected by the venue, and the driver answers the channel it was asked on.
1104    ///
1105    /// The rejection itself is the venue's own; what this pins is that the driver
1106    /// forwards it rather than leaving the caller's `oneshot` to be dropped — which is what the
1107    /// previous, mistyped implementation did, since it could not produce the channel's response
1108    /// type at all.
1109    /// An order retired by its own deadline reaches the client, in the emitter's queue rather than
1110    /// around it.
1111    ///
1112    /// The sweep runs synchronously at the top of `run`'s loop, while the emitter may still be
1113    /// asleep waiting out an earlier batch's latency. Sending it straight to `event_tx` would let
1114    /// it overtake that batch — and a balance is an absolute restatement, so a snapshot applied out
1115    /// of order leaves the client holding the wrong number, not merely a late one. This asserts the
1116    /// expiry lands *after* the fill booked before it.
1117    #[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        // Non-zero, so the emitter is genuinely asleep when the next request sweeps.
1128        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        // Booked before the deadline, so its fill is queued and still waiting out its latency.
1134        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        // A second request stamped past the deadline: advancing the clock for it sweeps the
1144        // seeded order while the fill above is still in the emitter's queue.
1145        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    /// The mock client's order-state lookup reaches the venue's ledger and back: an order that
1212    /// filled is reported, and one the venue never held is omitted.
1213    #[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    /// What the mock client's borrow capacity query gets back from a running driver, for an order
1269    /// on BTCUSDT on `side`, from a venue configured as `config`.
1270    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    /// The mock client's borrow capacity query reaches the venue and back.
1295    #[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    /// A venue's refusal to report a capacity reaches the client as the venue's API error.
1317    #[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    /// A request-priced driver answers a fetch after the output booked before it, so a balance
1339    /// read while a fill is in flight is never older than the fill's own restatement.
1340    #[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    /// `new_market_driven` refuses a venue that would ignore its feed.
1381    #[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    /// A market-driven driver under test: its channels, a clock moving with tokio's, and the
1408    /// running task.
1409    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        /// The instant the clock read when the driver started.
1415        start: DateTime<Utc>,
1416    }
1417
1418    impl Paper {
1419        /// Runs `config` market-driven, on a clock that reads `start` now and moves with tokio's.
1420        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        /// The next account event, waiting for it in tokio's (paused) time.
1485        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        /// Account events up to and including the first trade.
1493        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    /// A limit order rests on a market-driven driver, and fills when a later observation crosses
1514    /// it, with nothing requesting the fill. Its response takes the whole of `latency_ms`, not one
1515    /// and a half times it.
1516    #[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    /// A request is booked once its outbound half of the latency has passed, against the market
1548    /// held then, as a backtest of the same venue books it.
1549    #[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                // The snapshot a request carries prices nothing on a market-driven venue.
1563                buy_request("0.01", market_prices("1")),
1564            ))
1565            .unwrap();
1566        // Still on its way: the market moves before the order reaches the venue at 50 ms.
1567        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    /// An order's deadline falls when it is due, with no market event or request to move the
1586    /// clock.
1587    #[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    /// A lagged feed ends the account stream after everything booked before it, and the venue
1630    /// keeps serving.
1631    #[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    /// Once the feed ends, the driver trades on the last market it saw.
1663    #[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        // The replacement's receiver is dropped at once; dropping the original ends the feed.
1669        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    /// A clock that steps back does not move the venue back: the stamp holds at the latest one.
1678    #[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        // Settles the observation before the clock steps back.
1690        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    /// A request-priced driver books a request half its latency after the request was made, and a
1699    /// trades fetch bounded by that very instant includes the trade.
1700    #[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    /// Closing the request channel does not drop a request still on its way to the venue.
1741    #[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    /// The driver's `Debug` names it, whichever mode it runs in.
1765    #[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    /// Before the venue has observed an instrument a market order is refused, while a resting
1775    /// limit is accepted and can then be cancelled.
1776    #[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    /// A deadline further out than a tokio timer can wait is retired at its own instant, not early.
1812    #[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}