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