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