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