Skip to main content

rustrade_execution/exchange/mock/
orders.rs

1//! Resting orders, held in the order a venue would match them.
2
3use crate::order::{
4    Order, TimeInForce, UnindexedOrder,
5    id::ClientOrderId,
6    state::{ActiveOrderState, Open},
7};
8use chrono::{DateTime, Utc};
9use fnv::FnvHashMap;
10use rust_decimal::Decimal;
11use rustrade_instrument::{
12    Side, asset::name::AssetNameExchange, exchange::ExchangeId,
13    instrument::name::InstrumentNameExchange,
14};
15use std::collections::{BTreeMap, BTreeSet, hash_map::Entry};
16use thiserror::Error;
17
18/// An open order held by a simulated venue.
19pub type OpenOrder = Order<ExchangeId, InstrumentNameExchange, Open>;
20
21/// The balance a venue is holding against one resting order.
22///
23/// Recorded so the venue settles on a fill, and releases on a cancel, **exactly** what it took when
24/// the order rested — rather than recomputing an amount that a changed fee model or contract size
25/// could make disagree with the one the client was told about. A CFD fill is the exception: what
26/// it costs depends on the position it trades against, which can move while the order rests, so
27/// its hold is released and the fill costed afresh (see `SimulatedVenue`'s reserved balances).
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct Reservation {
30    /// The asset the order pays with: quote for a buy or a CFD, base for a spot sell.
31    pub asset: AssetNameExchange,
32    /// How much of it is held, inclusive of the fee the fill will charge. For a CFD, net of what
33    /// the fill will pay back by closing a position, so an order that only reduces one holds
34    /// nothing.
35    ///
36    /// Held against the order's **unfilled** quantity alone — its whole quantity for an order that
37    /// rested without trading, and the remainder alone for one the book filled in part before it
38    /// rested, or one a configured `initial_state` seeded already part-filled.
39    pub amount: Decimal,
40    /// How much of [`amount`](Self::amount) a margin account lent the order when it was placed,
41    /// repaid if the order ends unfilled. Zero on a cash account, and for an order the account
42    /// could pay for itself.
43    pub borrowed: Decimal,
44}
45
46/// One resting order, and whatever the venue is holding against it.
47///
48/// The two are one value rather than two collections because they have exactly one lifetime: an
49/// order that leaves the book takes its reservation with it, and a reservation that outlives its
50/// order is held against nothing.
51#[derive(Debug, Clone)]
52pub struct RestingOrder {
53    pub order: OpenOrder,
54    /// What the venue holds against [`order`](Self::order), or `None` if it holds nothing.
55    ///
56    /// `None` for an order that arrived through a configured `initial_state`: the venue did not
57    /// take that balance and has nothing of its own to give back. See [`OpenOrders`].
58    pub reservation: Option<Reservation>,
59}
60
61/// An order [`OpenOrders::insert`] refused, because another is already resting under its
62/// [`ClientOrderId`].
63///
64/// Holds the refused order and its reservation, untouched. Boxed because an order is large and the
65/// refusal is the rare path.
66#[derive(Debug, Clone, Error)]
67#[error("an order is already resting under {}", .0.order.key.cid)]
68pub struct AlreadyResting(pub Box<RestingOrder>);
69
70/// One order's place in one side of one instrument's queue.
71///
72/// Ordered so that a [`BTreeMap`]'s ascending iteration is **best first** for the side it belongs
73/// to. Derived field order is the priority rule: price, then arrival, then insertion.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
75struct QueuePosition {
76    /// The order's price, ranked for its side: negated on the buy side, so the **highest** bid and
77    /// the **lowest** offer both sort first.
78    price_rank: Decimal,
79    /// When the venue accepted the order. Earlier is matched first, at equal price.
80    time_exchange: DateTime<Utc>,
81    /// Insertion sequence, which breaks the tie between two orders accepted at the same instant.
82    ///
83    /// Without it two such orders compare equal and a `BTreeMap` keeps only one of them.
84    seq: u64,
85}
86
87impl QueuePosition {
88    fn new(side: Side, price: Decimal, time_exchange: DateTime<Utc>, seq: u64) -> Self {
89        Self {
90            price_rank: match side {
91                Side::Buy => -price,
92                Side::Sell => price,
93            },
94            time_exchange,
95            seq,
96        }
97    }
98}
99
100/// A venue's open orders, indexed for lookup by id **and** for matching in price-time order.
101///
102/// # Why two indices
103///
104/// The two accesses want opposite things. Cancelling an order, and answering a snapshot, address
105/// one order by [`ClientOrderId`]. Matching a tick walks an instrument's orders from the best price
106/// down, and must stop as soon as the price stops crossing.
107///
108/// Holding only the map makes matching a sort of every open order on every tick. Holding only the
109/// queues makes a cancel a scan. The map owns the orders, the queues hold ids.
110///
111/// # Why price-time and not "whatever the map yields"
112///
113/// A tick that crosses two resting orders with only enough balance to fill one fills whichever
114/// comes first. Read off a hash map, that is an arbitrary choice which a rebuild or a different
115/// `ClientOrderId` can silently reverse, so two runs of one backtest need not agree. Price-time
116/// priority is both the reproducible rule and the one real venues use.
117///
118/// # Orders with no price
119///
120/// An order resting without a limit price cannot be ranked against one, and is not a thing a venue
121/// holds: an order with no price to wait at has nothing to wait for. Such an order is kept in the
122/// map — a snapshot must report everything the account holds — but is absent from the queues, so
123/// matching can never reach it. It arrives one of two ways:
124/// - through a configured `initial_state`, and then nothing matches it, ever;
125/// - through [`hold`](Self::hold): a market order the venue is holding until its instrument's
126///   next bar, or its next fresh observation. Those are kept in a fourth index, per instrument and
127///   in arrival order, which [`take_held`](Self::take_held) empties when what they wait for
128///   arrives.
129///
130/// # Why a third index for deadlines
131///
132/// A [`TimeInForce::GoodTillDate`] order retires at a stated instant, and the venue sweeps for such
133/// orders on every clock advance *and* every market tick. Neither of the other two indices can
134/// answer "which orders are past their deadline": a deadline is unrelated to price, so the
135/// price-time queues cannot be walked with a `take_while` the way matching walks them, and the map
136/// is unordered. Answering from either would mean scanning every open order on every tick.
137///
138/// A third index therefore holds **only** the orders that have a deadline, keyed by it. A sweep is
139/// a range query that stops at the first order still working, so it costs nothing when nothing can
140/// expire — which is every venue whose orders are all `GoodUntilCancelled`, including every
141/// market-order-only backtest.
142///
143/// # Reservations, and the ones that are not there
144///
145/// An order the venue booked itself carries the [`Reservation`] the venue took for it, so a fill
146/// settles and a cancel releases the same amount that was held — see [`RestingOrder`].
147///
148/// An order seeded by a configured `initial_state` carries `None`, because the venue never took
149/// anything for it. Cancelling such an order therefore releases nothing and restates no balance.
150/// That is the honest answer rather than a convenient one: the snapshot says an order is resting
151/// and says what the balances are, but not which order any held portion belongs to, so a venue
152/// that credited a release would be inventing the amount.
153#[derive(Debug, Default)]
154pub struct OpenOrders {
155    by_id: FnvHashMap<ClientOrderId, RestingOrder>,
156    /// Per instrument and side, best first. Values are keys into [`by_id`](Self::by_id).
157    queues: FnvHashMap<(InstrumentNameExchange, Side), BTreeMap<QueuePosition, ClientOrderId>>,
158    /// Every order carrying a deadline, earliest first — see the type's note on deadlines.
159    ///
160    /// Keyed on `(deadline, cid)` rather than on the deadline alone because two orders can share
161    /// one instant. No further tie-break is needed: a [`ClientOrderId`] is unique across open
162    /// orders, since [`by_id`](Self::by_id) is keyed by it, so the pair is already total — and
163    /// therefore a sweep's order is reproducible between runs.
164    expiries: BTreeSet<(DateTime<Utc>, ClientOrderId)>,
165    /// Market orders held on each instrument, earliest arrival first — see the type's
166    /// note on orders with no price. Values are keys into [`by_id`](Self::by_id).
167    held: FnvHashMap<InstrumentNameExchange, Vec<ClientOrderId>>,
168    /// Monotone across every instrument, so insertion order is total even across queues.
169    seq: u64,
170}
171
172/// The instant `order` retires at of its own accord, or `None` if it works until cancelled.
173fn deadline_of(order: &OpenOrder) -> Option<DateTime<Utc>> {
174    match order.time_in_force {
175        TimeInForce::GoodTillDate { expiry } => Some(expiry),
176        _ => None,
177    }
178}
179
180impl OpenOrders {
181    /// Inserts `order` holding `reservation` against it, unless an order is already resting under
182    /// its [`ClientOrderId`].
183    ///
184    /// # Errors
185    /// [`AlreadyResting`], carrying `order` and `reservation` back untouched, if an order is already
186    /// held under its id. The book is left exactly as it was: the order already resting keeps its
187    /// queue position, its deadline and its own reservation. `reservation` is handed back rather
188    /// than dropped because it may already be held against the account, and only the caller knows
189    /// the ledger to give it back to.
190    ///
191    /// `reservation` is `None` only for an order the venue did not book itself — see the type's
192    /// note on reservations. It is a parameter rather than a later setter so that an order cannot
193    /// reach the book in a state where what is held against it has not yet been decided.
194    pub fn insert(
195        &mut self,
196        order: OpenOrder,
197        reservation: Option<Reservation>,
198    ) -> Result<(), AlreadyResting> {
199        let cid = order.key.cid.clone();
200        let vacant = match self.by_id.entry(cid.clone()) {
201            Entry::Occupied(_) => {
202                return Err(AlreadyResting(Box::new(RestingOrder {
203                    order,
204                    reservation,
205                })));
206            }
207            Entry::Vacant(vacant) => vacant,
208        };
209
210        if let Some(expiry) = deadline_of(&order) {
211            self.expiries.insert((expiry, cid.clone()));
212        }
213
214        if let Some(price) = order.price {
215            self.seq += 1;
216            self.queues
217                .entry((order.key.instrument.clone(), order.side))
218                .or_default()
219                .insert(
220                    QueuePosition::new(order.side, price, order.state.time_exchange, self.seq),
221                    cid,
222                );
223        }
224
225        vacant.insert(RestingOrder { order, reservation });
226        Ok(())
227    }
228
229    /// Holds `order`, a market order with no price, until its venue releases it, with
230    /// `reservation` held against it.
231    ///
232    /// Kept in the map like any open order, so a snapshot reports it and a cancel finds it, and
233    /// queued behind every order already held on its instrument. Matching never reaches it: only
234    /// [`take_held`](Self::take_held) does.
235    ///
236    /// # Errors
237    /// [`AlreadyResting`], as for [`insert`](Self::insert), leaving the book exactly as it was.
238    ///
239    /// # Panics
240    /// Panics if `order` carries a price. A priced order is queued for matching, which a held one
241    /// must never be, and would leave the held index out of step with the book.
242    pub fn hold(
243        &mut self,
244        order: OpenOrder,
245        reservation: Option<Reservation>,
246    ) -> Result<(), AlreadyResting> {
247        assert!(
248            order.price.is_none(),
249            "held order {} carries a price, so it belongs on the price-time queues",
250            order.key.cid
251        );
252
253        let instrument = order.key.instrument.clone();
254        let cid = order.key.cid.clone();
255        self.insert(order, reservation)?;
256        self.held.entry(instrument).or_default().push(cid);
257        Ok(())
258    }
259
260    /// Whether any order is held on `instrument`.
261    pub fn holds(&self, instrument: &InstrumentNameExchange) -> bool {
262        self.held.contains_key(instrument)
263    }
264
265    /// The orders held on `instrument`, earliest arrival first, still held.
266    ///
267    /// For a caller that releases some of them and leaves the rest waiting: it
268    /// [`remove`](Self::remove)s those it releases, which takes them out of the hold too.
269    pub fn held_on(&self, instrument: &InstrumentNameExchange) -> &[ClientOrderId] {
270        self.held.get(instrument).map_or(&[], Vec::as_slice)
271    }
272
273    /// The orders held on `instrument`, earliest arrival first, no longer held.
274    ///
275    /// Each stays in the map until its caller [`remove`](Self::remove)s it, which is what lets the
276    /// caller take its reservation with it.
277    pub fn take_held(&mut self, instrument: &InstrumentNameExchange) -> Vec<ClientOrderId> {
278        self.held.remove(instrument).unwrap_or_default()
279    }
280
281    /// Every order held, on any instrument: by instrument, then earliest arrival
282    /// first. A total order, so whatever a caller does with them happens in the same order on
283    /// every run.
284    pub fn held(&self) -> Vec<ClientOrderId> {
285        let mut instruments = self.held.keys().collect::<Vec<_>>();
286        instruments.sort_unstable();
287        instruments
288            .into_iter()
289            .flat_map(|instrument| self.held[instrument].iter().cloned())
290            .collect()
291    }
292
293    /// Whether an order is resting under `cid`.
294    pub fn contains(&self, cid: &ClientOrderId) -> bool {
295        self.by_id.contains_key(cid)
296    }
297
298    /// Removes and returns the order held under `cid`, with whatever is held against it.
299    pub fn remove(&mut self, cid: &ClientOrderId) -> Option<RestingOrder> {
300        let resting = self.by_id.remove(cid)?;
301        self.unindex(&resting.order);
302        Some(resting)
303    }
304
305    /// Every order whose deadline has been reached by `now`, earliest deadline first.
306    ///
307    /// "Reached" is inclusive: an order with a deadline of exactly `now` is returned, because a
308    /// [`TimeInForce::GoodTillDate`] order stops working *at* its stated instant rather than after
309    /// it. The venue relies on this to make a deadline an unconditional cutoff — see
310    /// `SimulatedVenue::advance_time`.
311    ///
312    /// Returned rather than iterated because the caller retires each one, which borrows `self`
313    /// mutably. The walk stops at the first order still working, so it costs a single comparison
314    /// when nothing is due, whatever the size of the book.
315    pub fn expired_as_of(&self, now: DateTime<Utc>) -> Vec<ClientOrderId> {
316        self.expiries
317            .iter()
318            .take_while(|(expiry, _)| *expiry <= now)
319            .map(|(_, cid)| cid.clone())
320            .collect()
321    }
322
323    /// The earliest deadline of any open order, or `None` if none has one.
324    pub fn next_expiry(&self) -> Option<DateTime<Utc>> {
325        self.expiries.first().map(|(expiry, _)| *expiry)
326    }
327
328    /// Takes `order` out of every index that ranks it, leaving [`by_id`](Self::by_id) to its caller.
329    fn unindex(&mut self, order: &OpenOrder) {
330        self.dequeue(order);
331        self.unhold(order);
332
333        if let Some(expiry) = deadline_of(order) {
334            self.expiries.remove(&(expiry, order.key.cid.clone()));
335        }
336    }
337
338    pub fn get(&self, cid: &ClientOrderId) -> Option<&OpenOrder> {
339        self.by_id.get(cid).map(|resting| &resting.order)
340    }
341
342    pub fn len(&self) -> usize {
343        self.by_id.len()
344    }
345
346    pub fn is_empty(&self) -> bool {
347        self.by_id.is_empty()
348    }
349
350    /// Every open order, in no particular order.
351    ///
352    /// Callers that need a reproducible sequence must impose one — [`resting`](Self::resting) for
353    /// matching, or an explicit sort for a snapshot.
354    pub fn iter(&self) -> impl Iterator<Item = &OpenOrder> + '_ {
355        self.by_id.values().map(|resting| &resting.order)
356    }
357
358    /// One instrument's resting orders on one side, **best price first** and earliest first within
359    /// a price.
360    ///
361    /// This is the order a venue matches in, so a caller filling against a crossing tick takes them
362    /// as they come and stops at the first that does not cross.
363    pub fn resting(
364        &self,
365        instrument: &InstrumentNameExchange,
366        side: Side,
367    ) -> impl Iterator<Item = &OpenOrder> + '_ {
368        self.queues
369            .get(&(instrument.clone(), side))
370            .into_iter()
371            .flat_map(|queue| queue.values())
372            // `by_id` and `queues` are written only together, so a queued id is always present.
373            .filter_map(|cid| self.by_id.get(cid))
374            .map(|resting| &resting.order)
375    }
376
377    /// Takes `order` out of the held index, if it is still there.
378    ///
379    /// Only an order with no price can be held, and a held one has left the index already when
380    /// [`take_held`](Self::take_held) handed it out, so this finds something only for a held order
381    /// cancelled or expired before its bar.
382    fn unhold(&mut self, order: &OpenOrder) {
383        if order.price.is_some() {
384            return;
385        }
386        let Some(held) = self.held.get_mut(&order.key.instrument) else {
387            return;
388        };
389        held.retain(|cid| *cid != order.key.cid);
390        if held.is_empty() {
391            self.held.remove(&order.key.instrument);
392        }
393    }
394
395    fn dequeue(&mut self, order: &OpenOrder) {
396        let Some(price) = order.price else {
397            // Never queued — see the type's note on orders with no price.
398            return;
399        };
400
401        let key = (order.key.instrument.clone(), order.side);
402        let Some(queue) = self.queues.get_mut(&key) else {
403            return;
404        };
405
406        // The position's `seq` is not known from the order, so this finds it by value. Queues are
407        // per instrument and side, and an order appears in exactly one, so this is a scan of one
408        // instrument's book rather than of every open order.
409        let position = queue
410            .range(QueuePosition::new(order.side, price, order.state.time_exchange, 0)..)
411            .find(|(_, queued)| *queued == &order.key.cid)
412            .map(|(position, _)| *position);
413
414        // A priced order is queued by `insert` and dequeued only from here, so failing to find one
415        // means its `price`, `side` or `state.time_exchange` changed while it was on the book: the
416        // range above no longer covers where it was filed. That silently leaves a queued id with no
417        // order behind it, which `resting` then silently filters out — invisible in both
418        // directions, and it would corrupt price-time priority for every later match. Asserted
419        // rather than tolerated, because there is no correct way to carry on from it.
420        debug_assert!(
421            position.is_some(),
422            "OpenOrders lost the queue position of {} ({} {price} at {}): its ranking fields were \
423             mutated while it was on the book",
424            order.key.cid,
425            order.side,
426            order.state.time_exchange
427        );
428
429        if let Some(position) = position {
430            queue.remove(&position);
431        }
432
433        if queue.is_empty() {
434            self.queues.remove(&key);
435        }
436    }
437}
438
439/// Narrows an order's state to `Open`, or `None` if it is in any other state.
440pub(super) fn as_open(order: UnindexedOrder) -> Option<OpenOrder> {
441    match order.state {
442        crate::order::state::OrderState::Active(ActiveOrderState::Open(open)) => Some(Order {
443            key: order.key,
444            side: order.side,
445            price: order.price,
446            quantity: order.quantity,
447            kind: order.kind,
448            time_in_force: order.time_in_force,
449            state: open,
450        }),
451        _ => None,
452    }
453}
454
455#[cfg(test)]
456#[allow(clippy::unwrap_used, clippy::expect_used)] // Test code: panics acceptable
457mod tests {
458    use super::*;
459    use crate::order::{
460        OrderKey, OrderKind, TimeInForce,
461        id::{OrderId, StrategyId, VenueOrderId},
462    };
463    use rust_decimal_macros::dec;
464
465    fn at(millis: i64) -> DateTime<Utc> {
466        DateTime::from_timestamp_millis(millis).unwrap()
467    }
468
469    fn instrument() -> InstrumentNameExchange {
470        InstrumentNameExchange::new("btc_usdt")
471    }
472
473    fn gtd(cid: &str, expiry_millis: i64) -> OpenOrder {
474        let mut order = order(cid, Side::Buy, Some(dec!(100)), 1_000);
475        order.time_in_force = TimeInForce::GoodTillDate {
476            expiry: at(expiry_millis),
477        };
478        order
479    }
480
481    fn order(cid: &str, side: Side, price: Option<Decimal>, millis: i64) -> OpenOrder {
482        Order {
483            key: OrderKey {
484                exchange: ExchangeId::BinanceSpot,
485                instrument: instrument(),
486                strategy: StrategyId::new("test"),
487                cid: ClientOrderId::new(cid),
488            },
489            side,
490            price,
491            quantity: dec!(1),
492            kind: OrderKind::Limit,
493            time_in_force: TimeInForce::GoodUntilCancelled { post_only: false },
494            state: Open {
495                id: VenueOrderId::Assigned(OrderId::new(cid)),
496                time_exchange: at(millis),
497                filled_quantity: Decimal::ZERO,
498            },
499        }
500    }
501
502    /// A book holding `orders`, none of them with a reservation.
503    fn book(orders: impl IntoIterator<Item = OpenOrder>) -> OpenOrders {
504        let mut open = OpenOrders::default();
505        for order in orders {
506            open.insert(order, None).unwrap();
507        }
508        open
509    }
510
511    fn cids(orders: impl Iterator<Item = &'static OpenOrder>) -> Vec<String> {
512        orders.map(|order| order.key.cid.0.to_string()).collect()
513    }
514
515    fn resting_cids(open: &OpenOrders, side: Side) -> Vec<String> {
516        open.resting(&instrument(), side)
517            .map(|order| order.key.cid.0.to_string())
518            .collect()
519    }
520
521    /// The best bid is the highest one, and the earliest of those at the same price.
522    #[test]
523    fn buys_rest_highest_price_then_earliest_first() {
524        let open = book([
525            order("low", Side::Buy, Some(dec!(99)), 1_000),
526            order("high_late", Side::Buy, Some(dec!(101)), 3_000),
527            order("high_early", Side::Buy, Some(dec!(101)), 2_000),
528            order("mid", Side::Buy, Some(dec!(100)), 500),
529        ]);
530
531        assert_eq!(
532            resting_cids(&open, Side::Buy),
533            ["high_early", "high_late", "mid", "low"],
534            "price first, then arrival — not insertion order, and not the map's order"
535        );
536    }
537
538    /// The best offer is the lowest one. Same rule, opposite price direction.
539    #[test]
540    fn sells_rest_lowest_price_then_earliest_first() {
541        let open = book([
542            order("high", Side::Sell, Some(dec!(101)), 1_000),
543            order("low_late", Side::Sell, Some(dec!(99)), 3_000),
544            order("low_early", Side::Sell, Some(dec!(99)), 2_000),
545        ]);
546
547        assert_eq!(
548            resting_cids(&open, Side::Sell),
549            ["low_early", "low_late", "high"]
550        );
551    }
552
553    /// Two orders at one price and one instant must both survive.
554    ///
555    /// Without the insertion sequence in the key they compare equal, and a `BTreeMap` keeps one —
556    /// an order silently vanishing from the book rather than merely being mis-ranked.
557    #[test]
558    fn orders_at_the_same_price_and_instant_both_rest() {
559        let open = book([
560            order("first", Side::Buy, Some(dec!(100)), 1_000),
561            order("second", Side::Buy, Some(dec!(100)), 1_000),
562        ]);
563
564        assert_eq!(open.len(), 2);
565        assert_eq!(
566            resting_cids(&open, Side::Buy),
567            ["first", "second"],
568            "the tie is broken by insertion, which is deterministic"
569        );
570    }
571
572    #[test]
573    fn the_two_sides_are_separate_queues() {
574        let open = book([
575            order("bid", Side::Buy, Some(dec!(99)), 1_000),
576            order("ask", Side::Sell, Some(dec!(101)), 1_000),
577        ]);
578
579        assert_eq!(resting_cids(&open, Side::Buy), ["bid"]);
580        assert_eq!(resting_cids(&open, Side::Sell), ["ask"]);
581    }
582
583    #[test]
584    fn a_removed_order_leaves_the_queue() {
585        let mut open = book([
586            order("a", Side::Buy, Some(dec!(100)), 1_000),
587            order("b", Side::Buy, Some(dec!(99)), 1_000),
588        ]);
589
590        let removed = open.remove(&ClientOrderId::new("a")).expect("a is open");
591        assert_eq!(removed.order.key.cid, ClientOrderId::new("a"));
592
593        assert_eq!(resting_cids(&open, Side::Buy), ["b"]);
594        assert_eq!(open.len(), 1);
595        assert!(open.get(&ClientOrderId::new("a")).is_none());
596        assert!(open.remove(&ClientOrderId::new("a")).is_none());
597    }
598
599    /// Removing every order must leave nothing behind for the next one to trip over.
600    #[test]
601    fn emptying_the_book_empties_the_queues() {
602        let mut open = book([
603            order("a", Side::Buy, Some(dec!(100)), 1_000),
604            order("b", Side::Buy, Some(dec!(99)), 2_000),
605        ]);
606
607        open.remove(&ClientOrderId::new("a"));
608        open.remove(&ClientOrderId::new("b"));
609
610        assert!(open.is_empty());
611        assert!(resting_cids(&open, Side::Buy).is_empty());
612
613        open.insert(order("c", Side::Buy, Some(dec!(98)), 3_000), None)
614            .unwrap();
615        assert_eq!(resting_cids(&open, Side::Buy), ["c"]);
616    }
617
618    /// A second order under a resting id is refused, handed back whole with its reservation, and
619    /// the order already resting keeps its place.
620    #[test]
621    fn a_second_order_under_a_resting_id_is_refused_and_the_first_keeps_its_place() {
622        let mut open = book([
623            order("taken", Side::Buy, Some(dec!(100)), 1_000),
624            order("other", Side::Buy, Some(dec!(99)), 1_000),
625        ]);
626        let held = Reservation {
627            asset: AssetNameExchange::new("usdt"),
628            amount: dec!(98),
629            borrowed: Decimal::ZERO,
630        };
631
632        let Err(AlreadyResting(refused)) = open.insert(
633            order("taken", Side::Buy, Some(dec!(98)), 2_000),
634            Some(held.clone()),
635        ) else {
636            panic!("an id already resting is refused");
637        };
638
639        assert_eq!(
640            refused.order.price,
641            Some(dec!(98)),
642            "the refused order comes back"
643        );
644        assert_eq!(
645            refused.reservation,
646            Some(held),
647            "and so does what the caller may already hold against it"
648        );
649        assert_eq!(open.len(), 2, "nothing was added");
650        assert_eq!(
651            open.get(&ClientOrderId::new("taken"))
652                .map(|order| order.price),
653            Some(Some(dec!(100))),
654            "the resting order is the one still held"
655        );
656        assert_eq!(
657            resting_cids(&open, Side::Buy),
658            ["taken", "other"],
659            "at its own price and place, appearing exactly once"
660        );
661    }
662
663    /// An order with no limit price is held but cannot be matched.
664    #[test]
665    fn an_order_without_a_price_is_held_but_never_rests() {
666        let open = book([
667            order("priceless", Side::Buy, None, 1_000),
668            order("priced", Side::Buy, Some(dec!(100)), 1_000),
669        ]);
670
671        assert_eq!(open.len(), 2, "a snapshot must still report it");
672        assert!(open.get(&ClientOrderId::new("priceless")).is_some());
673        assert_eq!(
674            resting_cids(&open, Side::Buy),
675            ["priced"],
676            "an order with no price to wait at has nothing to wait for"
677        );
678    }
679
680    /// Only orders that carry a deadline are indexed for the sweep, and only once reached.
681    #[test]
682    fn only_orders_past_their_deadline_are_swept() {
683        let open = book([
684            order("no_deadline", Side::Buy, Some(dec!(100)), 1_000),
685            gtd("due", 5_000),
686            gtd("later", 9_000),
687        ]);
688
689        assert!(
690            open.expired_as_of(at(4_999)).is_empty(),
691            "nothing is due before the earliest deadline"
692        );
693        assert_eq!(
694            open.expired_as_of(at(5_000)),
695            [ClientOrderId::new("due")],
696            "a deadline is reached at its instant, not after it"
697        );
698        assert_eq!(
699            open.expired_as_of(at(9_999)),
700            [ClientOrderId::new("due"), ClientOrderId::new("later")],
701            "earliest deadline first, and an order without one is never swept"
702        );
703    }
704
705    /// Two orders sharing one deadline are both swept, in a reproducible order.
706    ///
707    /// Without the id in the key they compare equal and a `BTreeSet` keeps one — an order silently
708    /// outliving its own deadline.
709    #[test]
710    fn orders_sharing_a_deadline_are_both_swept_in_id_order() {
711        let open = book([gtd("b", 5_000), gtd("a", 5_000)]);
712
713        assert_eq!(
714            open.expired_as_of(at(5_000)),
715            [ClientOrderId::new("a"), ClientOrderId::new("b")],
716            "the tie is broken by id, which does not depend on map iteration"
717        );
718    }
719
720    /// An order that leaves the book takes its deadline with it, whichever way it left.
721    #[test]
722    fn a_removed_order_is_no_longer_swept() {
723        let mut open = book([gtd("gone", 5_000), gtd("stays", 5_000)]);
724
725        open.remove(&ClientOrderId::new("gone"));
726
727        assert_eq!(
728            open.expired_as_of(at(5_000)),
729            [ClientOrderId::new("stays")],
730            "a deadline that outlived its order would retire an order twice"
731        );
732    }
733
734    /// A refused order brings no deadline of its own, and the resting order keeps its own.
735    #[test]
736    fn a_refused_order_leaves_the_resting_ones_deadline() {
737        let mut open = book([gtd("taken", 5_000)]);
738
739        assert!(open.insert(gtd("taken", 9_000), None).is_err());
740
741        assert_eq!(
742            open.expired_as_of(at(9_000)),
743            [ClientOrderId::new("taken")],
744            "the resting order's deadline, once: the refused one's never reached the index"
745        );
746        assert_eq!(open.expired_as_of(at(5_000)), [ClientOrderId::new("taken")]);
747    }
748
749    /// An id is free again once its order has left the book, and the next order under it brings
750    /// its own deadline.
751    #[test]
752    fn an_id_is_free_again_once_its_order_leaves_the_book() {
753        let mut open = book([gtd("reused", 5_000)]);
754        open.remove(&ClientOrderId::new("reused")).unwrap();
755
756        open.insert(order("reused", Side::Buy, Some(dec!(100)), 6_000), None)
757            .unwrap();
758
759        assert!(
760            open.expired_as_of(at(9_000)).is_empty(),
761            "the first order's deadline left with it"
762        );
763        assert_eq!(resting_cids(&open, Side::Buy), ["reused"]);
764    }
765
766    #[test]
767    fn iter_reports_every_open_order() {
768        let open = book([
769            order("a", Side::Buy, Some(dec!(100)), 1_000),
770            order("b", Side::Sell, Some(dec!(101)), 1_000),
771            order("c", Side::Buy, None, 1_000),
772        ]);
773
774        let mut all = cids(Vec::leak(open.iter().cloned().collect::<Vec<_>>()).iter());
775        all.sort();
776        assert_eq!(all, ["a", "b", "c"]);
777    }
778
779    fn market(cid: &str, instrument: &str, millis: i64) -> OpenOrder {
780        let mut order = order(cid, Side::Buy, None, millis);
781        order.key.instrument = InstrumentNameExchange::new(instrument);
782        order.kind = OrderKind::Market;
783        order
784    }
785
786    fn ids(cids: Vec<ClientOrderId>) -> Vec<String> {
787        cids.into_iter().map(|cid| cid.0.to_string()).collect()
788    }
789
790    #[test]
791    fn held_orders_are_handed_out_once_per_instrument_in_arrival_order() {
792        let mut open = OpenOrders::default();
793        for order in [
794            market("b1", "eth_usdt", 1_000),
795            market("a1", "btc_usdt", 2_000),
796            market("b2", "eth_usdt", 3_000),
797        ] {
798            open.hold(order, None).unwrap();
799        }
800
801        assert!(open.holds(&InstrumentNameExchange::new("eth_usdt")));
802        assert_eq!(
803            ids(open.held()),
804            ["a1", "b1", "b2"],
805            "by instrument, then earliest arrival first"
806        );
807        assert_eq!(open.len(), 3, "a snapshot reports what is held");
808        assert!(
809            resting_cids(&open, Side::Buy).is_empty(),
810            "and matching never reaches it"
811        );
812
813        let eth = InstrumentNameExchange::new("eth_usdt");
814        assert_eq!(ids(open.take_held(&eth)), ["b1", "b2"]);
815        assert!(!open.holds(&eth));
816        assert!(open.take_held(&eth).is_empty(), "handed out once");
817        assert_eq!(open.len(), 3, "still in the map until removed");
818    }
819
820    #[test]
821    fn removing_a_held_order_stops_it_being_held() {
822        let mut open = OpenOrders::default();
823        open.hold(market("gone", "btc_usdt", 1_000), None).unwrap();
824        open.hold(market("kept", "btc_usdt", 2_000), None).unwrap();
825
826        assert!(open.remove(&ClientOrderId::new("gone")).is_some());
827        assert_eq!(ids(open.held()), ["kept"]);
828
829        assert!(open.remove(&ClientOrderId::new("kept")).is_some());
830        assert!(
831            !open.holds(&InstrumentNameExchange::new("btc_usdt")),
832            "an instrument with nothing left held holds nothing"
833        );
834    }
835
836    #[test]
837    fn a_held_order_under_a_resting_id_is_refused_and_nothing_changes() {
838        let mut open = book([order("taken", Side::Buy, Some(dec!(100)), 1_000)]);
839
840        let refused = open.hold(market("taken", "btc_usdt", 2_000), None);
841
842        assert!(refused.is_err());
843        assert!(open.held().is_empty());
844        assert_eq!(resting_cids(&open, Side::Buy), ["taken"]);
845    }
846}