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