Skip to main content

egui_charts/data/
depth_history.rs

1//! The order book over time, for liquidity heatmaps: columns of resting size per price row on a
2//! fixed time grid. The newest columns keep the grid's cadence; older ones merge in pairs, so the
3//! history reaches far back in bounded memory.
4
5use super::FeedError;
6use super::depth::Book;
7use chrono::{DateTime, TimeDelta, Utc};
8use std::collections::VecDeque;
9use std::sync::Arc;
10
11/// Resolutions a history keeps: the newest columns, then columns merged from two of them, from
12/// four, and so on up to 128.
13const TIERS: usize = 8;
14
15/// How a [`DepthHistory`] samples the order book.
16///
17/// A column holds at most `rows` rows per side of 16 bytes each, 24 when it samples individual
18/// orders, and the history at most `columns * 8` columns: about 15 MB with the defaults, or
19/// 23 MB sampling individual orders, which span at least 42 hours.
20#[derive(Clone, Debug, PartialEq)]
21pub struct DepthHistoryConfig {
22    /// Length of a column. The book is sampled at the end of every period of this length counted
23    /// from the Unix epoch, and the sample stands for the whole period. At least a millisecond;
24    /// one second by default.
25    pub cadence: TimeDelta,
26    /// Price rows kept per side of each column, those nearest the best bid and ask. 100 by
27    /// default.
28    pub rows: usize,
29    /// Ticks of the instrument's tick size grouped into one price row, their sizes summed. 1 by
30    /// default.
31    pub ticks_per_row: u32,
32    /// Columns kept at each of 8 resolutions. The newest `columns` columns are samples; when there
33    /// are more, the two oldest merge into one, and the merged columns overflow into the next
34    /// resolution in the same way. At least 2; 600 by default.
35    pub columns: usize,
36    /// Sample the individual resting orders this filter accepts, summed into rows, instead of
37    /// the aggregate prices the feed sends, and record the largest of them in each row (see
38    /// [`DepthColumn::largest_bid_orders`]); `Some(OrderFilter::default())` samples every order.
39    /// Needs a feed that sends order events, see
40    /// [`Sink::push_order_event`](crate::data::Sink::push_order_event): with aggregate depth only
41    /// no column is sampled, and a period in which the filter cannot be applied (see
42    /// [`OrderFilterUnavailable`](crate::data::OrderFilterUnavailable)) has no column. `None`
43    /// samples the aggregate book.
44    pub orders: Option<crate::data::OrderFilter>,
45}
46
47impl Default for DepthHistoryConfig {
48    fn default() -> Self {
49        Self {
50            cadence: TimeDelta::seconds(1),
51            rows: 100,
52            ticks_per_row: 1,
53            columns: 600,
54            orders: None,
55        }
56    }
57}
58
59/// The resting size at each price row of the book over `[time, time + span)`, and the largest
60/// individual order in each row when the history samples individual orders.
61#[derive(Clone, Debug, Default, PartialEq)]
62pub struct DepthColumn {
63    /// Start of the column, on the cadence grid.
64    pub time: DateTime<Utc>,
65    /// Length of the column: a whole number of cadences. A sample spans more than one cadence
66    /// when the book did not change for longer, and a merged column spans the two it merged.
67    pub span: TimeDelta,
68    /// Bid rows, best first, as `(row, size)`: the row is the price in integer ticks of the
69    /// instrument's tick size divided by `ticks_per_row`, rounded down, and the size is the
70    /// resting size of the row's ticks, averaged over the span. A merged column keeps the rows of
71    /// both columns, so its first row is the best bid of either.
72    pub bids: Vec<(i64, f64)>,
73    /// Ask rows, best first, as in [`bids`](Self::bids).
74    pub asks: Vec<(i64, f64)>,
75    /// For a column sampled from individual orders (see [`DepthHistoryConfig::orders`]), the size
76    /// of the largest accepted order in each row of [`bids`](Self::bids), in the same order, or 0
77    /// for a row in which no order is known. A merged column keeps the larger of the two at each
78    /// row, so the value can exceed the row's size averaged over the span. Empty for a column of
79    /// aggregate depth, whose orders are not known.
80    pub largest_bid_orders: Vec<f64>,
81    /// The largest accepted order in each row of [`asks`](Self::asks), as in
82    /// [`largest_bid_orders`](Self::largest_bid_orders).
83    pub largest_ask_orders: Vec<f64>,
84}
85
86/// Columns of the order book of one chart, oldest first. Turn it on with
87/// `Chart::set_depth_history` and read it with `Chart::depth_history`.
88///
89/// ```
90/// use egui_charts::data::{DepthHistory, DepthHistoryConfig};
91///
92/// let config = DepthHistoryConfig { ticks_per_row: 4, ..Default::default() };
93/// let history = DepthHistory::new(config).unwrap();
94/// assert_eq!(history.columns().count(), 0);
95/// ```
96#[derive(Clone, Debug)]
97pub struct DepthHistory {
98    config: DepthHistoryConfig,
99    /// Column `k` holds columns merged from up to `2^k` samples, oldest first. Every column of a
100    /// tier is older than those of the tiers before it.
101    /// Shared, so that a heatmap tells a column it drew from a new one by identity instead of
102    /// keeping a copy of every column to compare with.
103    tiers: Vec<VecDeque<Arc<DepthColumn>>>,
104}
105
106impl DepthHistory {
107    /// An empty history, or an error if `config` breaks one of its limits.
108    pub fn new(config: DepthHistoryConfig) -> Result<Self, FeedError> {
109        if config.cadence < TimeDelta::milliseconds(1) {
110            return Err(FeedError::InvalidData(format!(
111                "depth history cadence must be at least one millisecond, got {}",
112                config.cadence
113            )));
114        }
115        if config.rows == 0 {
116            return Err(FeedError::InvalidData(
117                "depth history rows must be at least 1, got 0".into(),
118            ));
119        }
120        if config.ticks_per_row == 0 {
121            return Err(FeedError::InvalidData(
122                "depth history ticks_per_row must be at least 1, got 0".into(),
123            ));
124        }
125        if config.columns < 2 {
126            return Err(FeedError::InvalidData(format!(
127                "depth history columns must be at least 2, got {}",
128                config.columns
129            )));
130        }
131        Ok(Self {
132            config,
133            tiers: vec![VecDeque::new(); TIERS],
134        })
135    }
136
137    /// The configuration the history samples with.
138    pub fn config(&self) -> &DepthHistoryConfig {
139        &self.config
140    }
141
142    /// Every column, oldest first. Consecutive columns are adjacent unless the chart lost columns.
143    pub fn columns(&self) -> impl DoubleEndedIterator<Item = &DepthColumn> {
144        self.shared().map(Arc::as_ref)
145    }
146
147    /// The columns of [`columns`](Self::columns), shared. A column is replaced, never changed, so
148    /// a clone of the same `Arc` is the same column.
149    pub(crate) fn shared(&self) -> impl DoubleEndedIterator<Item = &Arc<DepthColumn>> {
150        self.tiers.iter().rev().flatten()
151    }
152
153    /// Merges a historical page, preserving every held column on overlap and keeping time gaps.
154    /// Older columns merge to the oldest tier's resolution, and columns that fill a gap to the
155    /// resolution of the tier they join; either completes a short held column right after it.
156    /// Full columns merge only when their tier overflows.
157    /// Columns must use this history's grid and sorted, unique rows, with finite non-negative
158    /// sizes and at most `config.rows` per side. Invalid pages leave the history unchanged.
159    /// A side's largest orders, if it has any, are one per row, finite and not negative.
160    /// Memory remains bounded by the configured tiers. Returns the number of input columns
161    /// retained, including those merged to a coarser resolution. Columns beyond the oldest
162    /// tier's capacity are dropped and do not count.
163    pub fn merge_page(&mut self, mut page: Vec<DepthColumn>) -> Result<usize, FeedError> {
164        let cadence = self.config.cadence;
165        let valid_side = |side: &[(i64, f64)], largest: &[f64], bid: bool| {
166            side.len() <= self.config.rows
167                && (largest.is_empty() || largest.len() == side.len())
168                && side
169                    .iter()
170                    .map(|(_, size)| size)
171                    .chain(largest)
172                    .all(|size| size.is_finite() && *size >= 0.0)
173                && side.windows(2).all(|w| {
174                    if bid {
175                        w[0].0 > w[1].0
176                    } else {
177                        w[0].0 < w[1].0
178                    }
179                })
180        };
181        if page.iter().any(|c| {
182            start(c.time, cadence) != Some(c.time)
183                || c.span <= TimeDelta::zero()
184                || c.span
185                    .num_nanoseconds()
186                    .zip(cadence.num_nanoseconds())
187                    .is_none_or(|(span, cadence)| span % cadence != 0)
188                || c.time.checked_add_signed(c.span).is_none()
189                || !valid_side(&c.bids, &c.largest_bid_orders, true)
190                || !valid_side(&c.asks, &c.largest_ask_orders, false)
191        }) {
192            return Err(FeedError::InvalidData(
193                "invalid historical depth columns".into(),
194            ));
195        }
196        page.sort_by_key(|c| c.time);
197        let mut held: Vec<_> = self.shared().cloned().collect();
198        let mut accepted = Vec::new();
199        for column in page {
200            let at = held.partition_point(|c| c.time < column.time);
201            let overlaps = (at > 0 && held[at - 1].time + held[at - 1].span > column.time)
202                || held
203                    .get(at)
204                    .is_some_and(|c| c.time < column.time + column.span);
205            if !overlaps {
206                let column = Arc::new(column);
207                held.insert(at, Arc::clone(&column));
208                accepted.push(column);
209            }
210        }
211        if accepted.is_empty() {
212            return Ok(0);
213        }
214        let mut older = 0;
215        if let Some(k) = self.tiers.iter().rposition(|tier| !tier.is_empty()) {
216            older = accepted.partition_point(|c| c.time < self.tiers[k][0].time);
217            let held = std::mem::take(&mut self.tiers[k]);
218            self.tiers[k] = self.merge_run(k, accepted[..older].iter().cloned().collect(), held);
219        }
220        // Columns that fill a gap are placed by span and age, as live corrections are; runs of
221        // them finer than their tier then merge to its resolution, as older columns do above.
222        let placed: Vec<_> = accepted[older..]
223            .iter()
224            .map(|column| (self.insert_column(Arc::clone(column)), column))
225            .collect();
226        let cadence = self.config.cadence;
227        let finer =
228            |(tier, column): &(usize, &Arc<DepthColumn>)| resolution(cadence, column.span) < *tier;
229        for run in placed
230            .chunk_by(|a, b| a.0 == b.0 && finer(a) == finer(b) && a.1.time + a.1.span == b.1.time)
231        {
232            let (tier, first) = run[0];
233            if finer(&run[0]) {
234                let at = self.tiers[tier].partition_point(|c| c.time < first.time);
235                let mut after = self.tiers[tier].split_off(at);
236                let columns = after.drain(..run.len()).collect();
237                let mut merged = self.merge_run(tier, columns, after);
238                self.tiers[tier].append(&mut merged);
239            }
240        }
241        self.compact();
242        let oldest = self.columns().next().expect("accepted columns").time;
243        Ok(accepted.len() - accepted.partition_point(|c| c.time < oldest))
244    }
245
246    #[cfg(feature = "widget")]
247    pub(crate) fn clear(&mut self) {
248        self.tiers.iter_mut().for_each(VecDeque::clear);
249    }
250
251    /// Live columns supersede overlapping history, including a page answered in the same drain.
252    pub(crate) fn push(&mut self, column: impl Into<Arc<DepthColumn>>) {
253        let column = column.into();
254        if self
255            .columns()
256            .next_back()
257            .is_some_and(|last| last.time + last.span > column.time)
258        {
259            let end = column.time + column.span;
260            for tier in &mut self.tiers {
261                tier.retain(|c| c.time >= end || c.time + c.span <= column.time);
262            }
263            self.insert_column(column);
264        } else {
265            self.tiers[0].push_back(column);
266        }
267        self.compact();
268    }
269
270    /// Places a column by its span, constrained by the ages of the held tiers, and returns its
271    /// tier. Older live corrections join the oldest occupied tier, leaving the newer resolutions
272    /// intact.
273    fn insert_column(&mut self, column: Arc<DepthColumn>) -> usize {
274        let tier = if let Some(k) = self.tiers.iter().rposition(|tier| !tier.is_empty())
275            && column.time < self.tiers[k][0].time
276        {
277            k
278        } else {
279            let lower = self
280                .tiers
281                .iter()
282                .rposition(|tier| tier.back().is_some_and(|c| c.time > column.time))
283                .unwrap_or(0);
284            let upper = self
285                .tiers
286                .iter()
287                .position(|tier| tier.front().is_some_and(|c| c.time < column.time))
288                .unwrap_or(TIERS - 1);
289            resolution(self.config.cadence, column.span).clamp(lower, upper)
290        };
291        let at = self.tiers[tier].partition_point(|c| c.time < column.time);
292        self.tiers[tier].insert(at, column);
293        tier
294    }
295
296    /// `columns`, new and oldest first, merged to the resolution of `tier` and followed by
297    /// `after`, the columns of that tier after them: the newest first complete a short column at
298    /// the front of `after`, then the rest merge in pairs `tier` times, working back from `after`
299    /// and leaving a short column at the oldest end.
300    fn merge_run(
301        &self,
302        tier: usize,
303        mut columns: VecDeque<Arc<DepthColumn>>,
304        mut after: VecDeque<Arc<DepthColumn>>,
305    ) -> VecDeque<Arc<DepthColumn>> {
306        // Complete a short boundary before it consumes a whole slot of the tier.
307        if let Some(span) = self.config.cadence.checked_mul(1 << tier) {
308            while columns
309                .back()
310                .zip(after.front())
311                .is_some_and(|(older, newer)| {
312                    older.time + older.span == newer.time && older.span + newer.span <= span
313                })
314            {
315                let older = columns.pop_back().expect("an older column");
316                let newer = after.pop_front().expect("a newer column");
317                after.push_front(Arc::new(merge(&older, &newer, self.config.rows)));
318            }
319        }
320        // Work back from the boundary, leaving a short column at the oldest end.
321        for _ in 0..tier {
322            let mut merged = VecDeque::new();
323            while let Some(newer) = columns.pop_back() {
324                let column = match columns.back() {
325                    Some(older) if older.time + older.span == newer.time => {
326                        let older = columns.pop_back().expect("an older column");
327                        Arc::new(merge(&older, &newer, self.config.rows))
328                    }
329                    _ => newer,
330                };
331                merged.push_front(column);
332            }
333            columns = merged;
334        }
335        columns.append(&mut after);
336        columns
337    }
338
339    /// Promotes only overflow, keeping columns in all other tiers at their held resolution.
340    fn compact(&mut self) {
341        for tier in 0..TIERS {
342            while self.tiers[tier].len() > self.config.columns {
343                let older = self.tiers[tier].pop_front().expect("a full tier");
344                if tier == TIERS - 1 {
345                    continue;
346                }
347                // A column lost between the two would be averaged over the time it does not cover.
348                let merged = match self.tiers[tier].front() {
349                    Some(newer) if older.time + older.span == newer.time => {
350                        let newer = self.tiers[tier].pop_front().expect("a newer column");
351                        Arc::new(merge(&older, &newer, self.config.rows))
352                    }
353                    _ => older,
354                };
355                self.tiers[tier + 1].push_back(merged);
356            }
357        }
358    }
359}
360
361/// Samples a book into columns on the grid of a [`DepthHistoryConfig`].
362pub(crate) struct Sampler {
363    pub(crate) config: DepthHistoryConfig,
364    /// Start of the period of the last update, whose sample is not taken yet.
365    open: Option<DateTime<Utc>>,
366}
367
368impl Sampler {
369    pub(crate) fn new(config: DepthHistoryConfig) -> Self {
370        Self { config, open: None }
371    }
372
373    /// Whether an update stamped `ts` opens a period after the open one, so that `sample` reads
374    /// the book.
375    pub(crate) fn due(&self, ts: DateTime<Utc>) -> bool {
376        start(ts, self.config.cadence)
377            .zip(self.open)
378            .is_some_and(|(period, open)| period > open)
379    }
380
381    /// The book cannot be sampled: the period that was open has no column, nor does the time until
382    /// the next sample.
383    pub(crate) fn lose(&mut self) {
384        self.open = None;
385    }
386
387    /// Called before `book` applies an update stamped `ts`. When the update opens a later period,
388    /// returns the book as it stood since the end of the open period's last update: one column
389    /// from the open period to the update's. An update stamped before the open period's start
390    /// samples nothing. `None` also for a book without a tick size. `largest` holds the largest
391    /// order at each price of the orders `book` sums and gives the column's largest orders; `None`
392    /// for aggregate depth, whose columns record none.
393    pub(crate) fn sample(
394        &mut self,
395        ts: DateTime<Utc>,
396        book: &Book,
397        largest: Option<&Book>,
398    ) -> Option<DepthColumn> {
399        let period = start(ts, self.config.cadence)?;
400        let open = self.open.replace(period.max(self.open.unwrap_or(period)))?;
401        if period <= open {
402            return None;
403        }
404        let (ticks, rows) = (self.config.ticks_per_row, self.config.rows);
405        let [bids, asks] = book.rows(ticks, rows, |a, b| (a + b).min(f64::MAX))?;
406        let [largest_bid_orders, largest_ask_orders]: [Vec<f64>; 2] = largest
407            .and_then(|book| book.rows(ticks, rows, f64::max))
408            .map(|sides| sides.map(|side| side.into_iter().map(|(_, size)| size).collect()))
409            .unwrap_or_default();
410        Some(DepthColumn {
411            time: open,
412            span: period - open,
413            bids,
414            asks,
415            largest_bid_orders,
416            largest_ask_orders,
417        })
418    }
419}
420
421/// The finest tier whose resolution, `cadence` doubled once per tier, is at least `span`.
422fn resolution(cadence: TimeDelta, span: TimeDelta) -> usize {
423    (0..TIERS - 1)
424        .find(|&k| {
425            cadence
426                .checked_mul(1 << k)
427                .is_none_or(|limit| span <= limit)
428        })
429        .unwrap_or(TIERS - 1)
430}
431
432/// Start of the period of `cadence` that holds `ts`, counted from the Unix epoch.
433fn start(ts: DateTime<Utc>, cadence: TimeDelta) -> Option<DateTime<Utc>> {
434    let (ts, cadence) = (ts.timestamp_nanos_opt()?, cadence.num_nanoseconds()?);
435    Some(DateTime::from_timestamp_nanos(
436        ts.checked_sub(ts.rem_euclid(cadence))?,
437    ))
438}
439
440/// One column spanning `older` and `newer`, adjacent, each row's size averaged over their spans
441/// and its largest order the larger of the two.
442fn merge(older: &DepthColumn, newer: &DepthColumn, rows: usize) -> DepthColumn {
443    let span = older.span + newer.span;
444    let weight = |part: TimeDelta| part.as_seconds_f64() / span.as_seconds_f64();
445    let (a, b) = (weight(older.span), weight(newer.span));
446    let (bids, largest_bid_orders) = merge_side(
447        (&older.bids, &older.largest_bid_orders),
448        (&newer.bids, &newer.largest_bid_orders),
449        (a, b),
450        rows,
451        |row| -row,
452    );
453    let (asks, largest_ask_orders) = merge_side(
454        (&older.asks, &older.largest_ask_orders),
455        (&newer.asks, &newer.largest_ask_orders),
456        (a, b),
457        rows,
458        |row| row,
459    );
460    DepthColumn {
461        time: older.time,
462        span,
463        bids,
464        asks,
465        largest_bid_orders,
466        largest_ask_orders,
467    }
468}
469
470/// Merges two sides, best first, into their best `rows` rows, and their largest orders when
471/// either side has them: the larger of the two at a shared row, 0 for a row of a side without
472/// them. `order` ranks a row: lower is better.
473fn merge_side(
474    (older, older_largest): (&[(i64, f64)], &[f64]),
475    (newer, newer_largest): (&[(i64, f64)], &[f64]),
476    (a, b): (f64, f64),
477    rows: usize,
478    order: fn(i128) -> i128,
479) -> (Vec<(i64, f64)>, Vec<f64>) {
480    let known = !older_largest.is_empty() || !newer_largest.is_empty();
481    let mut merged = Vec::with_capacity(rows.min(older.len() + newer.len()));
482    let mut largest = Vec::with_capacity(if known { merged.capacity() } else { 0 });
483    let (mut older, mut newer) = (
484        older.iter().enumerate().peekable(),
485        newer.iter().enumerate().peekable(),
486    );
487    while merged.len() < rows {
488        let rank =
489            |row: Option<&(usize, &(i64, f64))>| row.map(|(_, (row, _))| order(*row as i128));
490        let (row, size, at) = match (rank(older.peek()), rank(newer.peek())) {
491            (None, None) => break,
492            (Some(x), Some(y)) if x == y => {
493                let ((i, (row, x)), (j, (_, y))) = (older.next().unwrap(), newer.next().unwrap());
494                (*row, x * a + y * b, (Some(i), Some(j)))
495            }
496            (Some(x), y) if y.is_none_or(|y| x < y) => {
497                let (i, (row, x)) = older.next().unwrap();
498                (*row, x * a, (Some(i), None))
499            }
500            _ => {
501                let (j, (row, y)) = newer.next().unwrap();
502                (*row, y * b, (None, Some(j)))
503            }
504        };
505        merged.push((row, size));
506        if known {
507            let of = |side: &[f64], at: Option<usize>| {
508                at.and_then(|at| side.get(at)).copied().unwrap_or(0.0)
509            };
510            largest.push(of(older_largest, at.0).max(of(newer_largest, at.1)));
511        }
512    }
513    (merged, largest)
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519
520    fn at(second: i64) -> DateTime<Utc> {
521        DateTime::from_timestamp(second, 0).unwrap()
522    }
523
524    fn column(second: i64, span: i64, bid: f64) -> DepthColumn {
525        DepthColumn {
526            time: at(second),
527            span: TimeDelta::seconds(span),
528            bids: vec![(100, bid)],
529            ..Default::default()
530        }
531    }
532
533    /// Guards the decimation: a full tier must merge its two oldest columns into one spanning
534    /// both, with each row's size averaged over their spans (rows missing on one side count as
535    /// empty, the union stays best first and is cut to `rows`), and the history must stay bounded
536    /// with columns oldest first. Columns that are not adjacent must not be averaged together.
537    #[test]
538    fn full_tiers_merge_adjacent_columns_by_time_weight() {
539        // Spans of a cadence that is not a whole number of milliseconds.
540        let older = DepthColumn {
541            time: at(0),
542            span: TimeDelta::microseconds(500),
543            bids: vec![(101, 4.0), (99, 2.0)],
544            asks: vec![(102, 6.0)],
545            ..Default::default()
546        };
547        let newer = DepthColumn {
548            time: at(0) + TimeDelta::microseconds(500),
549            span: TimeDelta::microseconds(1_500),
550            bids: vec![(100, 8.0), (99, 6.0)],
551            asks: vec![(102, 2.0), (103, 4.0)],
552            ..Default::default()
553        };
554        // 101: 4 * 1/4; 100: 8 * 3/4; 99: (2 * 1 + 6 * 3) / 4; cut to two rows.
555        assert_eq!(
556            merge(&older, &newer, 2),
557            DepthColumn {
558                time: at(0),
559                span: TimeDelta::milliseconds(2),
560                bids: vec![(101, 1.0), (100, 6.0)],
561                asks: vec![(102, 3.0), (103, 3.0)],
562                ..Default::default()
563            }
564        );
565
566        // The largest order of a merged row is the larger of the two, 0 for a row of a column that
567        // knows no order, and none when neither column knows any (above).
568        let known = |column: &DepthColumn, bids: &[f64], asks: &[f64]| DepthColumn {
569            largest_bid_orders: bids.to_vec(),
570            largest_ask_orders: asks.to_vec(),
571            ..column.clone()
572        };
573        let (older_known, newer_known) = (
574            known(&older, &[3.0, 2.0], &[5.0]),
575            known(&newer, &[7.0, 4.0], &[1.0, 3.0]),
576        );
577        for (older, newer, bids, asks) in [
578            (&older_known, &newer, [3.0, 0.0], [5.0, 0.0]),
579            (&older, &newer_known, [0.0, 7.0], [1.0, 3.0]),
580            (&older_known, &newer_known, [3.0, 7.0], [5.0, 3.0]),
581        ] {
582            let merged = merge(older, newer, 2);
583            assert_eq!(
584                (merged.largest_bid_orders, merged.largest_ask_orders),
585                (bids.to_vec(), asks.to_vec())
586            );
587        }
588
589        let mut history = DepthHistory::new(config_columns(2)).unwrap();
590        for second in 0..1_000 {
591            history.push(column(second, 1, second as f64));
592        }
593        let spans = Vec::from_iter(history.columns().map(|c| c.span.num_seconds()));
594        assert!(history.columns().count() <= 2 * TIERS, "{spans:?}");
595        let times = Vec::from_iter(history.columns().map(|c| c.time));
596        assert!(times.is_sorted(), "{times:?}");
597        // The newest columns are samples and the oldest merged from 128 of them.
598        assert_eq!((spans.first(), spans.last()), (Some(&128), Some(&1)));
599        let first = history.columns().next().unwrap();
600        let mean = (first.time.timestamp()..first.time.timestamp() + 128).sum::<i64>() / 128;
601        assert_eq!(first.bids, [(100, mean as f64 + 0.5)]);
602
603        let mut history = DepthHistory::new(config_columns(2)).unwrap();
604        for (second, bid) in [(0, 1.0), (5, 3.0), (6, 5.0)] {
605            history.push(column(second, 1, bid));
606        }
607        let held = Vec::from_iter(history.columns().map(|c| (c.time, c.span, c.bids[0].1)));
608        let one = TimeDelta::seconds(1);
609        assert_eq!(
610            held,
611            [(at(0), one, 1.0), (at(5), one, 3.0), (at(6), one, 5.0)]
612        );
613    }
614
615    #[test]
616    fn historical_pages_preserve_live_columns_and_gaps() {
617        #[rustfmt::skip]
618        let cases = [
619            ("overlap", vec![column(1, 1, 1.0), column(2, 1, 99.0)],
620                vec![(1, 1.0), (2, 2.0), (3, 3.0)]),
621            ("gap", vec![column(-4, 1, 4.0)], vec![(-4, 4.0), (2, 2.0), (3, 3.0)]),
622            ("empty", vec![], vec![(2, 2.0), (3, 3.0)]),
623        ];
624        for (name, page, expected) in cases {
625            let mut history = DepthHistory::new(config_columns(10)).unwrap();
626            history.push(column(2, 1, 2.0));
627            history.push(column(3, 1, 3.0));
628            history.merge_page(page).unwrap();
629            let held: Vec<_> = history
630                .columns()
631                .map(|c| (c.time.timestamp(), c.bids[0].1))
632                .collect();
633            assert_eq!(held, expected, "{name}");
634            history.push(column(3, 1, 30.0));
635            assert_eq!(history.columns().filter(|c| c.time == at(3)).count(), 1);
636            assert_eq!(history.columns().next_back().unwrap().bids, [(100, 30.0)]);
637            let before: Vec<_> = history.columns().cloned().collect();
638            assert!(history.merge_page(vec![column(1, -1, 1.0)]).is_err());
639            let edge = DepthColumn {
640                time: DateTime::from_timestamp_nanos(i64::MIN),
641                ..column(1, 1, 1.0)
642            };
643            assert!(history.merge_page(vec![edge]).is_err());
644            // A side's largest orders are one per row, finite and not negative.
645            for largest in [vec![1.0, 1.0], vec![f64::NAN], vec![-1.0]] {
646                let invalid = DepthColumn {
647                    largest_bid_orders: largest,
648                    ..column(1, 1, 1.0)
649                };
650                assert!(history.merge_page(vec![invalid]).is_err());
651            }
652            assert_eq!(history.columns().cloned().collect::<Vec<_>>(), before);
653        }
654
655        // One streamed hour, then default-sized pages back through the promised 42 hours.
656        let mut history = DepthHistory::new(DepthHistoryConfig::default()).unwrap();
657        for second in 0..3_600 {
658            history.push(column(second, 1, second as f64));
659        }
660        for page in 1..=246 {
661            let before = history.columns().next().unwrap().time.timestamp();
662            let older = (before - 600..before)
663                .map(|s| column(s, 1, s.unsigned_abs() as f64))
664                .collect();
665            assert_eq!(history.merge_page(older).unwrap(), 600, "page {page}");
666            assert!(
667                history.columns().next().unwrap().time < at(before),
668                "page {page}"
669            );
670        }
671        assert_eq!(history.columns().next().unwrap().time, at(-41 * 3_600));
672        // At capacity, the accepted count must match the samples that actually survive.
673        let mut accepted = 600;
674        for _ in 0..16 {
675            let before = history.columns().next().unwrap().time.timestamp();
676            accepted = history
677                .merge_page((before - 600..before).map(|s| column(s, 1, 1.0)).collect())
678                .unwrap();
679            let oldest = history.columns().next().unwrap().time.timestamp();
680            assert_eq!(accepted, (before - oldest) as usize);
681            if accepted == 0 {
682                break;
683            }
684        }
685        assert_eq!(accepted, 0);
686    }
687
688    /// Paging and live corrections must leave unrelated, already merged columns at their
689    /// resolution, including when a page fills a gap inside the held history. A page that fills
690    /// a gap must merge to the resolution of the tier it joins, so that pages merged out of order
691    /// hold the same columns as in order.
692    #[test]
693    fn history_updates_preserve_held_resolution() {
694        let mut history = DepthHistory::new(config_columns(2)).unwrap();
695        for second in 0..40 {
696            history.push(column(second, 1, second as f64));
697        }
698        let before = history.tiers.clone();
699        for page in [vec![], vec![column(0, 40, 99.0)]] {
700            assert_eq!(history.merge_page(page).unwrap(), 0);
701            assert_eq!(history.tiers, before);
702        }
703        for page in 1..=5 {
704            let older = (-8 * page..-8 * (page - 1))
705                .map(|s| column(s, 1, s.unsigned_abs() as f64))
706                .collect();
707            assert_eq!(history.merge_page(older).unwrap(), 8);
708            for held in before.iter().flatten() {
709                assert!(history.shared().any(|c| Arc::ptr_eq(c, held)));
710            }
711        }
712        assert_eq!(history.columns().next().unwrap().time, at(-40));
713        history.push(column(39, 1, 99.0));
714        for held in before.iter().flatten().filter(|c| c.time < at(39)) {
715            assert!(history.shared().any(|c| Arc::ptr_eq(c, held)));
716        }
717        let paged = history;
718
719        // A coarse column followed by samples, with room for a page at each resolution.
720        let mut history = DepthHistory::new(config_columns(3)).unwrap();
721        history.merge_page(vec![column(0, 8, 4.0)]).unwrap();
722        history.push(column(20, 1, 20.0));
723        let held: Vec<_> = history.shared().cloned().collect();
724        assert_eq!(
725            history
726                .merge_page(vec![column(8, 4, 8.0), column(16, 1, 16.0)])
727                .unwrap(),
728            2
729        );
730        history.push(column(12, 4, 12.0));
731        for c in held {
732            assert!(history.shared().any(|h| Arc::ptr_eq(h, &c)));
733        }
734        assert_eq!(
735            history
736                .columns()
737                .map(|c| (c.time.timestamp(), c.span.num_seconds()))
738                .collect::<Vec<_>>(),
739            [(0, 8), (8, 4), (12, 4), (16, 1), (20, 1)]
740        );
741        for history in [&paged, &history] {
742            assert!(
743                history
744                    .tiers
745                    .iter()
746                    .all(|tier| tier.len() <= history.config.columns)
747            );
748            let columns: Vec<_> = history.columns().collect();
749            assert!(
750                columns
751                    .windows(2)
752                    .all(|w| w[0].time + w[0].span <= w[1].time)
753            );
754        }
755
756        // Pages of 24 samples behind a 16-second column, the third before the second: the second
757        // then fills a gap in the 16-second tier, completes the 8-second column the first left
758        // and merges the rest, as it does in order.
759        for order in [[1, 2, 3], [1, 3, 2]] {
760            let mut history = DepthHistory::new(DepthHistoryConfig::default()).unwrap();
761            history.merge_page(vec![column(0, 16, 1.0)]).unwrap();
762            for page in order {
763                let older = (-24 * page..-24 * (page - 1))
764                    .map(|s| column(s, 1, 1.0))
765                    .collect();
766                assert_eq!(history.merge_page(older).unwrap(), 24, "{order:?}");
767            }
768            assert_eq!(
769                history
770                    .columns()
771                    .map(|c| (c.time.timestamp(), c.span.num_seconds()))
772                    .collect::<Vec<_>>(),
773                [
774                    (-72, 8),
775                    (-64, 16),
776                    (-48, 16),
777                    (-32, 16),
778                    (-16, 16),
779                    (0, 16)
780                ],
781                "{order:?}"
782            );
783        }
784    }
785
786    fn config_columns(columns: usize) -> DepthHistoryConfig {
787        DepthHistoryConfig {
788            columns,
789            ..Default::default()
790        }
791    }
792}