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