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