Skip to main content

rustrade_execution/exchange/mock/
mod.rs

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
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 stopped delivering `instruments`, because the stream carrying them is reconnecting
50    /// or has ended. Each is marked with [`SimulatedVenue::mark_market_stale`] until its next
51    /// fresh observation.
52    Stale {
53        instruments: Vec<InstrumentNameExchange>,
54    },
55    /// The feed fell behind and dropped `missed` observations before this point.
56    ///
57    /// The venue never sees what they showed, so a price that crossed a resting order only in one
58    /// of them filled nothing. See [`MockExchange::new_market_driven`] for what the driver does.
59    Lagged { missed: u64 },
60}
61
62/// Asynchronous driver for a [`SimulatedVenue`]: channels, simulated latency, emission ordering.
63///
64/// It owns no ledger and prices nothing. What the venue models, what it rejects and what it leaves
65/// unaccounted for are documented on [`SimulatedVenue`]; this type decides only *when* the venue's
66/// output reaches a client.
67///
68/// It drives a venue in one of two [`VenueRegime`]s:
69/// - [`new`](Self::new): request-priced, for a backtest that sends one request at a time. A
70///   market order is priced from the snapshot its request carries, and nothing can rest.
71/// - [`new_market_driven`](Self::new_market_driven): fed a live market, for paper trading. Limit
72///   orders rest and fill as the market crosses them, as in a backtest of the same venue.
73///
74/// # Ordering
75/// Everything the venue produces — account events and every response, fetches included — is queued
76/// in the order the venue produced it and drained by a single task, so emission order equals
77/// booking order. A response is emitted after the account events produced before it, so a client
78/// holding a response has already been sent every event the venue booked ahead of it. Closing
79/// `request_rx` closes that queue in turn, and [`run`](Self::run) returns only once it has drained.
80pub struct MockExchange {
81    /// The venue's state machine. Ledger, pricing and ordering obligations live here.
82    pub venue: SimulatedVenue,
83    /// Simulated round-trip delay. Transport policy, so it lives on the driver, not the venue.
84    pub latency_ms: u64,
85    pub request_rx: mpsc::UnboundedReceiver<MockExchangeRequest>,
86    pub event_tx: broadcast::Sender<UnindexedAccountEvent>,
87    /// The market feed and clock of a market-driven driver, `None` for a request-priced one.
88    market: Option<MarketDrive>,
89}
90
91/// Manual because the market feed is a boxed stream, which has no `Debug`.
92impl 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
104/// What a market-driven driver reads besides its requests.
105struct MarketDrive {
106    feed: BoxStream<'static, MarketFeedEvent>,
107    clock: MockExchangeClock,
108}
109
110impl MockExchange {
111    /// A request-priced driver, building its venue from `config` with [`SimulatedVenue::new`].
112    ///
113    /// Each request is booked when it is received, at its own `time_request` plus half of
114    /// `latency_ms`, and its output is emitted `latency_ms` of real time later.
115    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    /// A driver that runs `venue` on a live market: `feed` supplies the market, and `clock` the
131    /// time everything is stamped with.
132    ///
133    /// # Time
134    /// The venue is stamped from `clock`, held so that it never moves backwards: an instant before
135    /// the latest one already stamped is stamped with that latest one instead. A request's own
136    /// `time_request` is not used. The clock is moved to `clock`'s instant when [`run`](Self::run)
137    /// starts, so interest and holding costs accrue from then, and on each venue deadline (an
138    /// order's expiry, a holding-cost or interest boundary) when it falls, whether or not anything
139    /// else arrives.
140    ///
141    /// # Latency, as in a backtest of the same venue
142    /// A request waits half of `latency_ms` of real time before it is booked, against the market
143    /// held at that moment, and its output is emitted half of `latency_ms` after that. A fill the
144    /// market causes on a resting order pays the second half alone. Integer division drops an odd
145    /// millisecond from each half.
146    ///
147    /// # Market input
148    /// Each [`MarketFeedEvent::Observation`] is applied with [`SimulatedVenue::apply_market`] as it
149    /// arrives, ahead of a request that is due at the same moment. An instrument the feed has not
150    /// yet observed refuses a market order, while a limit order on it rests.
151    ///
152    /// Each instrument a [`MarketFeedEvent::Stale`] names is marked with
153    /// [`SimulatedVenue::mark_market_stale`] at the clock's instant.
154    ///
155    /// When the feed reports [`MarketFeedEvent::Lagged`], the driver logs an error and emits a
156    /// [`StreamTerminated`](crate::AccountEventKind::StreamTerminated) after everything queued
157    /// before it, which ends the account stream of a [`MockExecution`](crate::client::mock::MockExecution)
158    /// client. The venue keeps running, and nothing it booked is lost: a client that reopens the
159    /// stream can read its fills back. What the dropped observations showed is lost, so a resting
160    /// order may have missed a fill. Fill recovery cannot repair that.
161    ///
162    /// When the feed ends, the driver logs a warning, marks every instrument stale as above, and
163    /// serves requests on the last market it saw. Deadlines keep falling.
164    ///
165    /// The feed is served ahead of requests, so while it has a backlog a request is read, and its
166    /// wait for the venue begins, only after that backlog is applied. The latency above is a lower
167    /// bound. The backlog is bounded by the feed's own buffer.
168    ///
169    /// # Panics
170    /// If `venue` is not [`VenueRegime::MarketDriven`]: a request-priced venue does not price from
171    /// the feed, so feeding it would change nothing.
172    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    /// Serves requests until the request channel closes, then lets queued output finish.
196    ///
197    /// Venue output is not emitted inline: it is queued and drained by a single task, so that
198    /// emission order equals booking order. Closing the request channel closes that queue in turn,
199    /// and this method returns only once the queue has drained, so shutting the venue down cannot
200    /// strand a fill the client is still waiting on. A market-driven driver first books the
201    /// requests still waiting out their latency.
202    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        // `emit` is dropped above, which closes the queue: let the emitter finish draining it. A
222        // fill booked on the final request would otherwise be dropped with the task, which is
223        // precisely the truncation the drain exists to prevent.
224        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    /// The request-priced loop: each request is booked as soon as it is received.
236    async fn serve_request_priced(&mut self, emit: &Emit) {
237        while let Some(request) = self.request_rx.recv().await {
238            // Advancing the clock can retire an order whose deadline has passed. Those events
239            // answer no request, and are queued before this request's own output so that the
240            // client sees the expiry before anything the request goes on to produce.
241            let expiries = self.advance_venue_time(request.time_request);
242            emit.queue(expiries, None);
243            self.book(request.kind, emit);
244        }
245    }
246
247    /// The market-driven loop: market observations, requests once their latency has passed, and
248    /// the venue's own deadlines, in that order of priority at any one moment.
249    ///
250    /// Every branch is cancel-safe: receiving from a channel or a stream loses nothing when another
251    /// branch wins, and both timers are rebuilt on each pass from state held outside them.
252    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        // Requests waiting out their way to the venue. The latency is fixed, so they fall due in
259        // the order they arrived.
260        let mut arriving = VecDeque::<(tokio::time::Instant, MockExchangeRequestKind)>::new();
261        let mut feed_open = true;
262        let mut requests_open = true;
263        // A deadline the venue still reports after being woken for it, which would otherwise wake
264        // this loop without end. See where it is set.
265        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            // Whether the sleep for the deadline is cut short at the cap, so that waking from it
277            // does not mean the deadline is due.
278            let mut capped = false;
279            let deadline_at = deadline.map(|deadline| {
280                // Rebuilt on every pass, so it needs no state of its own and cannot drift.
281                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                    // The timer and `clock` are two sources, so the wake can land a hair before the
351                    // deadline by `clock`. It is due by the timer, so the venue moves to it.
352                    if let Some(deadline) = deadline {
353                        // The cap woke this loop before the deadline: wait again.
354                        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    /// Books one request against the venue, queuing what it produced and the response it owes.
376    ///
377    /// The venue books the request against its own ledger before returning, so a later request
378    /// already sees it.
379    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                // The venue releases what the cancelled order held before returning, so the
440                // restatement it owes precedes the response, exactly as an open's does.
441                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    /// Applies this driver's latency model, then advances the venue to the resulting instant.
455    ///
456    /// Returns whatever the advance retired — see [`SimulatedVenue::advance_time`]. The caller must
457    /// deliver them; dropping them would leave the client holding an order the venue no longer has.
458    #[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    /// Drains queued venue output in order, emitting each batch's account events before its
470    /// response.
471    ///
472    /// Runs until `emit_rx` closes -- which happens when [`MockExchange::run`] returns -- and then
473    /// finishes whatever is still queued, so a shutdown cannot strand a booked fill or a swept
474    /// expiry.
475    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            // Order is the venue's contract; this preserves it verbatim.
484            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            // Absent for events nothing requested, such as an order retired by its own deadline.
494            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
516/// Sleeps until `at`, or forever when there is nothing to wait for; the `select!` branch using it
517/// is disabled in that case, so the forever is never awaited.
518async 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
525/// A driver's clock, held so that it never moves backwards.
526struct 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    /// The instant to stamp now: the clock's, unless it is before the latest already stamped.
540    fn now(&mut self) -> DateTime<Utc> {
541        let now = (self.clock)();
542        self.raise_to(now)
543    }
544
545    /// The instant [`now`](Self::now) would return, without recording it.
546    fn peek(&self) -> DateTime<Utc> {
547        let now = (self.clock)();
548        self.latest.map_or(now, |latest| latest.max(now))
549    }
550
551    /// Moves the clock to at least `time`, returning the instant to stamp.
552    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
559/// The emission queue as the serving loop sees it: what it owes the client, and how long each
560/// batch waits before it is sent.
561struct Emit {
562    tx: mpsc::UnboundedSender<PendingEmission>,
563    exchange: ExchangeId,
564    /// How long each batch waits after the venue produced it.
565    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    /// Queues `events`, followed by `response` on `response_tx`.
582    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    /// Queues account events, and any response they precede, for the emitter to drain in order.
603    ///
604    /// Everything this venue produces goes through here rather than to `event_tx` directly, so
605    /// that emission order equals production order across *all* of it -- fills, expiries, cancel
606    /// restatements and responses alike. Sending any of them inline would let it overtake a batch
607    /// still waiting out its latency, and an absolute balance delivered out of order is the wrong
608    /// balance, not merely a late one.
609    ///
610    /// # Why a queue rather than a task per fill
611    /// A balance is an **absolute snapshot**, not a delta: successive fills report `9_999_500`,
612    /// then `9_999_000`, then `9_998_500`. Applying them out of order therefore does not merely
613    /// reorder history, it yields the wrong balance. Each fill previously got its own
614    /// [`tokio::spawn`]. Those tasks raced -- at `latency_ms: 0` they all become runnable at once
615    /// -- so snapshots reached the client in arbitrary order and the last to *arrive* won. One
616    /// queue, drained by one task, makes emission order equal booking order by construction.
617    fn queue(&self, events: Vec<UnindexedAccountEvent>, respond: Option<Respond>) {
618        if events.is_empty() && respond.is_none() {
619            return;
620        }
621
622        // Stamped at production time, not at emission time, so the delay each batch waits is
623        // measured from when the venue produced *it*. The delay is fixed, so these are
624        // non-decreasing in queue order and the drain never sleeps away a latency twice.
625        let ready_at = tokio::time::Instant::now() + self.delay;
626
627        // Failure means the emitter is gone, which happens only once `run` has returned. There is
628        // no client left to notify, so there is nothing to do but say so.
629        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
646/// Sends one response to the channel owed it, erased so that every response type shares one queue.
647type Respond = Box<dyn FnOnce(ExchangeId) + Send>;
648
649/// One batch of venue output awaiting emission, held in booking order by [`MockExchange`]'s
650/// emitter queue.
651///
652/// # Why a response is optional
653/// Not everything this venue produces answers a request. An order retired by its own deadline is
654/// swept whenever the clock advances, so the events belong to nobody's response. They must still be
655/// delivered in the same ordered stream as everything else: a balance is an absolute restatement,
656/// so an expiry overtaking a fill still queued behind its latency would leave the client holding
657/// the wrong balance, which is the exact failure this queue exists to prevent.
658struct PendingEmission {
659    /// When this batch's latency expires, measured from the instant the venue produced it.
660    ready_at: tokio::time::Instant,
661    /// The account events the venue produced, in the order it requires.
662    events: Vec<UnindexedAccountEvent>,
663    /// Sends the response, once every event above has been emitted, if this batch answers a
664    /// request at all.
665    respond: Option<Respond>,
666}
667
668/// Fixtures shared by this module's driver tests and [`venue`]'s state-machine tests.
669///
670/// They live in the parent rather than beside either set of tests because a private item is visible
671/// to descendant modules: `venue::tests` can reach `mock::fixtures`, whereas `mock::tests` could not
672/// reach anything declared inside `venue`.
673#[cfg(test)]
674#[allow(clippy::unwrap_used, clippy::expect_used)] // Test code: panics on bad input are acceptable
675pub(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    /// A balance with nothing reserved, which is the only shape this venue can model.
725    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    /// Zero latency, so a driver's queue is exercised without any wall-clock sleeping.
737    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, // latency_ms
757            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    /// A spot config whose account already holds one open order, as `initial_state` seeds it.
771    ///
772    /// The only way an order can rest on a venue [`MockExchange`] drives: that venue is
773    /// [`VenueRegime::RequestPriced`], which refuses `OrderKind::Limit` outright.
774    ///
775    /// [`VenueRegime::RequestPriced`]: crate::exchange::mock::venue::VenueRegime::RequestPriced
776    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, // latency_ms
795            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    /// An open buy resting at `price`, retiring itself at `expiry`.
809    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    /// The spot fixture: `btc` of the base asset and `usdt` of the quote, both fully free.
836    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    /// A Market order carrying the snapshot the venue will price it against.
886    ///
887    /// The snapshot is set on the request rather than passed alongside it, because
888    /// [`RequestOpen::market`] is the venue's only price source and a second copy on the same path
889    /// could disagree with it.
890    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, // Market orders don't have a limit price
908                    d(quantity),
909                    OrderKind::Market,
910                    TimeInForce::ImmediateOrCancel,
911                )
912            },
913        }
914    }
915
916    /// `request`, carrying `effect` as its [`RequestOpen::margin_effect`].
917    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    /// The terms a plainly resting order carries: on the book until somebody cancels it.
940    pub(super) fn gtc() -> TimeInForce {
941        TimeInForce::GoodUntilCancelled { post_only: false }
942    }
943
944    /// A Limit order at `price`, carrying its own `cid` so several can rest at once.
945    ///
946    /// `market` is deliberately `None`: a limit order is judged and priced against the venue's own
947    /// market, never against a snapshot its sender stamped.
948    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    /// Both sides of a book and no trade price, which is what an L1-only feed supplies.
973    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    /// A trade price and no book, which is what a trades-only feed supplies.
982    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    /// A snapshot whose three prices are all `price`, wrapped as the venue receives it.
991    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)] // Test code: panics on bad input are acceptable
1003mod 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    /// A [`SimulatedVenue`] balance is an absolute restatement, so emission order *is* the answer.
1020    ///
1021    /// Each fill used to be emitted by its own spawned task. Those tasks raced, so the snapshots
1022    /// `9_999_500`, `9_999_000`, `9_998_500`, ... reached the client in arbitrary order and the
1023    /// last to arrive won — which is not the same thing as the last to be booked. That went
1024    /// unnoticed only because the timestamp each snapshot carries is derived from a clock
1025    /// contaminated with wall-clock time, and so happened to be unique and increasing, letting the
1026    /// engine discard the out-of-order ones as stale. Correcting that clock removes the accident.
1027    ///
1028    /// Asserted on the broadcast stream rather than on any downstream balance, because the ordering
1029    /// is the venue's contract to keep: the engine cannot repair snapshots it receives out of order.
1030    ///
1031    /// # Why this must stay multi-threaded
1032    /// The race is between spawned tasks. On a `current_thread` runtime they are serialised by the
1033    /// scheduler and this test passes against the unfixed code, detecting nothing.
1034    #[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        // Closing the request channel ends `run`, which drains the emitter before returning.
1070        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        // Each fill also restates the base it received; this test pins the quote it paid with.
1085        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        // One quote snapshot per fill, each debiting the quote asset by the same notional.
1095        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    /// The async driver builds its venue from the configuration, slippage included.
1106    #[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    /// A cancel is rejected by the venue, and the driver answers the channel it was asked on.
1139    ///
1140    /// The rejection itself is the venue's own; what this pins is that the driver
1141    /// forwards it rather than leaving the caller's `oneshot` to be dropped — which is what the
1142    /// previous, mistyped implementation did, since it could not produce the channel's response
1143    /// type at all.
1144    /// An order retired by its own deadline reaches the client, in the emitter's queue rather than
1145    /// around it.
1146    ///
1147    /// The sweep runs synchronously at the top of `run`'s loop, while the emitter may still be
1148    /// asleep waiting out an earlier batch's latency. Sending it straight to `event_tx` would let
1149    /// it overtake that batch — and a balance is an absolute restatement, so a snapshot applied out
1150    /// of order leaves the client holding the wrong number, not merely a late one. This asserts the
1151    /// expiry lands *after* the fill booked before it.
1152    #[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        // Non-zero, so the emitter is genuinely asleep when the next request sweeps.
1163        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        // Booked before the deadline, so its fill is queued and still waiting out its latency.
1169        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        // A second request stamped past the deadline: advancing the clock for it sweeps the
1179        // seeded order while the fill above is still in the emitter's queue.
1180        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    /// The mock client's order-state lookup reaches the venue's ledger and back: an order that
1247    /// filled is reported, and one the venue never held is omitted.
1248    #[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    /// What the mock client's borrow capacity query gets back from a running driver, for an order
1304    /// on BTCUSDT on `side`, from a venue configured as `config`.
1305    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    /// The mock client's borrow capacity query reaches the venue and back.
1330    #[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    /// A venue's refusal to report a capacity reaches the client as the venue's API error.
1352    #[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    /// A request-priced driver answers a fetch after the output booked before it, so a balance
1374    /// read while a fill is in flight is never older than the fill's own restatement.
1375    #[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    /// `new_market_driven` refuses a venue that would ignore its feed.
1416    #[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    /// A market-driven driver under test: its channels, a clock moving with tokio's, and the
1443    /// running task.
1444    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        /// The instant the clock read when the driver started.
1450        start: DateTime<Utc>,
1451    }
1452
1453    impl Paper {
1454        /// Runs `config` market-driven, on a clock that reads `start` now and moves with tokio's.
1455        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        /// The next account event, waiting for it in tokio's (paused) time.
1520        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        /// Account events up to and including the first trade.
1528        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    /// A [`MarketFeedEvent::Stale`] naming the instrument, and the feed ending, each reach the
1549    /// venue: under `Wait`, an immediate-or-cancel order on a market observed fresh a moment ago
1550    /// is then refused, naming the feed.
1551    #[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                // Replacing the only sender ends the feed.
1581                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    /// A limit order rests on a market-driven driver, and fills when a later observation crosses
1602    /// it, with nothing requesting the fill. Its response takes the whole of `latency_ms`, not one
1603    /// and a half times it.
1604    #[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    /// A request is booked once its outbound half of the latency has passed, against the market
1636    /// held then, as a backtest of the same venue books it.
1637    #[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                // The snapshot a request carries prices nothing on a market-driven venue.
1651                buy_request("0.01", market_prices("1")),
1652            ))
1653            .unwrap();
1654        // Still on its way: the market moves before the order reaches the venue at 50 ms.
1655        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    /// An order's deadline falls when it is due, with no market event or request to move the
1674    /// clock.
1675    #[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    /// A lagged feed ends the account stream after everything booked before it, and the venue
1718    /// keeps serving.
1719    #[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    /// Once the feed ends, the driver trades on the last market it saw.
1751    #[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        // The replacement's receiver is dropped at once; dropping the original ends the feed.
1757        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    /// A clock that steps back does not move the venue back: the stamp holds at the latest one.
1766    #[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        // Settles the observation before the clock steps back.
1778        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    /// A request-priced driver books a request half its latency after the request was made, and a
1787    /// trades fetch bounded by that very instant includes the trade.
1788    #[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    /// Closing the request channel does not drop a request still on its way to the venue.
1829    #[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    /// The driver's `Debug` names it, whichever mode it runs in.
1853    #[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    /// Before the venue has observed an instrument a market order is refused, while a resting
1863    /// limit is accepted and can then be cancelled.
1864    #[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    /// A deadline further out than a tokio timer can wait is retired at its own instant, not early.
1900    #[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}