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