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