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::FetchTrades {
385                response_tx,
386                start,
387                end,
388            } => {
389                let mut trades = self.venue.trades(start);
390                trades.retain(|trade| trade.time_exchange <= end);
391                emit.respond(Vec::new(), response_tx, trades);
392            }
393            MockExchangeRequestKind::CancelOrder {
394                response_tx,
395                request,
396            } => {
397                // The venue releases what the cancelled order held before returning, so the
398                // restatement it owes precedes the response, exactly as an open's does.
399                let outcome = self.venue.cancel_order(request);
400                emit.respond(outcome.events, response_tx, outcome.response);
401            }
402            MockExchangeRequestKind::OpenOrder {
403                response_tx,
404                request,
405            } => {
406                let outcome = self.venue.open_order(request);
407                emit.respond(outcome.events, response_tx, outcome.response);
408            }
409        }
410    }
411
412    /// Applies this driver's latency model, then advances the venue to the resulting instant.
413    ///
414    /// Returns whatever the advance retired — see [`SimulatedVenue::advance_time`]. The caller must
415    /// deliver them; dropping them would leave the client holding an order the venue no longer has.
416    #[must_use]
417    fn advance_venue_time(&mut self, time_request: DateTime<Utc>) -> Vec<UnindexedAccountEvent> {
418        let client_to_exchange_latency = self.latency_ms / 2;
419
420        let time_exchange = time_request
421            .checked_add_signed(TimeDelta::milliseconds(client_to_exchange_latency as i64))
422            .unwrap_or(time_request);
423
424        self.venue.advance_time(time_exchange)
425    }
426
427    /// Drains queued venue output in order, emitting each batch's account events before its
428    /// response.
429    ///
430    /// Runs until `emit_rx` closes -- which happens when [`MockExchange::run`] returns -- and then
431    /// finishes whatever is still queued, so a shutdown cannot strand a booked fill or a swept
432    /// expiry.
433    async fn emit_queued_output(
434        mut emit_rx: mpsc::UnboundedReceiver<PendingEmission>,
435        event_tx: broadcast::Sender<UnindexedAccountEvent>,
436        exchange: ExchangeId,
437    ) {
438        while let Some(emission) = emit_rx.recv().await {
439            tokio::time::sleep_until(emission.ready_at).await;
440
441            // Order is the venue's contract; this preserves it verbatim.
442            for event in emission.events {
443                if event_tx.send(event).is_err() {
444                    error!(
445                        %exchange,
446                        "MockExchange failed to send AccountEvent notification to client"
447                    );
448                }
449            }
450
451            // Absent for events nothing requested, such as an order retired by its own deadline.
452            if let Some(respond) = emission.respond {
453                respond(exchange);
454            }
455        }
456    }
457
458    pub fn account_stream(&self) -> BoxStream<'static, UnindexedAccountEvent> {
459        futures::StreamExt::boxed(BroadcastStream::new(self.event_tx.subscribe()).map_while(
460            |result| match result {
461                Ok(event) => Some(event),
462                Err(error) => {
463                    error!(
464                        ?error,
465                        "MockExchange Broadcast AccountStream lagged - terminating"
466                    );
467                    None
468                }
469            },
470        ))
471    }
472}
473
474/// Sleeps until `at`, or forever when there is nothing to wait for; the `select!` branch using it
475/// is disabled in that case, so the forever is never awaited.
476async fn sleep_until(at: Option<tokio::time::Instant>) {
477    match at {
478        Some(at) => tokio::time::sleep_until(at).await,
479        None => std::future::pending().await,
480    }
481}
482
483/// A driver's clock, held so that it never moves backwards.
484struct MonotonicClock {
485    clock: MockExchangeClock,
486    latest: Option<DateTime<Utc>>,
487}
488
489impl MonotonicClock {
490    fn new(clock: MockExchangeClock) -> Self {
491        Self {
492            clock,
493            latest: None,
494        }
495    }
496
497    /// The instant to stamp now: the clock's, unless it is before the latest already stamped.
498    fn now(&mut self) -> DateTime<Utc> {
499        let now = (self.clock)();
500        self.raise_to(now)
501    }
502
503    /// The instant [`now`](Self::now) would return, without recording it.
504    fn peek(&self) -> DateTime<Utc> {
505        let now = (self.clock)();
506        self.latest.map_or(now, |latest| latest.max(now))
507    }
508
509    /// Moves the clock to at least `time`, returning the instant to stamp.
510    fn raise_to(&mut self, time: DateTime<Utc>) -> DateTime<Utc> {
511        let latest = self.latest.map_or(time, |latest| latest.max(time));
512        self.latest = Some(latest);
513        latest
514    }
515}
516
517/// The emission queue as the serving loop sees it: what it owes the client, and how long each
518/// batch waits before it is sent.
519struct Emit {
520    tx: mpsc::UnboundedSender<PendingEmission>,
521    exchange: ExchangeId,
522    /// How long each batch waits after the venue produced it.
523    delay: Duration,
524}
525
526impl Emit {
527    fn new(
528        tx: mpsc::UnboundedSender<PendingEmission>,
529        exchange: ExchangeId,
530        delay_ms: u64,
531    ) -> Self {
532        Self {
533            tx,
534            exchange,
535            delay: Duration::from_millis(delay_ms),
536        }
537    }
538
539    /// Queues `events`, followed by `response` on `response_tx`.
540    fn respond<Response>(
541        &self,
542        events: Vec<UnindexedAccountEvent>,
543        response_tx: oneshot::Sender<Response>,
544        response: Response,
545    ) where
546        Response: Send + 'static,
547    {
548        let respond: Respond = Box::new(move |exchange| {
549            if response_tx.send(response).is_err() {
550                error!(
551                    %exchange,
552                    kind = std::any::type_name::<Response>(),
553                    "MockExchange failed to send oneshot response to client"
554                );
555            }
556        });
557        self.queue(events, Some(respond));
558    }
559
560    /// Queues account events, and any response they precede, for the emitter to drain in order.
561    ///
562    /// Everything this venue produces goes through here rather than to `event_tx` directly, so
563    /// that emission order equals production order across *all* of it -- fills, expiries, cancel
564    /// restatements and responses alike. Sending any of them inline would let it overtake a batch
565    /// still waiting out its latency, and an absolute balance delivered out of order is the wrong
566    /// balance, not merely a late one.
567    ///
568    /// # Why a queue rather than a task per fill
569    /// A balance is an **absolute snapshot**, not a delta: successive fills report `9_999_500`,
570    /// then `9_999_000`, then `9_998_500`. Applying them out of order therefore does not merely
571    /// reorder history, it yields the wrong balance. Each fill previously got its own
572    /// [`tokio::spawn`]. Those tasks raced -- at `latency_ms: 0` they all become runnable at once
573    /// -- so snapshots reached the client in arbitrary order and the last to *arrive* won. One
574    /// queue, drained by one task, makes emission order equal booking order by construction.
575    fn queue(&self, events: Vec<UnindexedAccountEvent>, respond: Option<Respond>) {
576        if events.is_empty() && respond.is_none() {
577            return;
578        }
579
580        // Stamped at production time, not at emission time, so the delay each batch waits is
581        // measured from when the venue produced *it*. The delay is fixed, so these are
582        // non-decreasing in queue order and the drain never sleeps away a latency twice.
583        let ready_at = tokio::time::Instant::now() + self.delay;
584
585        // Failure means the emitter is gone, which happens only once `run` has returned. There is
586        // no client left to notify, so there is nothing to do but say so.
587        if self
588            .tx
589            .send(PendingEmission {
590                ready_at,
591                events,
592                respond,
593            })
594            .is_err()
595        {
596            error!(
597                exchange = %self.exchange,
598                "MockExchange could not queue venue output: the emitter has stopped"
599            );
600        }
601    }
602}
603
604/// Sends one response to the channel owed it, erased so that every response type shares one queue.
605type Respond = Box<dyn FnOnce(ExchangeId) + Send>;
606
607/// One batch of venue output awaiting emission, held in booking order by [`MockExchange`]'s
608/// emitter queue.
609///
610/// # Why a response is optional
611/// Not everything this venue produces answers a request. An order retired by its own deadline is
612/// swept whenever the clock advances, so the events belong to nobody's response. They must still be
613/// delivered in the same ordered stream as everything else: a balance is an absolute restatement,
614/// so an expiry overtaking a fill still queued behind its latency would leave the client holding
615/// the wrong balance, which is the exact failure this queue exists to prevent.
616struct PendingEmission {
617    /// When this batch's latency expires, measured from the instant the venue produced it.
618    ready_at: tokio::time::Instant,
619    /// The account events the venue produced, in the order it requires.
620    events: Vec<UnindexedAccountEvent>,
621    /// Sends the response, once every event above has been emitted, if this batch answers a
622    /// request at all.
623    respond: Option<Respond>,
624}
625
626/// Fixtures shared by this module's driver tests and [`venue`]'s state-machine tests.
627///
628/// They live in the parent rather than beside either set of tests because a private item is visible
629/// to descendant modules: `venue::tests` can reach `mock::fixtures`, whereas `mock::tests` could not
630/// reach anything declared inside `venue`.
631#[cfg(test)]
632#[allow(clippy::unwrap_used, clippy::expect_used)] // Test code: panics on bad input are acceptable
633pub(crate) mod fixtures {
634    use crate::{
635        InstrumentAccountSnapshot, UnindexedAccountSnapshot,
636        balance::{AssetBalance, Balance},
637        client::mock::MockExecutionConfig,
638        fee::{FeeModelConfig, SpotFeeAsset},
639        fill::SimFillConfig,
640        market::MarketSnapshot,
641        order::{
642            Order, OrderEvent, OrderKey, OrderKind, TimeInForce, UnindexedOrder,
643            id::{ClientOrderId, OrderId, StrategyId, VenueOrderId},
644            request::{OrderRequestOpen, RequestOpen},
645            state::{Open, OrderState},
646        },
647        position::PositionReport,
648    };
649    use chrono::{DateTime, Utc};
650    use fnv::FnvHashMap;
651    use rust_decimal::Decimal;
652    use rustrade_instrument::{
653        Side, Underlying,
654        asset::name::AssetNameExchange,
655        exchange::ExchangeId,
656        instrument::{
657            Instrument,
658            kind::InstrumentKind,
659            name::{InstrumentNameExchange, InstrumentNameInternal},
660            quote::InstrumentQuoteAsset,
661        },
662    };
663
664    pub(super) const EXCHANGE: ExchangeId = ExchangeId::BinanceSpot;
665
666    pub(super) fn d(s: &str) -> Decimal {
667        s.parse().unwrap()
668    }
669
670    pub(super) fn base() -> AssetNameExchange {
671        AssetNameExchange::new("BTC")
672    }
673
674    pub(super) fn quote() -> AssetNameExchange {
675        AssetNameExchange::new("USDT")
676    }
677
678    pub(super) fn instrument_name() -> InstrumentNameExchange {
679        InstrumentNameExchange::new("BTCUSDT")
680    }
681
682    /// A balance with nothing reserved, which is the only shape this venue can model.
683    pub(super) fn funded(
684        asset: AssetNameExchange,
685        amount: Decimal,
686    ) -> AssetBalance<AssetNameExchange> {
687        AssetBalance {
688            asset,
689            balance: Balance::new(amount, amount),
690            time_exchange: Utc::now(),
691        }
692    }
693
694    /// Zero latency, so a driver's queue is exercised without any wall-clock sleeping.
695    pub(super) fn config_from_balances(
696        balances: Vec<AssetBalance<AssetNameExchange>>,
697        fee_model: FeeModelConfig,
698    ) -> MockExecutionConfig {
699        config_from_balances_with_fill(balances, fee_model, SimFillConfig::default())
700    }
701
702    pub(super) fn config_from_balances_with_fill(
703        balances: Vec<AssetBalance<AssetNameExchange>>,
704        fee_model: FeeModelConfig,
705        fill_model: SimFillConfig,
706    ) -> MockExecutionConfig {
707        MockExecutionConfig::new(
708            EXCHANGE,
709            UnindexedAccountSnapshot {
710                exchange: EXCHANGE,
711                balances,
712                instruments: vec![],
713            },
714            0, // latency_ms
715            fee_model,
716            fill_model,
717            SpotFeeAsset::default(),
718            crate::slippage::SlippageConfig::default(),
719            crate::fill::MarketOrderTiming::default(),
720            crate::fill::StaleMarketPolicy::default(),
721            None,
722            None,
723        )
724    }
725
726    /// A spot config whose account already holds one open order, as `initial_state` seeds it.
727    ///
728    /// The only way an order can rest on a venue [`MockExchange`] drives: that venue is
729    /// [`VenueRegime::RequestPriced`], which refuses `OrderKind::Limit` outright.
730    ///
731    /// [`VenueRegime::RequestPriced`]: crate::exchange::mock::venue::VenueRegime::RequestPriced
732    pub(super) fn spot_config_holding(
733        btc: &str,
734        usdt: &str,
735        order: UnindexedOrder,
736    ) -> MockExecutionConfig {
737        MockExecutionConfig::new(
738            EXCHANGE,
739            UnindexedAccountSnapshot {
740                exchange: EXCHANGE,
741                balances: vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
742                instruments: vec![InstrumentAccountSnapshot {
743                    instrument: instrument_name(),
744                    orders: vec![order],
745                    orders_complete: true,
746                    position: PositionReport::Unreported,
747                    isolated: None,
748                }],
749            },
750            0, // latency_ms
751            FeeModelConfig::default(),
752            SimFillConfig::default(),
753            SpotFeeAsset::default(),
754            crate::slippage::SlippageConfig::default(),
755            crate::fill::MarketOrderTiming::default(),
756            crate::fill::StaleMarketPolicy::default(),
757            None,
758            None,
759        )
760    }
761
762    /// An open buy resting at `price`, retiring itself at `expiry`.
763    pub(super) fn seeded_gtd(
764        cid: &str,
765        price: &str,
766        opened: DateTime<Utc>,
767        expiry: DateTime<Utc>,
768    ) -> UnindexedOrder {
769        Order {
770            key: OrderKey {
771                exchange: EXCHANGE,
772                instrument: instrument_name(),
773                strategy: StrategyId::new("test"),
774                cid: ClientOrderId::new(cid),
775            },
776            side: Side::Buy,
777            price: Some(d(price)),
778            quantity: d("0.01"),
779            kind: OrderKind::Limit,
780            time_in_force: TimeInForce::GoodTillDate { expiry },
781            state: OrderState::active(Open::new(
782                VenueOrderId::Assigned(OrderId::new(cid)),
783                opened,
784                rust_decimal::Decimal::ZERO,
785            )),
786        }
787    }
788
789    /// The spot fixture: `btc` of the base asset and `usdt` of the quote, both fully free.
790    pub(super) fn spot_config(
791        btc: &str,
792        usdt: &str,
793        fee_model: FeeModelConfig,
794    ) -> MockExecutionConfig {
795        config_from_balances(
796            vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
797            fee_model,
798        )
799    }
800
801    pub(super) fn spot_config_with_fill(
802        btc: &str,
803        usdt: &str,
804        fee_model: FeeModelConfig,
805        fill_model: SimFillConfig,
806    ) -> MockExecutionConfig {
807        config_from_balances_with_fill(
808            vec![funded(base(), d(btc)), funded(quote(), d(usdt))],
809            fee_model,
810            fill_model,
811        )
812    }
813
814    pub(super) fn instruments_of(
815        instrument: Instrument<ExchangeId, AssetNameExchange>,
816    ) -> FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
817        let mut instruments = FnvHashMap::default();
818        instruments.insert(instrument.name_exchange.clone(), instrument);
819        instruments
820    }
821
822    pub(super) fn spot_instruments()
823    -> FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
824        instruments_of(Instrument {
825            exchange: EXCHANGE,
826            name_internal: InstrumentNameInternal::new("btcusdt"),
827            name_exchange: instrument_name(),
828            underlying: Underlying {
829                base: base(),
830                quote: quote(),
831            },
832            quote: InstrumentQuoteAsset::UnderlyingQuote,
833            kind: InstrumentKind::Spot,
834            spec: None,
835            data_venue: None,
836        })
837    }
838
839    /// A Market order carrying the snapshot the venue will price it against.
840    ///
841    /// The snapshot is set on the request rather than passed alongside it, because
842    /// [`RequestOpen::market`] is the venue's only price source and a second copy on the same path
843    /// could disagree with it.
844    pub(super) fn request(
845        instrument: InstrumentNameExchange,
846        side: Side,
847        quantity: &str,
848        market: Option<MarketSnapshot>,
849    ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
850        OrderEvent {
851            key: OrderKey {
852                exchange: EXCHANGE,
853                instrument,
854                strategy: StrategyId::new("test"),
855                cid: ClientOrderId::new("test-cid"),
856            },
857            state: RequestOpen {
858                market,
859                ..RequestOpen::new(
860                    side,
861                    None, // Market orders don't have a limit price
862                    d(quantity),
863                    OrderKind::Market,
864                    TimeInForce::ImmediateOrCancel,
865                )
866            },
867        }
868    }
869
870    /// `request`, carrying `effect` as its [`RequestOpen::margin_effect`].
871    pub(super) fn with_effect(
872        mut request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
873        effect: crate::margin::MarginEffect,
874    ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
875        request.state = request.state.with_margin_effect(effect);
876        request
877    }
878
879    pub(super) fn buy_request(
880        quantity: &str,
881        market: Option<MarketSnapshot>,
882    ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
883        request(instrument_name(), Side::Buy, quantity, market)
884    }
885
886    pub(super) fn sell_request(
887        quantity: &str,
888        market: Option<MarketSnapshot>,
889    ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
890        request(instrument_name(), Side::Sell, quantity, market)
891    }
892
893    /// The terms a plainly resting order carries: on the book until somebody cancels it.
894    pub(super) fn gtc() -> TimeInForce {
895        TimeInForce::GoodUntilCancelled { post_only: false }
896    }
897
898    /// A Limit order at `price`, carrying its own `cid` so several can rest at once.
899    ///
900    /// `market` is deliberately `None`: a limit order is judged and priced against the venue's own
901    /// market, never against a snapshot its sender stamped.
902    pub(super) fn limit_request(
903        cid: &str,
904        side: Side,
905        quantity: &str,
906        price: &str,
907        time_in_force: TimeInForce,
908    ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
909        OrderEvent {
910            key: OrderKey {
911                exchange: EXCHANGE,
912                instrument: instrument_name(),
913                strategy: StrategyId::new("test"),
914                cid: ClientOrderId::new(cid),
915            },
916            state: RequestOpen::new(
917                side,
918                Some(d(price)),
919                d(quantity),
920                OrderKind::Limit,
921                time_in_force,
922            ),
923        }
924    }
925
926    /// Both sides of a book and no trade price, which is what an L1-only feed supplies.
927    pub(super) fn book(best_bid: &str, best_ask: &str) -> MarketSnapshot {
928        MarketSnapshot {
929            best_bid: Some(d(best_bid)),
930            best_ask: Some(d(best_ask)),
931            last_price: None,
932        }
933    }
934
935    /// A trade price and no book, which is what a trades-only feed supplies.
936    pub(super) fn traded(last_price: &str) -> MarketSnapshot {
937        MarketSnapshot {
938            best_bid: None,
939            best_ask: None,
940            last_price: Some(d(last_price)),
941        }
942    }
943
944    /// A snapshot whose three prices are all `price`, wrapped as the venue receives it.
945    pub(super) fn market_prices(price: &str) -> Option<MarketSnapshot> {
946        let p = Some(d(price));
947        Some(MarketSnapshot {
948            best_bid: p,
949            best_ask: p,
950            last_price: p,
951        })
952    }
953}
954
955#[cfg(test)]
956#[allow(clippy::unwrap_used, clippy::expect_used)] // Test code: panics on bad input are acceptable
957mod tests {
958    use super::{fixtures::*, *};
959    use crate::market::MarketSnapshot;
960    use crate::{
961        AccountEventKind,
962        fee::FeeModelConfig,
963        order::{
964            OrderEvent, OrderKey,
965            id::ClientOrderId,
966            request::RequestCancel,
967            state::{InactiveOrderState, OrderState},
968        },
969    };
970    use rust_decimal::Decimal;
971    use rustrade_instrument::Side;
972
973    /// A [`SimulatedVenue`] balance is an absolute restatement, so emission order *is* the answer.
974    ///
975    /// Each fill used to be emitted by its own spawned task. Those tasks raced, so the snapshots
976    /// `9_999_500`, `9_999_000`, `9_998_500`, ... reached the client in arbitrary order and the
977    /// last to arrive won — which is not the same thing as the last to be booked. That went
978    /// unnoticed only because the timestamp each snapshot carries is derived from a clock
979    /// contaminated with wall-clock time, and so happened to be unique and increasing, letting the
980    /// engine discard the out-of-order ones as stale. Correcting that clock removes the accident.
981    ///
982    /// Asserted on the broadcast stream rather than on any downstream balance, because the ordering
983    /// is the venue's contract to keep: the engine cannot repair snapshots it receives out of order.
984    ///
985    /// # Why this must stay multi-threaded
986    /// The race is between spawned tasks. On a `current_thread` runtime they are serialised by the
987    /// scheduler and this test passes against the unfixed code, detecting nothing.
988    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
989    async fn open_order_emissions_are_ordered_across_fills() {
990        const FILLS: usize = 8;
991        const DEBIT_PER_FILL: &str = "500";
992
993        let usdt_start = "10000000";
994
995        let (request_tx, request_rx) = mpsc::unbounded_channel();
996        let (event_tx, mut events) = broadcast::channel(64);
997
998        let exchange = MockExchange::new(
999            spot_config("100", usdt_start, FeeModelConfig::default()),
1000            request_rx,
1001            event_tx,
1002            spot_instruments(),
1003        );
1004
1005        let driver = tokio::spawn(exchange.run());
1006
1007        let mut responses = Vec::with_capacity(FILLS);
1008        for nth in 0..FILLS {
1009            let (response_tx, response_rx) = oneshot::channel();
1010            let mut request = buy_request("0.01", market_prices("50000"));
1011            request.key.cid = ClientOrderId::new(format!("cid-{nth}"));
1012
1013            request_tx
1014                .send(MockExchangeRequest::open_order(
1015                    Utc::now(),
1016                    response_tx,
1017                    request,
1018                ))
1019                .unwrap();
1020            responses.push(response_rx);
1021        }
1022
1023        // Closing the request channel ends `run`, which drains the emitter before returning.
1024        drop(request_tx);
1025        driver.await.unwrap();
1026
1027        for response in responses {
1028            let response = response.await.expect("every open is answered");
1029            assert!(
1030                matches!(
1031                    response.state,
1032                    OrderState::Active(_) | OrderState::Inactive(_)
1033                ),
1034                "the account is funded and the instrument priced, so every open fills: {response:?}"
1035            );
1036        }
1037
1038        // Each fill also restates the base it received; this test pins the quote it paid with.
1039        let balances = std::iter::from_fn(|| events.try_recv().ok())
1040            .filter_map(|event| match event.kind {
1041                AccountEventKind::BalanceSnapshot(snapshot) if snapshot.0.asset == quote() => {
1042                    Some(snapshot.0.balance.total)
1043                }
1044                _ => None,
1045            })
1046            .collect::<Vec<_>>();
1047
1048        // One quote snapshot per fill, each debiting the quote asset by the same notional.
1049        let expected = (1..=FILLS)
1050            .map(|nth| d(usdt_start) - d(DEBIT_PER_FILL) * Decimal::from(nth))
1051            .collect::<Vec<_>>();
1052
1053        assert_eq!(
1054            balances, expected,
1055            "balance snapshots must reach the client in the order the venue booked them"
1056        );
1057    }
1058
1059    /// The async driver builds its venue from the configuration, slippage included.
1060    #[tokio::test]
1061    async fn the_driver_fills_at_the_configured_slippage() {
1062        let (request_tx, request_rx) = mpsc::unbounded_channel();
1063        let (event_tx, mut events) = broadcast::channel(16);
1064        let config = MockExecutionConfig {
1065            slippage: crate::slippage::SlippageConfig::fixed(d("0.01")).unwrap(),
1066            ..spot_config("100", "10000000", FeeModelConfig::default())
1067        };
1068        let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1069        let driver = tokio::spawn(exchange.run());
1070
1071        let (response_tx, response_rx) = oneshot::channel();
1072        request_tx
1073            .send(MockExchangeRequest::open_order(
1074                Utc::now(),
1075                response_tx,
1076                buy_request("0.01", market_prices("50000")),
1077            ))
1078            .unwrap();
1079        response_rx.await.expect("the open is answered");
1080        drop(request_tx);
1081        driver.await.unwrap();
1082
1083        let prices = std::iter::from_fn(|| events.try_recv().ok())
1084            .filter_map(|event| match event.kind {
1085                AccountEventKind::Trade(trade) => Some(trade.price),
1086                _ => None,
1087            })
1088            .collect::<Vec<_>>();
1089        assert_eq!(prices, vec![d("50500")], "50,000 bought 1% above");
1090    }
1091
1092    /// A cancel is rejected by the venue, and the driver answers the channel it was asked on.
1093    ///
1094    /// The rejection itself is the venue's own; what this pins is that the driver
1095    /// forwards it rather than leaving the caller's `oneshot` to be dropped — which is what the
1096    /// previous, mistyped implementation did, since it could not produce the channel's response
1097    /// type at all.
1098    /// An order retired by its own deadline reaches the client, in the emitter's queue rather than
1099    /// around it.
1100    ///
1101    /// The sweep runs synchronously at the top of `run`'s loop, while the emitter may still be
1102    /// asleep waiting out an earlier batch's latency. Sending it straight to `event_tx` would let
1103    /// it overtake that batch — and a balance is an absolute restatement, so a snapshot applied out
1104    /// of order leaves the client holding the wrong number, not merely a late one. This asserts the
1105    /// expiry lands *after* the fill booked before it.
1106    #[tokio::test(start_paused = true)]
1107    async fn a_swept_expiry_is_emitted_behind_the_fill_booked_before_it() {
1108        let opened = Utc::now();
1109        let expiry = opened + chrono::TimeDelta::seconds(30);
1110
1111        let (request_tx, request_rx) = mpsc::unbounded_channel();
1112        let (event_tx, mut events) = broadcast::channel(64);
1113
1114        let mut config =
1115            spot_config_holding("100", "10000000", seeded_gtd("gtd", "1", opened, expiry));
1116        // Non-zero, so the emitter is genuinely asleep when the next request sweeps.
1117        config.latency_ms = 50;
1118
1119        let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1120        let driver = tokio::spawn(exchange.run());
1121
1122        // Booked before the deadline, so its fill is queued and still waiting out its latency.
1123        let (fill_tx, fill_rx) = oneshot::channel();
1124        request_tx
1125            .send(MockExchangeRequest::open_order(
1126                opened,
1127                fill_tx,
1128                buy_request("0.01", market_prices("50000")),
1129            ))
1130            .unwrap();
1131
1132        // A second request stamped past the deadline: advancing the clock for it sweeps the
1133        // seeded order while the fill above is still in the emitter's queue.
1134        let (snapshot_tx, snapshot_rx) = oneshot::channel();
1135        request_tx
1136            .send(MockExchangeRequest::fetch_account_snapshot(
1137                expiry + chrono::TimeDelta::seconds(1),
1138                snapshot_tx,
1139            ))
1140            .unwrap();
1141
1142        drop(request_tx);
1143        driver.await.unwrap();
1144        fill_rx.await.expect("the open is answered");
1145        let _ = snapshot_rx.await;
1146
1147        let kinds = std::iter::from_fn(|| events.try_recv().ok())
1148            .map(|event| match event.kind {
1149                AccountEventKind::BalanceSnapshot(_) => "balance",
1150                AccountEventKind::Trade(_) => "trade",
1151                AccountEventKind::OrderSnapshot(_) => "order",
1152                _ => "other",
1153            })
1154            .collect::<Vec<_>>();
1155
1156        assert_eq!(
1157            kinds,
1158            ["balance", "balance", "trade", "order"],
1159            "the fill's events must all precede the sweep, which for a seeded order — held with \
1160             no reservation — owes only its terminal snapshot"
1161        );
1162    }
1163
1164    #[tokio::test]
1165    async fn a_cancel_request_is_answered_rather_than_dropped() {
1166        let (request_tx, request_rx) = mpsc::unbounded_channel();
1167        let (event_tx, _events) = broadcast::channel(16);
1168
1169        let exchange = MockExchange::new(
1170            spot_config("100", "10000", FeeModelConfig::default()),
1171            request_rx,
1172            event_tx,
1173            spot_instruments(),
1174        );
1175        let driver = tokio::spawn(exchange.run());
1176
1177        let (response_tx, response_rx) = oneshot::channel();
1178        let open = buy_request("1", market_prices("50000"));
1179        request_tx
1180            .send(MockExchangeRequest::cancel_order(
1181                Utc::now(),
1182                response_tx,
1183                OrderEvent {
1184                    key: open.key,
1185                    state: RequestCancel { id: None },
1186                },
1187            ))
1188            .unwrap();
1189
1190        let response = response_rx.await.expect("a cancel must be answered");
1191        assert!(
1192            response.state.is_err(),
1193            "this venue rests no orders, so a cancel must be rejected: {response:?}"
1194        );
1195
1196        drop(request_tx);
1197        driver.await.unwrap();
1198    }
1199
1200    /// The mock client's order-state lookup reaches the venue's ledger and back: an order that
1201    /// filled is reported, and one the venue never held is omitted.
1202    #[tokio::test]
1203    async fn an_order_state_lookup_is_answered_from_the_ledger() {
1204        use crate::client::{OrderStatusClient, mock::MockExecution};
1205
1206        let (request_tx, request_rx) = mpsc::unbounded_channel();
1207        let (event_tx, event_rx) = broadcast::channel(16);
1208        let exchange = MockExchange::new(
1209            spot_config("100", "10000000", FeeModelConfig::default()),
1210            request_rx,
1211            event_tx,
1212            spot_instruments(),
1213        );
1214        let driver = tokio::spawn(exchange.run());
1215
1216        let open = buy_request("1", market_prices("50000"));
1217        let (response_tx, response_rx) = oneshot::channel();
1218        request_tx
1219            .send(MockExchangeRequest::open_order(
1220                Utc::now(),
1221                response_tx,
1222                open.clone(),
1223            ))
1224            .unwrap();
1225        let opened = response_rx.await.expect("an open must be answered");
1226        assert!(matches!(
1227            opened.state,
1228            OrderState::Inactive(InactiveOrderState::FullyFilled(_))
1229        ));
1230
1231        let client = MockExecution {
1232            mocked_exchange: EXCHANGE,
1233            clock: Utc::now,
1234            request_tx: request_tx.clone(),
1235            event_rx,
1236        };
1237        let unknown = OrderKey {
1238            cid: ClientOrderId::new("never-opened"),
1239            ..open.key.clone()
1240        };
1241        let ended = client
1242            .fetch_ended_orders(&[open.key.clone(), unknown])
1243            .await
1244            .expect("the venue is running");
1245
1246        let [order] = ended.as_slice() else {
1247            panic!("only the filled order is reported: {ended:?}");
1248        };
1249        assert_eq!(order.key, open.key);
1250        assert!(matches!(order.state, InactiveOrderState::FullyFilled(_)));
1251
1252        drop(client);
1253        drop(request_tx);
1254        driver.await.unwrap();
1255    }
1256
1257    /// A request-priced driver answers a fetch after the output booked before it, so a balance
1258    /// read while a fill is in flight is never older than the fill's own restatement.
1259    #[tokio::test(start_paused = true)]
1260    async fn a_fetch_is_answered_after_the_fill_booked_before_it() {
1261        let (request_tx, request_rx) = mpsc::unbounded_channel();
1262        let (event_tx, mut events) = broadcast::channel(64);
1263        let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1264        config.latency_ms = 50;
1265        let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1266        let driver = tokio::spawn(exchange.run());
1267
1268        let (open_tx, open_rx) = oneshot::channel();
1269        request_tx
1270            .send(MockExchangeRequest::open_order(
1271                Utc::now(),
1272                open_tx,
1273                buy_request("0.01", market_prices("50000")),
1274            ))
1275            .unwrap();
1276        let (balances_tx, balances_rx) = oneshot::channel();
1277        request_tx
1278            .send(MockExchangeRequest::fetch_balances(
1279                Utc::now(),
1280                vec![quote()],
1281                balances_tx,
1282            ))
1283            .unwrap();
1284
1285        balances_rx.await.expect("the fetch is answered");
1286        let kinds = std::iter::from_fn(|| events.try_recv().ok())
1287            .map(|event| event_name(&event))
1288            .collect::<Vec<_>>();
1289        assert!(
1290            kinds.contains(&"trade"),
1291            "the fill booked before the fetch is emitted before the fetch is answered: {kinds:?}"
1292        );
1293
1294        open_rx.await.expect("the open is answered");
1295        drop(request_tx);
1296        driver.await.unwrap();
1297    }
1298
1299    /// `new_market_driven` refuses a venue that would ignore its feed.
1300    #[test]
1301    #[should_panic(expected = "request-priced venue")]
1302    fn a_market_driven_driver_refuses_a_request_priced_venue() {
1303        let config = spot_config("0", "10000", FeeModelConfig::default());
1304        let (_request_tx, request_rx) = mpsc::unbounded_channel();
1305        let (event_tx, _events) = broadcast::channel(16);
1306        let _ = MockExchange::new_market_driven(
1307            SimulatedVenue::new(&config, spot_instruments()),
1308            0,
1309            request_rx,
1310            event_tx,
1311            futures::StreamExt::boxed(futures::stream::empty()),
1312            Arc::new(Utc::now),
1313        );
1314    }
1315
1316    fn event_name(event: &UnindexedAccountEvent) -> &'static str {
1317        match &event.kind {
1318            AccountEventKind::BalanceSnapshot(_) => "balance",
1319            AccountEventKind::Trade(_) => "trade",
1320            AccountEventKind::OrderSnapshot(_) => "order",
1321            AccountEventKind::StreamTerminated(_) => "terminated",
1322            _ => "other",
1323        }
1324    }
1325
1326    /// A market-driven driver under test: its channels, a clock moving with tokio's, and the
1327    /// running task.
1328    struct Paper {
1329        request_tx: mpsc::UnboundedSender<MockExchangeRequest>,
1330        market_tx: mpsc::UnboundedSender<MarketFeedEvent>,
1331        events: broadcast::Receiver<UnindexedAccountEvent>,
1332        driver: tokio::task::JoinHandle<()>,
1333        /// The instant the clock read when the driver started.
1334        start: DateTime<Utc>,
1335    }
1336
1337    impl Paper {
1338        /// Runs `config` market-driven, on a clock that reads `start` now and moves with tokio's.
1339        fn start(config: MockExecutionConfig) -> Self {
1340            let start = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap();
1341            let anchor = tokio::time::Instant::now();
1342            let clock: MockExchangeClock = Arc::new(move || {
1343                start + TimeDelta::from_std(anchor.elapsed()).expect("a test runs for less")
1344            });
1345            Self::start_on(config, clock, start)
1346        }
1347
1348        fn start_on(
1349            config: MockExecutionConfig,
1350            clock: MockExchangeClock,
1351            start: DateTime<Utc>,
1352        ) -> Self {
1353            let (request_tx, request_rx) = mpsc::unbounded_channel();
1354            let (event_tx, events) = broadcast::channel(256);
1355            let (market_tx, market_rx) = mpsc::unbounded_channel();
1356            let exchange = MockExchange::new_market_driven(
1357                SimulatedVenue::new_market_driven(&config, spot_instruments()),
1358                config.latency_ms,
1359                request_rx,
1360                event_tx,
1361                futures::StreamExt::boxed(tokio_stream::wrappers::UnboundedReceiverStream::new(
1362                    market_rx,
1363                )),
1364                clock,
1365            );
1366            Self {
1367                request_tx,
1368                market_tx,
1369                events,
1370                driver: tokio::spawn(exchange.run()),
1371                start,
1372            }
1373        }
1374
1375        fn observe(&self, snapshot: MarketSnapshot) {
1376            self.market_tx
1377                .send(MarketFeedEvent::Observation {
1378                    instrument: instrument_name(),
1379                    observation: MarketObservation::new(snapshot),
1380                })
1381                .unwrap();
1382        }
1383
1384        async fn open(
1385            &self,
1386            request: crate::order::request::OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
1387        ) -> crate::order::Order<
1388            ExchangeId,
1389            InstrumentNameExchange,
1390            crate::order::state::UnindexedOrderState,
1391        > {
1392            let (response_tx, response_rx) = oneshot::channel();
1393            self.request_tx
1394                .send(MockExchangeRequest::open_order(
1395                    Utc::now(),
1396                    response_tx,
1397                    request,
1398                ))
1399                .unwrap();
1400            response_rx.await.expect("an open is answered")
1401        }
1402
1403        /// The next account event, waiting for it in tokio's (paused) time.
1404        async fn next_event(&mut self) -> UnindexedAccountEvent {
1405            tokio::time::timeout(std::time::Duration::from_secs(3600), self.events.recv())
1406                .await
1407                .expect("an account event arrives")
1408                .expect("the account channel is open")
1409        }
1410
1411        /// Account events up to and including the first trade.
1412        async fn until_trade(
1413            &mut self,
1414        ) -> crate::trade::Trade<AssetNameExchange, InstrumentNameExchange> {
1415            loop {
1416                if let AccountEventKind::Trade(trade) = self.next_event().await.kind {
1417                    return trade;
1418                }
1419            }
1420        }
1421
1422        async fn stop(self) {
1423            drop(self.request_tx);
1424            self.driver.await.unwrap();
1425        }
1426    }
1427
1428    fn priced(price: &str) -> MarketSnapshot {
1429        market_prices(price).unwrap()
1430    }
1431
1432    /// A limit order rests on a market-driven driver, and fills when a later observation crosses
1433    /// it, with nothing requesting the fill. Its response takes the whole of `latency_ms`, not one
1434    /// and a half times it.
1435    #[tokio::test(start_paused = true)]
1436    async fn a_paper_limit_rests_then_fills_when_the_market_crosses_it() {
1437        let mut config = spot_config("0", "100000", FeeModelConfig::default());
1438        config.latency_ms = 100;
1439        let mut paper = Paper::start(config);
1440        paper.observe(priced("50000"));
1441
1442        let sent = tokio::time::Instant::now();
1443        let response = paper
1444            .open(limit_request("rest", Side::Buy, "1", "49000", gtc()))
1445            .await;
1446        assert!(
1447            matches!(response.state, OrderState::Active(_)),
1448            "a limit below the market rests: {response:?}"
1449        );
1450        assert_eq!(
1451            sent.elapsed(),
1452            std::time::Duration::from_millis(100),
1453            "half the latency to the venue, half back"
1454        );
1455
1456        paper.observe(priced("48000"));
1457        let trade = paper.until_trade().await;
1458        assert_eq!(trade.quantity, d("1"));
1459        assert!(
1460            trade.price <= d("49000"),
1461            "a buy limit never pays more: {trade:?}"
1462        );
1463        paper.stop().await;
1464    }
1465
1466    /// A request is booked once its outbound half of the latency has passed, against the market
1467    /// held then, as a backtest of the same venue books it.
1468    #[tokio::test(start_paused = true)]
1469    async fn a_paper_request_is_booked_against_the_market_when_it_arrives() {
1470        let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1471        config.latency_ms = 100;
1472        let mut paper = Paper::start(config);
1473        paper.observe(priced("50000"));
1474
1475        let (response_tx, response_rx) = oneshot::channel();
1476        paper
1477            .request_tx
1478            .send(MockExchangeRequest::open_order(
1479                Utc::now(),
1480                response_tx,
1481                // The snapshot a request carries prices nothing on a market-driven venue.
1482                buy_request("0.01", market_prices("1")),
1483            ))
1484            .unwrap();
1485        // Still on its way: the market moves before the order reaches the venue at 50 ms.
1486        tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1487        paper.observe(priced("51000"));
1488
1489        let trade = paper.until_trade().await;
1490        assert_eq!(
1491            trade.price,
1492            d("51000"),
1493            "booked against the market as it arrived"
1494        );
1495        assert_eq!(
1496            trade.time_exchange,
1497            paper.start + TimeDelta::milliseconds(50),
1498            "stamped when it reached the venue"
1499        );
1500        response_rx.await.expect("the open is answered");
1501        paper.stop().await;
1502    }
1503
1504    /// An order's deadline falls when it is due, with no market event or request to move the
1505    /// clock.
1506    #[tokio::test(start_paused = true)]
1507    async fn a_paper_order_expires_at_its_deadline_in_a_quiet_market() {
1508        let config = spot_config("0", "100000", FeeModelConfig::default());
1509        let mut paper = Paper::start(config);
1510        paper.observe(priced("50000"));
1511        let expiry = paper.start + TimeDelta::seconds(10);
1512        let started = tokio::time::Instant::now();
1513
1514        let response = paper
1515            .open(limit_request(
1516                "gtd",
1517                Side::Buy,
1518                "1",
1519                "49000",
1520                crate::order::TimeInForce::GoodTillDate { expiry },
1521            ))
1522            .await;
1523        assert!(matches!(response.state, OrderState::Active(_)));
1524
1525        let expired = loop {
1526            let event = paper.next_event().await;
1527            if let AccountEventKind::OrderSnapshot(order) = event.kind
1528                && matches!(
1529                    order.0.state,
1530                    OrderState::Inactive(InactiveOrderState::Expired(_))
1531                )
1532            {
1533                break order.0;
1534            }
1535        };
1536        let OrderState::Inactive(InactiveOrderState::Expired(expired)) = expired.state else {
1537            unreachable!("matched above")
1538        };
1539        assert_eq!(expired.time_exchange, expiry, "retired at its own deadline");
1540        assert!(
1541            started.elapsed() >= std::time::Duration::from_secs(10),
1542            "and not before that time has passed: {:?}",
1543            started.elapsed()
1544        );
1545        paper.stop().await;
1546    }
1547
1548    /// A lagged feed ends the account stream after everything booked before it, and the venue
1549    /// keeps serving.
1550    #[tokio::test(start_paused = true)]
1551    async fn a_lagged_paper_feed_ends_the_account_stream_after_the_fills_before_it() {
1552        let config = spot_config("0", "10000000", FeeModelConfig::default());
1553        let mut paper = Paper::start(config);
1554        paper.observe(priced("50000"));
1555        paper.open(buy_request("0.01", None)).await;
1556        paper
1557            .market_tx
1558            .send(MarketFeedEvent::Lagged { missed: 3 })
1559            .unwrap();
1560
1561        let mut kinds = Vec::new();
1562        while kinds.last() != Some(&"terminated") {
1563            kinds.push(event_name(&paper.next_event().await));
1564        }
1565        assert!(
1566            kinds.contains(&"trade"),
1567            "the fill booked before the lag precedes the termination: {kinds:?}"
1568        );
1569
1570        let response = paper.open(buy_request("0.01", None)).await;
1571        assert!(
1572            matches!(
1573                response.state,
1574                OrderState::Inactive(InactiveOrderState::FullyFilled(_))
1575            ),
1576            "the venue keeps trading after the lag: {response:?}"
1577        );
1578        paper.stop().await;
1579    }
1580
1581    /// Once the feed ends, the driver trades on the last market it saw.
1582    #[tokio::test(start_paused = true)]
1583    async fn a_paper_driver_keeps_trading_once_its_feed_ends() {
1584        let config = spot_config("0", "10000000", FeeModelConfig::default());
1585        let mut paper = Paper::start(config);
1586        paper.observe(priced("50000"));
1587        // The replacement's receiver is dropped at once; dropping the original ends the feed.
1588        let (replacement, _) = mpsc::unbounded_channel();
1589        drop(std::mem::replace(&mut paper.market_tx, replacement));
1590
1591        paper.open(buy_request("0.01", None)).await;
1592        assert_eq!(paper.until_trade().await.price, d("50000"));
1593        paper.stop().await;
1594    }
1595
1596    /// A clock that steps back does not move the venue back: the stamp holds at the latest one.
1597    #[tokio::test(start_paused = true)]
1598    async fn a_paper_clock_that_steps_back_does_not_move_the_venue_back() {
1599        let later = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap();
1600        let reading = Arc::new(std::sync::Mutex::new(later));
1601        let clock: MockExchangeClock = {
1602            let reading = Arc::clone(&reading);
1603            Arc::new(move || *reading.lock().unwrap())
1604        };
1605        let config = spot_config("0", "10000000", FeeModelConfig::default());
1606        let mut paper = Paper::start_on(config, clock, later);
1607        paper.observe(priced("50000"));
1608        // Settles the observation before the clock steps back.
1609        tokio::time::sleep(std::time::Duration::from_millis(1)).await;
1610        *reading.lock().unwrap() = later - TimeDelta::seconds(100);
1611
1612        paper.open(buy_request("0.01", None)).await;
1613        assert_eq!(paper.until_trade().await.time_exchange, later);
1614        paper.stop().await;
1615    }
1616
1617    /// A request-priced driver books a request half its latency after the request was made, and a
1618    /// trades fetch bounded by that very instant includes the trade.
1619    #[tokio::test]
1620    async fn a_request_priced_request_is_booked_half_the_latency_after_it_was_made() {
1621        let (request_tx, request_rx) = mpsc::unbounded_channel();
1622        let (event_tx, _events) = broadcast::channel(16);
1623        let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1624        config.latency_ms = 100;
1625        let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1626        let driver = tokio::spawn(exchange.run());
1627
1628        let made = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap();
1629        let (open_tx, open_rx) = oneshot::channel();
1630        request_tx
1631            .send(MockExchangeRequest::open_order(
1632                made,
1633                open_tx,
1634                buy_request("0.01", market_prices("50000")),
1635            ))
1636            .unwrap();
1637        open_rx.await.expect("the open is answered");
1638
1639        let booked = made + TimeDelta::milliseconds(50);
1640        let (trades_tx, trades_rx) = oneshot::channel();
1641        request_tx
1642            .send(MockExchangeRequest::fetch_trades(
1643                made, trades_tx, made, booked,
1644            ))
1645            .unwrap();
1646        let trades = trades_rx.await.expect("the fetch is answered");
1647        assert_eq!(
1648            trades
1649                .iter()
1650                .map(|trade| trade.time_exchange)
1651                .collect::<Vec<_>>(),
1652            vec![booked],
1653            "booked at half the latency, and a bound at that instant includes it"
1654        );
1655        drop(request_tx);
1656        driver.await.unwrap();
1657    }
1658
1659    /// Closing the request channel does not drop a request still on its way to the venue.
1660    #[tokio::test(start_paused = true)]
1661    async fn a_paper_request_in_flight_is_answered_after_the_request_channel_closes() {
1662        let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1663        config.latency_ms = 100;
1664        let paper = Paper::start(config);
1665        paper.observe(priced("50000"));
1666
1667        let (response_tx, response_rx) = oneshot::channel();
1668        paper
1669            .request_tx
1670            .send(MockExchangeRequest::open_order(
1671                Utc::now(),
1672                response_tx,
1673                buy_request("0.01", None),
1674            ))
1675            .unwrap();
1676        drop(paper.request_tx);
1677
1678        let response = response_rx.await.expect("the open is still answered");
1679        assert!(matches!(response.state, OrderState::Inactive(_)));
1680        paper.driver.await.unwrap();
1681    }
1682
1683    /// The driver's `Debug` names it, whichever mode it runs in.
1684    #[tokio::test]
1685    async fn a_driver_debug_names_the_driver() {
1686        let (_request_tx, request_rx) = mpsc::unbounded_channel();
1687        let (event_tx, _events) = broadcast::channel(16);
1688        let config = spot_config("0", "10000000", FeeModelConfig::default());
1689        let exchange = MockExchange::new(config, request_rx, event_tx, spot_instruments());
1690        assert!(format!("{exchange:?}").contains("MockExchange"));
1691    }
1692
1693    /// Before the venue has observed an instrument a market order is refused, while a resting
1694    /// limit is accepted and can then be cancelled.
1695    #[tokio::test(start_paused = true)]
1696    async fn a_paper_venue_refuses_a_market_order_before_the_first_observation_and_cancels_a_rest()
1697    {
1698        let mut config = spot_config("0", "10000000", FeeModelConfig::default());
1699        config.latency_ms = 100;
1700        let paper = Paper::start(config);
1701
1702        let refused = paper.open(buy_request("0.01", None)).await;
1703        assert!(
1704            matches!(refused.state, OrderState::Inactive(_)),
1705            "nothing to price a market order from: {refused:?}"
1706        );
1707
1708        let rest = limit_request("rest", Side::Buy, "1", "49000", gtc());
1709        let key = rest.key.clone();
1710        let resting = paper.open(rest).await;
1711        assert!(matches!(resting.state, OrderState::Active(_)));
1712
1713        let (response_tx, response_rx) = oneshot::channel();
1714        paper
1715            .request_tx
1716            .send(MockExchangeRequest::cancel_order(
1717                Utc::now(),
1718                response_tx,
1719                OrderEvent {
1720                    key,
1721                    state: RequestCancel { id: None },
1722                },
1723            ))
1724            .unwrap();
1725        let cancelled = response_rx.await.expect("the cancel is answered");
1726        assert!(cancelled.state.is_ok(), "{cancelled:?}");
1727        paper.stop().await;
1728    }
1729
1730    /// A deadline further out than a tokio timer can wait is retired at its own instant, not early.
1731    #[tokio::test(start_paused = true)]
1732    async fn a_paper_deadline_beyond_the_longest_sleep_is_retired_on_time() {
1733        let config = spot_config("0", "100000", FeeModelConfig::default());
1734        let mut paper = Paper::start(config);
1735        paper.observe(priced("50000"));
1736        let expiry = paper.start + TimeDelta::days(3 * 365);
1737        let started = tokio::time::Instant::now();
1738
1739        paper
1740            .open(limit_request(
1741                "far",
1742                Side::Buy,
1743                "1",
1744                "49000",
1745                crate::order::TimeInForce::GoodTillDate { expiry },
1746            ))
1747            .await;
1748
1749        let expired = loop {
1750            let event = paper
1751                .events
1752                .recv()
1753                .await
1754                .expect("the account channel is open");
1755            if let AccountEventKind::OrderSnapshot(order) = event.kind
1756                && let OrderState::Inactive(InactiveOrderState::Expired(expired)) = order.0.state
1757            {
1758                break expired;
1759            }
1760        };
1761        assert_eq!(expired.time_exchange, expiry);
1762        assert!(started.elapsed() >= std::time::Duration::from_secs(3 * 365 * 24 * 60 * 60));
1763        paper.stop().await;
1764    }
1765}