Skip to main content

egui_charts/data/
trades.rs

1//! The trades of one series, kept for order flow.
2//!
3//! A page adds trades up to the oldest held trade and leaves later ones to the live stream. A trade
4//! whose id is already held is dropped, whatever its time, so history and live trades join exactly
5//! when both carry ids. Otherwise time decides: a page adds a trade at the instant of the oldest
6//! held trade only if it and every held trade at that instant carry ids, and a live trade is
7//! dropped if a page added a trade at or after it and the two do not both carry ids. Trades
8//! sharing the boundary instant may be lost but are never counted twice.
9
10use crate::model::{OrderSide, Tick, next_revision};
11use chrono::{DateTime, Utc};
12use std::collections::{HashMap, VecDeque};
13
14/// Size taken from one resting order, or from an unidentified one, at one price.
15#[derive(Clone, Debug, PartialEq)]
16#[non_exhaustive]
17pub struct ConsumedOrder {
18    /// Exchange id of the resting order; fills without one are listed apart, as their orders
19    /// cannot be told from each other.
20    pub resting_order_id: Option<u64>,
21    /// Price of the fills.
22    pub price: f64,
23    /// Total retained size filled, saturating at the largest finite value on overflow.
24    pub size: f64,
25}
26
27/// Fills of one explicitly identified aggressive order, or one fill without an order id.
28/// Only retained fills count: eviction from the raw store also removes them from this view.
29#[derive(Clone, Debug, PartialEq)]
30#[non_exhaustive]
31pub struct CombinedTrade {
32    /// Explicit aggressor order id; unknown fills are never grouped together.
33    pub aggressor_order_id: Option<u64>,
34    /// Time of the first retained fill.
35    pub first_time: DateTime<Utc>,
36    /// Time of the last retained fill.
37    pub last_time: DateTime<Utc>,
38    /// Price of the first retained fill.
39    pub first_price: f64,
40    /// Price of the last retained fill.
41    pub last_price: f64,
42    /// Volume-weighted price; the first price for zero total size.
43    pub price: f64,
44    /// Total retained size, saturating at the largest finite value on overflow.
45    pub size: f64,
46    /// Number of retained fills.
47    pub fills: usize,
48    /// Aggressor side, unknown if fills disagree.
49    pub side: Option<OrderSide>,
50    /// The resting orders the retained fills consumed, in fill order; consecutive fills of
51    /// one identified order at one price are one entry. Their sizes add up to
52    /// [`size`](Self::size).
53    pub consumed: Vec<ConsumedOrder>,
54    /// Whether the order is fully reconstructed: its aggressor id is explicit and every
55    /// retained fill names its resting order. Otherwise `consumed` lacks evidence.
56    pub complete: bool,
57}
58
59/// Combines explicit aggressor ids, in order of their first fill. Unknown ids stay separate.
60/// Use raw fills for per-price volume. This allocates at most one entry per input fill and
61/// ignores invalid fills. A bounded input gives a bounded combined view.
62pub fn combine_trades<'a>(trades: impl IntoIterator<Item = &'a Tick>) -> Vec<CombinedTrade> {
63    let mut indices: HashMap<u64, usize> = HashMap::new();
64    let mut combined: Vec<CombinedTrade> = Vec::new();
65    for fill in trades.into_iter().filter(|t| valid(t)) {
66        if let Some(&at) = fill
67            .aggressor_order_id
68            .as_ref()
69            .and_then(|id| indices.get(id))
70        {
71            let trade = &mut combined[at];
72            let scale = trade.size.max(fill.size);
73            if scale > 0.0 {
74                let weight = (fill.size / scale) / (trade.size / scale + fill.size / scale);
75                trade.price += (fill.price - trade.price) * weight;
76            }
77            trade.size = (trade.size + fill.size).min(f64::MAX);
78            trade.last_time = fill.ts;
79            trade.last_price = fill.price;
80            trade.fills += 1;
81            if trade.side != fill.side {
82                trade.side = None;
83            }
84            trade.consume(fill);
85        } else {
86            if let Some(id) = fill.aggressor_order_id {
87                indices.insert(id, combined.len());
88            }
89            let mut trade = CombinedTrade {
90                aggressor_order_id: fill.aggressor_order_id,
91                first_time: fill.ts,
92                last_time: fill.ts,
93                first_price: fill.price,
94                last_price: fill.price,
95                price: fill.price,
96                size: fill.size,
97                fills: 1,
98                side: fill.side,
99                consumed: Vec::new(),
100                complete: fill.aggressor_order_id.is_some(),
101            };
102            trade.consume(fill);
103            combined.push(trade);
104        }
105    }
106    combined
107}
108
109impl CombinedTrade {
110    /// Adds the resting order that `fill` consumed to the breakdown.
111    fn consume(&mut self, fill: &Tick) {
112        self.complete &= fill.resting_order_id.is_some();
113        match self.consumed.last_mut().filter(|c| {
114            fill.resting_order_id.is_some()
115                && c.resting_order_id == fill.resting_order_id
116                && c.price == fill.price
117        }) {
118            Some(order) => order.size = (order.size + fill.size).min(f64::MAX),
119            None => self.consumed.push(ConsumedOrder {
120                resting_order_id: fill.resting_order_id,
121                price: fill.price,
122                size: fill.size,
123            }),
124        }
125    }
126}
127
128/// Trades in arrival order, at most `capacity` of them: the oldest go first.
129pub(crate) struct Trades {
130    trades: VecDeque<Tick>,
131    capacity: usize,
132    /// Position stamps of the held trades, relative to `front` by wrapping subtraction.
133    ids: HashMap<u64, usize>,
134    /// Position stamp of the deque's front, moving with pushes and evictions at either end.
135    front: usize,
136    /// Held trades prepended by history pages, before the live suffix.
137    paged: usize,
138    /// Time of the newest trade a page added: live trades without an id at or before it repeat it.
139    paged_until: Option<DateTime<Utc>>,
140    /// Time of the newest trade without an id a page added: any live trade at or before it may
141    /// repeat it.
142    paged_without_id_until: Option<DateTime<Utc>>,
143    /// Time of the newest trade dropped to stay within `capacity`: evidence before it is incomplete.
144    lost_until: Option<DateTime<Utc>>,
145    /// Changes whenever a trade is added or evicted.
146    revision: u64,
147}
148
149impl Trades {
150    pub(crate) fn new(capacity: usize) -> Self {
151        Self {
152            trades: VecDeque::new(),
153            capacity,
154            ids: HashMap::new(),
155            front: 0,
156            paged: 0,
157            paged_until: None,
158            paged_without_id_until: None,
159            lost_until: None,
160            revision: next_revision(),
161        }
162    }
163
164    /// Identifies the held trades: it differs after any trade was added or evicted.
165    #[cfg_attr(
166        not(feature = "widget"),
167        expect(dead_code, reason = "only the chart widget consumes a feed")
168    )]
169    pub(crate) fn revision(&self) -> u64 {
170        self.revision
171    }
172
173    /// Time of the newest trade dropped to stay within capacity, evicted or left out of a page.
174    #[cfg_attr(
175        not(feature = "widget"),
176        expect(dead_code, reason = "only the chart widget consumes a feed")
177    )]
178    pub(crate) fn lost_until(&self) -> Option<DateTime<Utc>> {
179        self.lost_until
180    }
181
182    /// Held executions that arrived live, including those sharing a page's boundary time.
183    #[cfg_attr(
184        not(any(feature = "widget", test)),
185        expect(dead_code, reason = "only the chart widget consumes a feed")
186    )]
187    pub(crate) fn live(&self) -> impl Iterator<Item = &Tick> {
188        self.trades.iter().skip(self.paged)
189    }
190
191    /// An empty store of the same capacity, for the next drain.
192    pub(crate) fn empty(&self) -> Self {
193        Self::new(self.capacity)
194    }
195
196    pub(crate) fn iter(&self) -> impl Iterator<Item = &Tick> {
197        self.trades.iter()
198    }
199
200    /// Time of the oldest held trade.
201    #[cfg_attr(
202        not(feature = "widget"),
203        expect(dead_code, reason = "only the chart widget consumes a feed")
204    )]
205    pub(crate) fn oldest(&self) -> Option<DateTime<Utc>> {
206        self.trades.iter().map(|trade| trade.ts).min()
207    }
208
209    /// Trades a page can still add before the store evicts live ones.
210    #[cfg_attr(
211        not(feature = "widget"),
212        expect(dead_code, reason = "only the chart widget consumes a feed")
213    )]
214    pub(crate) fn room(&self) -> usize {
215        self.capacity.saturating_sub(self.trades.len())
216    }
217
218    /// Appends a trade validated by the caller unless its id is held, evicting the oldest beyond
219    /// capacity. Repeated ids can complete missing order ids without counting the fill again.
220    pub(crate) fn push(&mut self, trade: Tick) {
221        self.insert(trade, &mut |_, _| ());
222    }
223
224    /// [`push`](Self::push), calling `changed` with the trade if it is added (`true`) and with
225    /// the trade it evicts (`false`).
226    fn insert(&mut self, trade: Tick, changed: &mut impl FnMut(&Tick, bool)) {
227        if trade.id.is_some_and(|id| self.ids.contains_key(&id)) {
228            self.annotate(&trade);
229            return;
230        }
231        if let Some(id) = trade.id {
232            self.ids
233                .insert(id, self.front.wrapping_add(self.trades.len()));
234        }
235        self.revision = next_revision();
236        changed(&trade, true);
237        self.trades.push_back(trade);
238        if self.trades.len() > self.capacity
239            && let Some(evicted) = self.trades.pop_front()
240        {
241            self.front = self.front.wrapping_add(1);
242            self.paged = self.paged.saturating_sub(1);
243            if let Some(id) = evicted.id {
244                self.ids.remove(&id);
245            }
246            self.lost_until = self.lost_until.max(Some(evicted.ts));
247            changed(&evicted, false);
248        }
249    }
250
251    /// A tape print may precede the order execution or recorded fill that identifies its orders.
252    fn annotate(&mut self, trade: &Tick) {
253        if (trade.aggressor_order_id.is_some() || trade.resting_order_id.is_some())
254            && let Some(at) = trade.id.and_then(|id| self.ids.get(&id))
255        {
256            let held = &mut self.trades[at.wrapping_sub(self.front)];
257            held.aggressor_order_id = held.aggressor_order_id.or(trade.aggressor_order_id);
258            held.resting_order_id = held.resting_order_id.or(trade.resting_order_id);
259        }
260    }
261
262    /// Appends `newer`, the trades pushed since the last drain, without those a page already
263    /// added. Calls `changed` with each trade added (`true`) or evicted (`false`).
264    pub(crate) fn append(&mut self, newer: Trades, changed: &mut impl FnMut(&Tick, bool)) {
265        self.lost_until = self.lost_until.max(newer.lost_until);
266        for trade in newer.trades {
267            let paged_until = match trade.id {
268                Some(_) => self.paged_without_id_until,
269                None => self.paged_until,
270            };
271            if trade.id.is_some_and(|id| self.ids.contains_key(&id))
272                || paged_until.is_none_or(|until| trade.ts > until)
273            {
274                self.insert(trade, changed);
275            }
276        }
277    }
278
279    /// Puts the trades of a history page, oldest first, in front of the held ones, as many as fit,
280    /// so a page never evicts newer trades. Calls `changed` with each trade added. Returns the
281    /// number of invalid trades dropped.
282    pub(crate) fn prepend(
283        &mut self,
284        mut page: Vec<Tick>,
285        changed: &mut impl FnMut(&Tick, bool),
286    ) -> u64 {
287        let received = page.len();
288        page.retain(valid);
289        let rejected = (received - page.len()) as u64;
290        for trade in &page {
291            self.annotate(trade);
292        }
293        // Stable, so trades sharing a time keep the feed's order.
294        page.sort_by_key(|trade| trade.ts);
295        if let Some(oldest) = self.trades.iter().map(|trade| trade.ts).min() {
296            let ids_at_oldest = (self.trades.iter())
297                .filter(|held| held.ts == oldest)
298                .all(|held| held.id.is_some());
299            page.retain(|trade| {
300                trade.ts < oldest || (trade.ts == oldest && trade.id.is_some() && ids_at_oldest)
301            });
302        }
303        let mut room = self.capacity.saturating_sub(self.trades.len());
304        let mut added = Vec::new();
305        for trade in page.into_iter().rev() {
306            if trade.id.is_some_and(|id| self.ids.contains_key(&id)) {
307                continue;
308            }
309            if room == 0 {
310                // The newest trade left out; the older ones are lost with it.
311                self.lost_until = self.lost_until.max(Some(trade.ts));
312                break;
313            }
314            room -= 1;
315            if let Some(id) = trade.id {
316                self.ids
317                    .insert(id, self.front.wrapping_sub(added.len() + 1));
318            }
319            added.push(trade);
320        }
321        for trade in added {
322            self.paged_until = self.paged_until.max(Some(trade.ts));
323            if trade.id.is_none() {
324                self.paged_without_id_until = self.paged_without_id_until.max(Some(trade.ts));
325            }
326            self.revision = next_revision();
327            changed(&trade, true);
328            self.front = self.front.wrapping_sub(1);
329            self.paged += 1;
330            self.trades.push_front(trade);
331        }
332        rejected
333    }
334}
335
336/// A finite price and a finite, non-negative size.
337pub(super) fn valid(trade: &Tick) -> bool {
338    trade.price.is_finite() && trade.size.is_finite() && trade.size >= 0.0
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344    use chrono::TimeDelta;
345
346    /// A trade `second` seconds after 10:00, told apart by its price.
347    fn trade(second: i64, price: f64) -> Tick {
348        let ts = "2024-03-15T10:00:00Z".parse::<DateTime<Utc>>().unwrap();
349        Tick::new(ts + TimeDelta::seconds(second), price, 1.0, None)
350    }
351
352    enum Step {
353        /// Trades pushed between two drains.
354        Live(Vec<Tick>),
355        Page(Vec<Tick>),
356    }
357    use Step::{Live, Page};
358
359    /// Group only explicit aggressor ids, across pages and drains, without moving price volume.
360    #[test]
361    fn combines_only_fills_of_the_same_aggressor() {
362        let fill = |second, price, size, order| {
363            let mut t = trade(second, price);
364            t.size = size;
365            t.aggressor_order_id = order;
366            t
367        };
368        #[rustfmt::skip]
369        let cases = [
370            ("empty input", 10, vec![], vec![]),
371            ("zero total size keeps the first price", 10,
372                vec![Live(vec![fill(1, 100.0, 0.0, Some(7)), fill(2, 101.0, 0.0, Some(7))])],
373                vec![(0.0, 100.0, 101.0, 100.0, 2)]),
374            ("invalid fills do not combine", 10,
375                vec![Live(vec![fill(1, f64::NAN, 1.0, Some(7)), fill(2, 100.0, f64::NAN, Some(7)),
376                    fill(3, 100.0, -1.0, Some(7)), fill(4, 100.0, 1.0, Some(7))])],
377                vec![(1.0, 100.0, 100.0, 100.0, 1)]),
378            ("three prices", 10, vec![Live(vec![fill(1, 100.0, 2.0, Some(7)),
379                fill(2, 101.0, 3.0, Some(7)), fill(3, 102.0, 5.0, Some(7))])],
380                vec![(10.0, 100.0, 102.0, 101.3, 3)]),
381            ("interleaved ids", 10, vec![Live(vec![fill(1, 100.0, 2.0, Some(7)),
382                fill(2, 90.0, 1.0, Some(8)), fill(3, 102.0, 2.0, Some(7))])],
383                vec![(4.0, 100.0, 102.0, 101.0, 2), (1.0, 90.0, 90.0, 90.0, 1)]),
384            ("unknown ids stay separate", 10, vec![Live(vec![fill(1, 100.0, 2.0, None),
385                fill(2, 100.0, 3.0, None)])],
386                vec![(2.0, 100.0, 100.0, 100.0, 1), (3.0, 100.0, 100.0, 100.0, 1)]),
387            ("history and live fills", 10, vec![Page(vec![fill(1, 100.0, 2.0, Some(7))]),
388                Live(vec![fill(2, 102.0, 2.0, Some(7))])],
389                vec![(4.0, 100.0, 102.0, 101.0, 2)]),
390            ("separate drains", 10, vec![Live(vec![fill(1, 100.0, 2.0, Some(7))]),
391                Live(vec![fill(2, 102.0, 2.0, Some(7))])],
392                vec![(4.0, 100.0, 102.0, 101.0, 2)]),
393            ("a page completes order ids on retained fills", 10,
394                vec![Live(vec![fill(1, 100.0, 2.0, None).with_id(1), fill(2, 102.0, 2.0, None).with_id(2)]),
395                    Page(vec![fill(1, 100.0, 2.0, Some(7)).with_id(1), fill(2, 102.0, 2.0, Some(7)).with_id(2)])],
396                vec![(4.0, 100.0, 102.0, 101.0, 2)]),
397            ("only retained fills count", 2, vec![Live(vec![fill(1, 100.0, 2.0, Some(7)),
398                fill(2, 101.0, 3.0, Some(7))]), Live(vec![fill(3, 102.0, 1.0, Some(7))])],
399                vec![(4.0, 101.0, 102.0, 101.25, 2)]),
400        ];
401        for (name, capacity, steps, expected) in cases {
402            let mut store = Trades::new(capacity);
403            for step in steps {
404                match step {
405                    Live(trades) => {
406                        let mut pending = store.empty();
407                        trades.into_iter().for_each(|t| pending.push(t));
408                        store.append(pending, &mut |_, _| ());
409                    }
410                    Page(page) => {
411                        store.prepend(page, &mut |_, _| ());
412                    }
413                }
414            }
415            let combined = combine_trades(store.iter());
416            let held: Vec<_> = combined
417                .iter()
418                .map(|t| (t.size, t.first_price, t.last_price, t.price, t.fills))
419                .collect();
420            assert_eq!(held.len(), expected.len(), "{name}");
421            for (actual, expected) in held.iter().zip(expected) {
422                assert_eq!(
423                    (actual.0, actual.1, actual.2, actual.4),
424                    (expected.0, expected.1, expected.2, expected.4),
425                    "{name}"
426                );
427                assert!((actual.3 - expected.3).abs() < 1e-10, "{name}: VWAP");
428            }
429        }
430        // All fills at one price have exactly that VWAP, including a bar's body edge.
431        let fills = [
432            fill(1, 99.99, 1.0, Some(7)),
433            fill(2, 99.99, 2.0, Some(7)),
434            fill(3, 99.99, 3.0, Some(7)),
435        ];
436        assert_eq!(combine_trades(&fills)[0].price, 99.99);
437    }
438
439    /// The breakdown lists the resting orders the retained fills consumed and adds up to the
440    /// combined size, also after eviction; it is complete only when every fill names its
441    /// resting order.
442    #[test]
443    fn the_breakdown_sums_to_the_retained_fills_and_reports_missing_evidence() {
444        let fill = |second, price, size, resting| {
445            let mut t = trade(second, price).with_aggressor_order_id(7);
446            t.size = size;
447            t.resting_order_id = resting;
448            t
449        };
450        let fills = || {
451            vec![
452                fill(1, 100.0, 2.0, Some(1)),
453                fill(2, 100.0, 1.0, Some(1)),
454                fill(3, 101.0, 4.0, Some(2)),
455                fill(4, 101.0, 3.0, None),
456            ]
457        };
458        for (capacity, want, complete) in [
459            (
460                10,
461                vec![
462                    (Some(1), 100.0, 3.0),
463                    (Some(2), 101.0, 4.0),
464                    (None, 101.0, 3.0),
465                ],
466                false,
467            ),
468            // The oldest two fills are evicted.
469            (2, vec![(Some(2), 101.0, 4.0), (None, 101.0, 3.0)], false),
470        ] {
471            let mut store = Trades::new(capacity);
472            fills().into_iter().for_each(|t| store.push(t));
473            let combined = combine_trades(store.iter());
474            let trade = &combined[0];
475            let got: Vec<_> = trade
476                .consumed
477                .iter()
478                .map(|c| (c.resting_order_id, c.price, c.size))
479                .collect();
480            assert_eq!(
481                (got, trade.complete),
482                (want, complete),
483                "capacity {capacity}"
484            );
485            assert_eq!(
486                trade.consumed.iter().map(|c| c.size).sum::<f64>(),
487                trade.size
488            );
489        }
490        let named: Vec<_> = fills().into_iter().take(3).collect();
491        assert!(combine_trades(&named)[0].complete);
492    }
493
494    /// Guards the memory bound and the join of history and live trades: a store that never evicts
495    /// grows with the trade rate, a page that evicts newer trades erases the live tape, a trade
496    /// counted twice or lost at the join skews every footprint and cumulative delta after a
497    /// reload, sorting a page must keep trades sharing a time in their order, and an invalid page
498    /// trade poisons every sum.
499    #[test]
500    fn the_store_is_bounded_and_joins_pages_without_repeats() {
501        let id = |trade: Tick, id| trade.with_id(id);
502        let later = |mut trade: Tick, micros| {
503            trade.ts += TimeDelta::microseconds(micros);
504            trade
505        };
506        #[rustfmt::skip]
507        let cases = [
508            ("zero capacity holds no ids", 0,
509                vec![Live(vec![id(trade(1, 1.0), 1)]), Page(vec![id(trade(0, 0.0), 0)]),
510                    Live(vec![id(trade(2, 2.0), 1)])], vec![], 0, vec![]),
511            ("the oldest trades make room", 3,
512                vec![Live(vec![id(trade(10, 10.0), 10), trade(11, 11.0), trade(12, 12.0)]),
513                    Live(vec![trade(13, 13.0)])],
514                vec![11.0, 12.0, 13.0], 0, vec![11.0, 12.0, 13.0]),
515            ("a page fills free room in front, without invalid or repeated trades", 4,
516                vec![Live(vec![trade(10, 10.0), trade(11, 11.0), trade(12, 12.0)]),
517                    Page(vec![trade(9, 9.0), trade(8, f64::NAN), trade(11, 99.0), trade(7, 7.0)])],
518                vec![9.0, 10.0, 11.0, 12.0], 1, vec![10.0, 11.0, 12.0]),
519            ("a full store takes nothing from a page", 2,
520                vec![Live(vec![trade(10, 10.0), trade(11, 11.0)]), Page(vec![trade(1, 1.0)])],
521                vec![10.0, 11.0], 0, vec![10.0, 11.0]),
522            ("with ids, a page keeps a trade at the instant of the oldest live trade", 10,
523                vec![Live(vec![id(trade(5, 3.0), 3), id(trade(5, 4.0), 4)]),
524                    Page(vec![id(trade(4, 1.0), 1), id(trade(5, 2.0), 2), id(trade(5, 3.0), 3),
525                        id(trade(5, 4.0), 4)])],
526                vec![1.0, 2.0, 3.0, 4.0], 0, vec![3.0, 4.0]),
527            ("without ids, a page drops trades at the instant of the oldest live trade", 10,
528                vec![Live(vec![trade(5, 3.0), trade(5, 4.0)]),
529                    Page(vec![trade(4, 1.0), trade(5, 2.0), trade(5, 3.0), trade(5, 4.0)])],
530                vec![1.0, 3.0, 4.0], 0, vec![3.0, 4.0]),
531            ("with ids, live trades a page already added are dropped", 10,
532                vec![Page(vec![id(trade(1, 1.0), 1), id(trade(2, 2.0), 2)]),
533                    Live(vec![id(trade(2, 2.0), 2), id(trade(2, 2.5), 5), id(trade(3, 3.0), 3)])],
534                vec![1.0, 2.0, 2.5, 3.0], 0, vec![2.5, 3.0]),
535            ("without ids, live trades at or before the newest page trade are dropped", 10,
536                vec![Page(vec![trade(1, 1.0), trade(2, 2.0)]),
537                    Live(vec![trade(2, 2.0), trade(3, 3.0)])],
538                vec![1.0, 2.0, 3.0], 0, vec![3.0]),
539            ("with ids, page trades after the oldest live trade are left to live", 10,
540                vec![Live(vec![id(trade(5, 3.0), 3)]),
541                    Page(vec![id(trade(4, 1.0), 1), id(trade(5, 3.0), 3), id(trade(6, 4.0), 4),
542                        id(trade(7, 5.0), 5)]),
543                    Live(vec![id(trade(6, 4.0), 4), id(trade(7, 5.0), 5)])],
544                vec![1.0, 3.0, 4.0, 5.0], 0, vec![3.0, 4.0, 5.0]),
545            ("with ids, a replayed live trade is dropped wherever the held one sits", 10,
546                vec![Page(vec![id(trade(8, 8.0), 8), id(trade(9, 9.0), 9)]),
547                    Live(vec![id(trade(7, 7.0), 7), id(trade(8, 8.0), 8), id(trade(9, 9.0), 9)])],
548                vec![8.0, 9.0, 7.0], 0, vec![7.0]),
549            ("with ids, a live trade stamped later than the page's copy is dropped", 10,
550                vec![Page(vec![id(trade(5, 5.0), 5)]),
551                    Live(vec![id(later(trade(5, 5.0), 400), 5)])],
552                vec![5.0], 0, vec![]),
553            ("with ids, a page trade stamped earlier than the live copy is dropped", 10,
554                vec![Live(vec![id(later(trade(5, 5.0), 400), 5)]),
555                    Page(vec![id(trade(4, 4.0), 4), id(trade(5, 5.0), 5)])],
556                vec![4.0, 5.0], 0, vec![5.0]),
557            ("a page without ids drops live trades with ids at or before its newest trade", 10,
558                vec![Page(vec![trade(1, 1.0), trade(2, 2.0)]),
559                    Live(vec![id(trade(2, 2.0), 2), id(trade(3, 3.0), 3)])],
560                vec![1.0, 2.0, 3.0], 0, vec![3.0]),
561            ("a page with ids drops trades at the instant of a held trade without one", 10,
562                vec![Live(vec![trade(5, 5.0)]),
563                    Page(vec![id(trade(4, 4.0), 4), id(trade(5, 5.0), 5)])],
564                vec![4.0, 5.0], 0, vec![5.0]),
565            ("a page is sorted by time, keeping the order of trades sharing a time", 10,
566                vec![Page(vec![trade(5, 4.0), trade(5, 3.0), trade(4, 2.0)])],
567                vec![2.0, 4.0, 3.0], 0, vec![]),
568            ("eviction removes history before the live suffix", 4,
569                vec![Live(vec![trade(10, 10.0), trade(11, 11.0)]),
570                    Page(vec![trade(8, 8.0), trade(9, 9.0)]), Live(vec![trade(12, 12.0)])],
571                vec![9.0, 10.0, 11.0, 12.0], 0, vec![10.0, 11.0, 12.0]),
572        ];
573        for (name, capacity, steps, prices, rejected, live_prices) in cases {
574            let mut store = Trades::new(capacity);
575            let mut dropped = 0;
576            for step in steps {
577                match step {
578                    Live(trades) => {
579                        let mut pending = store.empty();
580                        trades.into_iter().for_each(|trade| pending.push(trade));
581                        store.append(pending, &mut |_, _| ());
582                    }
583                    Page(page) => dropped += store.prepend(page, &mut |_, _| ()),
584                }
585            }
586            let held = Vec::from_iter(store.iter().map(|trade| trade.price));
587            let live = Vec::from_iter(store.live().map(|trade| trade.price));
588            assert_eq!(
589                (held, dropped, live),
590                (prices, rejected, live_prices),
591                "{name}"
592            );
593            let ids = store.iter().filter(|trade| trade.id.is_some()).count();
594            assert_eq!(store.ids.len(), ids, "{name}: held ids");
595            let repeats: Vec<_> = store.iter().filter(|t| t.id.is_some()).cloned().collect();
596            for trade in repeats {
597                let id = trade.id.unwrap();
598                store.push(trade.with_aggressor_order_id(id).with_resting_order_id(id));
599            }
600            for trade in store.iter().filter(|t| t.id.is_some()) {
601                assert_eq!(trade.aggressor_order_id, trade.id, "{name}");
602                assert_eq!(trade.resting_order_id, trade.id, "{name}");
603            }
604        }
605    }
606
607    /// Reconnects and overlapping pages must annotate every retained fill without counting it twice.
608    #[test]
609    fn replay_with_order_ids_annotates_retained_fills() {
610        let count = 100_000;
611        let fill = |id: usize| trade(id as i64, 100.0).with_id(id as u64);
612        for path in ["push", "append", "prepend"] {
613            let mut store = Trades::new(count);
614            for id in 0..count {
615                store.push(fill(id));
616            }
617            let page: Vec<_> = (0..count)
618                .map(|id| fill(id).with_aggressor_order_id(id as u64 + 1))
619                .collect();
620            match path {
621                "push" => page.into_iter().for_each(|t| store.push(t)),
622                "append" => {
623                    let mut pending = store.empty();
624                    page.into_iter().for_each(|t| pending.push(t));
625                    store.append(pending, &mut |_, _| panic!("repeated fill"));
626                }
627                _ => {
628                    store.prepend(page, &mut |_, _| panic!("repeated fill"));
629                }
630            }
631            assert_eq!(store.iter().count(), count, "{path}");
632            for (id, held) in store.iter().enumerate() {
633                assert_eq!(held.aggressor_order_id, Some(id as u64 + 1), "{path}");
634            }
635        }
636    }
637}