Skip to main content

egui_charts/data/
order_flow.rs

1//! Stream inputs for studies that cannot be calculated from OHLC bars.
2
3use super::OrderFilter;
4use super::footprint::{Footprint, FootprintBar};
5use crate::model::{BarClock, Level1Quote, OrderBook, OrderBookLevel, OrderSide, Tick};
6use chrono::{DateTime, TimeDelta, Utc};
7use std::collections::{BTreeMap, HashMap, VecDeque};
8use std::sync::{Arc, Mutex, MutexGuard};
9
10/// Fixed stream aggregation performed before a feed coalesces its updates for painting.
11#[derive(Clone, Copy, Debug, PartialEq)]
12#[non_exhaustive]
13pub enum OrderFlowKind {
14    /// Last, arithmetic mean and maximum quoted spread, in instrument ticks, per bar.
15    Spread,
16    /// Last resting bid size, ask size and their difference. `distance == 0` selects the
17    /// nearest `levels` on each side; a positive distance selects prices within that many
18    /// price units of each side's inside market, inclusive. With an [`OrderFilter`] (see
19    /// [`OrderFlowInput::with_order_filter`]) the sizes are those of the individual orders it
20    /// accepts, and a bar whose feed sends aggregate depth only has no value.
21    BookSize {
22        /// Maximum levels when distance is zero.
23        levels: usize,
24        /// Price distance, zero for level count.
25        distance: f64,
26    },
27    /// Bar total, rolling rate per second and instant rate per second. Windows are
28    /// `(now - seconds, now]`; `volume == false` counts executions, including zero-size ones.
29    TradeRate {
30        /// Sum size rather than execution count.
31        volume: bool,
32        /// Rolling window in seconds.
33        seconds: usize,
34        /// Instant window in seconds.
35        instant: usize,
36    },
37    /// Distinct explicitly identified aggressor orders: the bar total, and the rolling and
38    /// instant rates per second over `(now - seconds, now]`. Fills sharing an aggressor order
39    /// id count once, however many fills or windows they span; fills without one are neither
40    /// merged nor counted.
41    AggressorRate {
42        /// Rolling window in seconds.
43        seconds: usize,
44        /// Instant window in seconds.
45        instant: usize,
46    },
47    /// Distinct prices that buy and sell executions printed at, per bar when `seconds == 0`,
48    /// otherwise over `(now - seconds, now]`, and their sum. Only executions count, so a
49    /// quote that moves because a resting order was cancelled adds nothing, unlike
50    /// [`LevelsTradedThrough`](Self::LevelsTradedThrough). Executions of unknown side count
51    /// for neither side.
52    LevelsExecuted {
53        /// Rolling seconds, or zero for per-bar totals.
54        seconds: usize,
55        /// Restart a side's count when its executions reverse: a buy below the previous buy,
56        /// or a sell above the previous sell.
57        reversal: bool,
58    },
59    /// Buy minus sell volume per bar, counting only executions whose size lies in each of three
60    /// inclusive `[min, max]` ranges, all from the same trades. A range with `min > max`
61    /// matches nothing; executions of unknown side count for no range.
62    SizeDelta {
63        /// Inclusive size ranges, one per output.
64        ranges: [[f64; 2]; 3],
65    },
66    /// Total volume and buy minus sell volume per bar, which a study sums over `bars` bars.
67    FlowSum {
68        /// Bars in the rolling sum.
69        bars: usize,
70    },
71    /// Buy minus sell volume, its highest and lowest running value including zero, per bar,
72    /// counting executions whose size lies in the inclusive range. A study accumulates the
73    /// delta into a cumulative volume delta. The sum starts at the oldest
74    /// bar with a retained value, so it starts again from a later bar after the input drops its
75    /// oldest bars (see [`OrderFlowInput`]).
76    FilteredCvd {
77        /// Smallest size counted.
78        min: f64,
79        /// Largest size counted.
80        max: f64,
81    },
82    /// Absolute bid and ask movement in ticks and their sum, per bar when `seconds == 0`,
83    /// otherwise over `(now - seconds, now]`. The first quote after book loss seeds anew.
84    LevelsTradedThrough {
85        /// Rolling seconds, or zero for per-bar totals.
86        seconds: usize,
87    },
88}
89
90impl OrderFlowKind {
91    pub(crate) fn normalized(self) -> Self {
92        match self {
93            Self::BookSize { levels, distance } => Self::BookSize {
94                levels: levels.clamp(1, 10000),
95                distance: if distance.is_nan() {
96                    0.0
97                } else {
98                    distance.clamp(0.0, f64::MAX)
99                },
100            },
101            Self::TradeRate {
102                volume,
103                seconds,
104                instant,
105            } => Self::TradeRate {
106                volume,
107                seconds: seconds.clamp(1, 86400),
108                instant: instant.clamp(1, 86400),
109            },
110            Self::AggressorRate { seconds, instant } => Self::AggressorRate {
111                seconds: seconds.clamp(1, 86400),
112                instant: instant.clamp(1, 86400),
113            },
114            Self::LevelsExecuted { seconds, reversal } => Self::LevelsExecuted {
115                seconds: seconds.min(86400),
116                reversal,
117            },
118            Self::SizeDelta { ranges } => Self::SizeDelta {
119                ranges: ranges.map(|[min, max]| [size_bound(min), size_bound(max)]),
120            },
121            Self::FlowSum { bars } => Self::FlowSum {
122                bars: bars.clamp(1, KEPT_BARS),
123            },
124            Self::FilteredCvd { min, max } => Self::FilteredCvd {
125                min: size_bound(min),
126                max: size_bound(max),
127            },
128            Self::LevelsTradedThrough { seconds } => Self::LevelsTradedThrough {
129                seconds: seconds.min(86400),
130            },
131            Self::Spread => Self::Spread,
132        }
133    }
134}
135
136/// A size bound: NaN counts as zero, and the bound is kept in 0..=`f64::MAX`.
137fn size_bound(size: f64) -> f64 {
138    if size.is_nan() {
139        0.0
140    } else {
141        size.clamp(0.0, f64::MAX)
142    }
143}
144
145/// Bars an input keeps when it drops its oldest: the longest span of a flow sum.
146const KEPT_BARS: usize = 10_000;
147/// Bars an input holds before it drops its oldest down to `KEPT_BARS`. A drop recalculates the
148/// studies on the input in full, so it comes once per 1,001 new bars rather than on every bar.
149const MAX_BARS: usize = 11_000;
150/// Price rows a footprint or book source keeps when it drops its oldest bars.
151const KEPT_ROWS: usize = 100_000;
152/// Price rows a footprint or book source holds before it drops its oldest bars, down to
153/// `KEPT_BARS` bars and `KEPT_ROWS` rows.
154const MAX_ROWS: usize = 110_000;
155
156/// Bars copied from an [`OrderFlowInput`] at one moment, with what they cover.
157#[derive(Clone, Debug, PartialEq)]
158#[non_exhaustive]
159pub struct OrderFlowBars<T> {
160    /// Bars by opening time, from the opening time asked for. A bar before
161    /// [`complete_from`](Self::complete_from) is left out. During a chart replay only bars before
162    /// the [`cutoff`](Self::cutoff) are present, and the bar containing it holds the data before
163    /// the cutoff when the source knows it.
164    pub bars: BTreeMap<DateTime<Utc>, T>,
165    /// Opening time of the oldest bar the source holds in full, or `None` while it holds none.
166    /// Older bars were dropped for retention or lost data. A bar from here on that is absent had
167    /// none, except the bar containing the cutoff, which is also absent when its data before the
168    /// cutoff is not known.
169    pub complete_from: Option<DateTime<Utc>>,
170    /// The exclusive replay cutoff applied to these bars, or `None` outside replay.
171    pub cutoff: Option<DateTime<Utc>>,
172    /// The tick size the source was given: footprint level `n` lies at the price
173    /// `n * tick_size`.
174    pub tick_size: Option<f64>,
175    /// The source's revision when copied. It changes whenever the bars may have changed.
176    pub revision: u64,
177}
178
179/// The last synchronized order book of one bar, as an [`OrderFlowInput::book`] source holds it.
180#[derive(Clone, Debug, PartialEq)]
181#[non_exhaustive]
182pub struct BarBook {
183    /// Time of the update that left the book in this state.
184    pub time: DateTime<Utc>,
185    /// The nearest bid levels, best (highest price) first.
186    pub bids: Vec<OrderBookLevel>,
187    /// The nearest ask levels, best (lowest price) first.
188    pub asks: Vec<OrderBookLevel>,
189}
190
191/// What a source aggregates.
192#[derive(Clone, Copy)]
193enum Source {
194    /// Three outputs per bar.
195    Fixed(OrderFlowKind),
196    /// Each bar's executions by price.
197    Footprint,
198    /// Each bar's last synchronized book, this many levels per side.
199    Book(usize),
200}
201
202/// A chart replay cutoff and the bar containing it, with what is known of that bar before it.
203/// Options rather than an enum: only the chart builds one, and without `widget` an enum's
204/// variants would never be constructed (`dead_code`).
205struct Replay {
206    cutoff: DateTime<Utc>,
207    open: DateTime<Utc>,
208    row: Option<[f64; 3]>,
209    footprint: Option<FootprintBar>,
210    book: Option<BarBook>,
211}
212
213/// Whether `clock`'s bars span time: footprint and book sources keep no others.
214fn time_based(clock: &BarClock) -> bool {
215    clock
216        .timeframe()
217        .is_some_and(|timeframe| !timeframe.is_trade_based())
218}
219
220#[derive(Default)]
221struct Window {
222    events: VecDeque<(DateTime<Utc>, [f64; 2])>,
223    sum: [f64; 2],
224    lost_until: Option<DateTime<Utc>>,
225    now: Option<DateTime<Utc>>,
226}
227
228impl Window {
229    fn push(&mut self, time: DateTime<Utc>, value: [f64; 2], seconds: usize) -> [f64; 2] {
230        let now = self.now.map_or(time, |last| time.max(last));
231        self.now = Some(now);
232        let cutoff = now
233            .checked_sub_signed(TimeDelta::seconds(seconds.clamp(1, 86400) as i64))
234            .unwrap_or(DateTime::<Utc>::MIN_UTC);
235        while self.events.front().is_some_and(|(t, _)| *t <= cutoff) {
236            let (_, old) = self.events.pop_front().unwrap();
237            for (line, old) in old.into_iter().enumerate() {
238                self.sum[line] =
239                    if !self.sum[line].is_finite() || (old > 0.0 && old >= self.sum[line] * 0.5) {
240                        self.events.iter().map(|(_, value)| value[line]).sum()
241                    } else {
242                        self.sum[line] - old
243                    };
244            }
245        }
246        if self.events.is_empty() {
247            self.sum = [0.0; 2];
248        }
249        if time > cutoff && value != [0.0; 2] {
250            let at = self.events.partition_point(|(t, _)| *t <= time);
251            self.events.insert(at, (time, value));
252            for (sum, x) in self.sum.iter_mut().zip(value) {
253                *sum += x;
254            }
255        }
256        if self.events.len() > 100_000 {
257            let (time, old) = self.events.pop_front().unwrap();
258            self.lost_until = self.lost_until.max(Some(time));
259            for (sum, old) in self.sum.iter_mut().zip(old) {
260                *sum -= old;
261            }
262        }
263        if self.lost_until.is_some_and(|t| t > cutoff) {
264            [f64::NAN; 2]
265        } else {
266            self.sum
267        }
268    }
269}
270
271/// Distinct keys with an event inside a window, for counts that must not repeat a key.
272#[derive(Default)]
273struct Distinct {
274    events: VecDeque<(DateTime<Utc>, u64)>,
275    counts: HashMap<u64, usize>,
276    lost_until: Option<DateTime<Utc>>,
277    now: Option<DateTime<Utc>>,
278}
279
280impl Distinct {
281    /// Adds `key` at `time` (none to only expire), then expires events not after the cutoff:
282    /// `seconds` before the newest time seen, or `bar` when given. Returns the distinct keys
283    /// held, NaN while expired events were dropped to stay within bounds.
284    fn push(
285        &mut self,
286        time: DateTime<Utc>,
287        key: Option<u64>,
288        seconds: usize,
289        bar: Option<DateTime<Utc>>,
290    ) -> f64 {
291        let now = self.now.map_or(time, |last| time.max(last));
292        self.now = Some(now);
293        let cutoff = bar
294            .map(|open| open - TimeDelta::nanoseconds(1))
295            .or_else(|| now.checked_sub_signed(TimeDelta::seconds(seconds.clamp(1, 86400) as i64)))
296            .unwrap_or(DateTime::<Utc>::MIN_UTC);
297        while self.events.front().is_some_and(|(t, _)| *t <= cutoff) {
298            let (_, old) = self.events.pop_front().unwrap();
299            self.forget(old);
300        }
301        if let Some(key) = key.filter(|_| time > cutoff) {
302            self.events.push_back((time, key));
303            *self.counts.entry(key).or_default() += 1;
304        }
305        if self.events.len() > 100_000 {
306            let (time, old) = self.events.pop_front().unwrap();
307            self.lost_until = self.lost_until.max(Some(time));
308            self.forget(old);
309        }
310        if self.lost_until.is_some_and(|t| t > cutoff) {
311            f64::NAN
312        } else {
313            self.counts.len() as f64
314        }
315    }
316
317    fn forget(&mut self, key: u64) {
318        if let Some(count) = self.counts.get_mut(&key) {
319            *count -= 1;
320            if *count == 0 {
321                self.counts.remove(&key);
322            }
323        }
324    }
325
326    /// Drops every key, keeping the time seen.
327    fn restart(&mut self) {
328        self.events.clear();
329        self.counts.clear();
330    }
331}
332
333struct State {
334    source: Source,
335    trade_bars: super::inbox::TradeBars,
336    bars: BTreeMap<DateTime<Utc>, ([f64; 3], u64)>,
337    last: Option<DateTime<Utc>>,
338    inside: Option<(f64, f64)>,
339    window: Window,
340    instant: Window,
341    /// Distinct keys of the bar, rolling and instant windows, or of the buy and sell prices.
342    distinct: [Distinct; 3],
343    /// Price of the previous buy and sell execution.
344    last_price: [Option<f64>; 2],
345    revision: u64,
346    reset_revision: u64,
347    changes: BTreeMap<DateTime<Utc>, u64>,
348    filter: Option<OrderFilter>,
349    /// A footprint source's executions, from its first execution with a tick size.
350    footprint: Option<Footprint>,
351    /// A book source's book at the end of each bar.
352    books: BTreeMap<DateTime<Utc>, BarBook>,
353    /// Levels held in `books`.
354    book_rows: usize,
355    tick_size: Option<f64>,
356    replay: Option<Replay>,
357}
358
359/// A bounded, thread-safe source shared by a stream study and its feed subscription.
360///
361/// The chart connects these automatically. Hosts without a feed can push validated historical
362/// and live events through the same handle, in chronological order, then recalculate the study.
363/// Clones share the source, including when a study is cloned for presentation or undo.
364///
365/// At most 11,000 bars are retained: the bar after that drops the oldest at once, down to the
366/// newest 10,000, which still cover the longest flow sum. A study recalculated after a drop has
367/// no values for the dropped bars, and a cumulative one, such as
368/// [`FilteredCvd`](OrderFlowKind::FilteredCvd), starts again from the oldest retained bar. The
369/// chart recalculates the studies on a source in full after each drop, once per 1,001 new bars,
370/// and otherwise updates them from the first changed bar. At most 100,000 events per rolling
371/// window are retained; an overflowing window gives no value until the missing events have
372/// expired, rather than understating the rate.
373///
374/// Besides the fixed aggregations of [`OrderFlowKind`], a source can hold each bar's executions
375/// by price ([`footprint`](Self::footprint)) or its last synchronized book
376/// ([`book`](Self::book)) for a host's study. These keep at most 11,000 bars and 110,000 price
377/// rows, footprint levels or book levels; past either, the oldest bars go at once, down to 10,000
378/// bars and 100,000 rows, and the chart recalculates their studies in full.
379///
380/// During a chart replay every reader stops at the cutoff (see [`OrderFlowBars`]).
381#[derive(Clone)]
382pub struct OrderFlowInput(Arc<Mutex<State>>);
383
384impl OrderFlowInput {
385    /// An empty source for this fixed aggregation.
386    pub fn new(kind: OrderFlowKind) -> Self {
387        Self::of(Source::Fixed(kind.normalized()))
388    }
389
390    /// An empty source of each bar's executions by price, one level per instrument tick, as
391    /// [`Footprint`] aggregates them, for a study to read with
392    /// [`footprints_from`](Self::footprints_from). It keeps nothing until it has a tick size
393    /// ([`set_tick_size`](Self::set_tick_size); the chart gives it its instrument's), and only
394    /// bars of an interval measured in time: under a clock without a timeframe, or with a tick,
395    /// volume, range or delta interval, it ignores executions. An execution older than the one
396    /// before it is left out, as from the chart's footprint.
397    pub fn footprint() -> Self {
398        Self::of(Source::Footprint)
399    }
400
401    /// An empty source of the last synchronized book of each bar, its nearest `levels` on each
402    /// side (clamped to 1..=100), for a study to read with [`books_from`](Self::books_from). A
403    /// bar without a synchronized update keeps no book, and neither does one whose last update
404    /// was a loss. An update with an invalid level, or older than the source's last, is ignored.
405    /// Only bars of an interval measured in time are kept, and an order filter does not apply.
406    pub fn book(levels: usize) -> Self {
407        Self::of(Source::Book(levels.clamp(1, 100)))
408    }
409
410    fn of(source: Source) -> Self {
411        Self(Arc::new(Mutex::new(State::new(source))))
412    }
413
414    /// The same source, with book size summed over the individual orders `filter` accepts. The
415    /// feed must send order events; with aggregate depth only, or when the filter needs a
416    /// participant or placement time that no held order has, a book size bar has no value (NaN),
417    /// instead of an unfiltered one. Other aggregations ignore it. A filter that accepts every
418    /// order is the same as none.
419    pub fn with_order_filter(self, filter: OrderFilter) -> Self {
420        self.state().filter = (filter != OrderFilter::default()).then_some(filter);
421        self
422    }
423
424    /// Feed services read by this source. Book size with an order filter needs individual
425    /// orders; executions need trade-side classification only for side-dependent outputs.
426    pub fn services(&self) -> &'static [crate::model::FeedService] {
427        use crate::model::FeedService::*;
428        let state = self.state();
429        match state.source {
430            Source::Fixed(OrderFlowKind::Spread | OrderFlowKind::LevelsTradedThrough { .. }) => {
431                &[BestBidAsk]
432            }
433            Source::Fixed(OrderFlowKind::BookSize { .. }) if state.filter.is_some() => {
434                &[OrderDepth]
435            }
436            Source::Fixed(OrderFlowKind::BookSize { .. }) | Source::Book(_) => &[Depth],
437            Source::Footprint
438            | Source::Fixed(
439                OrderFlowKind::TradeRate { .. } | OrderFlowKind::AggressorRate { .. },
440            ) => &[LiveTrades],
441            Source::Fixed(
442                OrderFlowKind::LevelsExecuted { .. }
443                | OrderFlowKind::SizeDelta { .. }
444                | OrderFlowKind::FlowSum { .. }
445                | OrderFlowKind::FilteredCvd { .. },
446            ) => &[LiveTrades, TradeSides],
447        }
448    }
449
450    /// The filter of book size, if it has one.
451    pub(crate) fn order_filter(&self) -> Option<OrderFilter> {
452        let state = self.state();
453        state
454            .filter
455            .clone()
456            .filter(|_| matches!(state.source, Source::Fixed(OrderFlowKind::BookSize { .. })))
457    }
458
459    fn state(&self) -> MutexGuard<'_, State> {
460        self.0
461            .lock()
462            .unwrap_or_else(std::sync::PoisonError::into_inner)
463    }
464
465    #[cfg(any(feature = "widget", feature = "alert-engine"))]
466    pub(crate) fn same_source(&self, other: &Self) -> bool {
467        Arc::ptr_eq(&self.0, &other.0)
468    }
469
470    /// Clears values on a new symbol, interval or subscription. The aggregation, order filter,
471    /// tick size and any replay cutoff stay.
472    pub fn clear(&self) {
473        let mut state = self.state();
474        let revision = state.revision.wrapping_add(1);
475        // The cutoff stays, so nothing after it shows; what was known before it is gone.
476        let replay = state.replay.take().map(|replay| Replay {
477            row: None,
478            footprint: None,
479            book: None,
480            ..replay
481        });
482        let filter = state.filter.take();
483        *state = State {
484            filter,
485            tick_size: state.tick_size,
486            replay,
487            revision,
488            reset_revision: revision,
489            ..State::new(state.source)
490        };
491    }
492
493    /// Sets the instrument tick size that footprint levels and range intervals count in. A size
494    /// that is not finite and positive counts as none; a footprint source keeps nothing without
495    /// one. Another size clears the footprint, whose levels no longer apply. Fixed sources use it
496    /// for range bars. The chart sets its instrument's when connecting or subscribing the source.
497    pub fn set_tick_size(&self, tick_size: Option<f64>) {
498        let tick_size = tick_size.filter(|size| size.is_finite() && *size > 0.0);
499        let mut state = self.state();
500        if state.tick_size == tick_size {
501            return;
502        }
503        state.tick_size = tick_size;
504        if state.footprint.take().is_some() {
505            state.changes.clear();
506            state.revision = state.revision.wrapping_add(1);
507            state.reset_revision = state.revision;
508        }
509    }
510
511    #[cfg(feature = "studies")]
512    pub(crate) fn reset_revision(&self) -> u64 {
513        self.state().reset_revision
514    }
515
516    #[cfg(feature = "widget")]
517    pub(crate) fn revision(&self) -> u64 {
518        self.state().revision
519    }
520
521    #[cfg(feature = "widget")]
522    pub(crate) fn changed_since(&self, revision: u64) -> Option<DateTime<Utc>> {
523        let state = self.state();
524        if state.reset_revision > revision {
525            return None;
526        }
527        state
528            .changes
529            .iter()
530            .find(|(_, changed)| **changed > revision)
531            .map(|(time, _)| *time)
532    }
533
534    pub(crate) fn values_from(&self, time: DateTime<Utc>) -> BTreeMap<DateTime<Utc>, [f64; 3]> {
535        let state = self.state();
536        let first = state.bars.keys().next().copied();
537        // The count beside each row only serves the spread; readers drop it.
538        (state.visible(&state.bars, first, time, |replay| {
539            replay.row.map(|row| (row, 0))
540        }))
541        .into_iter()
542        .map(|(time, (row, _))| (time, row))
543        .collect()
544    }
545
546    /// Copies the retained bar outputs in time order. Missing outputs are NaN. During a chart
547    /// replay, as [`OrderFlowBars::bars`]: nothing from the cutoff's bar on, except that bar's
548    /// output of the executions before the cutoff, when known.
549    pub fn values(&self) -> BTreeMap<DateTime<Utc>, [f64; 3]> {
550        self.values_from(DateTime::<Utc>::MIN_UTC)
551    }
552
553    /// Copies the bars of a footprint source that open at or after `open`, with what they
554    /// cover. Empty for other sources.
555    pub fn footprints_from(&self, open: DateTime<Utc>) -> OrderFlowBars<FootprintBar> {
556        let state = self.state();
557        let complete_from = state.footprint.as_ref().and_then(Footprint::complete_from);
558        let bars = match &state.footprint {
559            Some(flow) => state.visible(flow.bars(), complete_from, open, |replay| {
560                replay.footprint.clone()
561            }),
562            None => BTreeMap::new(),
563        };
564        state.snapshot(bars, complete_from)
565    }
566
567    /// The calendar a footprint source opens its bars by: the clock of the first execution it
568    /// received since it was cleared or given another tick size. `None` while it holds no
569    /// footprint, and for other sources.
570    #[cfg(feature = "studies")]
571    pub(crate) fn footprint_clock(&self) -> Option<BarClock> {
572        self.state()
573            .footprint
574            .as_ref()
575            .map(|flow| flow.clock().clone())
576    }
577
578    /// Copies the books of a book source for the bars that open at or after `open`, with what
579    /// they cover. Empty for other sources.
580    pub fn books_from(&self, open: DateTime<Utc>) -> OrderFlowBars<BarBook> {
581        let state = self.state();
582        let complete_from = state.books.keys().next().copied();
583        let bars = state.visible(&state.books, complete_from, open, |replay| {
584            replay.book.clone()
585        });
586        state.snapshot(bars, complete_from)
587    }
588
589    /// Adds a quote. Invalid quotes, tick sizes and timestamps older than this stream are
590    /// ignored. Crossed quotes are retained, so their spread can be negative. Footprint and book
591    /// sources ignore it.
592    pub fn push_quote(&self, clock: &BarClock, quote: &Level1Quote, tick_size: f64) {
593        if !tick_size.is_finite()
594            || tick_size <= 0.0
595            || !quote.bid.is_finite()
596            || !quote.ask.is_finite()
597        {
598            return;
599        }
600        let mut state = self.state();
601        if state.last.is_some_and(|t| quote.ts < t) {
602            return;
603        }
604        let Source::Fixed(kind) = state.source else {
605            return;
606        };
607        let Some(open) = state.open(clock, quote.ts) else {
608            return;
609        };
610        match kind {
611            OrderFlowKind::Spread => {
612                let spread = (quote.ask - quote.bid) / tick_size;
613                let (row, count) = state.bars.entry(open).or_insert(([spread; 3], 0));
614                *count += 1;
615                row[0] = spread;
616                row[1] += (spread - row[1]) / *count as f64;
617                row[2] = row[2].max(spread);
618            }
619            OrderFlowKind::LevelsTradedThrough { seconds } => {
620                let movement = state.inside.map_or([0.0; 2], |(bid, ask)| {
621                    [
622                        ((quote.bid - bid) / tick_size).round().abs(),
623                        ((quote.ask - ask) / tick_size).round().abs(),
624                    ]
625                });
626                state.inside = Some((quote.bid, quote.ask));
627                let values = if seconds == 0 {
628                    let old = state.bars.get(&open).map_or([0.0; 3], |(v, _)| *v);
629                    [old[0] + movement[0], old[1] + movement[1]]
630                } else {
631                    state.window.push(quote.ts, movement, seconds)
632                };
633                state
634                    .bars
635                    .insert(open, ([values[0], values[1], values[0] + values[1]], 0));
636            }
637            _ => return,
638        }
639        state.finish(quote.ts, open);
640    }
641
642    /// Adds an execution already deduplicated by the host or chart's feed. A footprint source
643    /// adds it at its price level; a book source ignores it. Fixed sources on tick, volume,
644    /// range and delta intervals use the same allocations as [`super::aggregate_trades`]. Push
645    /// every execution from the same grouping anchor as the chart, including into quote and
646    /// book size sources, so later updates belong to the current bar. Range intervals need
647    /// [`set_tick_size`](Self::set_tick_size) first. A split fill contributes only its allocated
648    /// size to each bar, with size filters tested against the original execution.
649    pub fn push_trade(&self, clock: &BarClock, trade: &Tick) {
650        if !trade.price.is_finite() || !trade.size.is_finite() || trade.size < 0.0 {
651            return;
652        }
653        let mut state = self.state();
654        if matches!(state.source, Source::Footprint) {
655            return state.push_footprint(clock, trade);
656        }
657        if let Some(timeframe) = clock.timeframe().filter(|tf| tf.is_trade_based()) {
658            let tick_size = state.tick_size;
659            let Ok(parts) = state.trade_bars.push(timeframe, trade, tick_size) else {
660                return;
661            };
662            for (i, (bar, size)) in parts.into_iter().enumerate() {
663                state.add_trade(trade, bar.time, size, i == 0);
664            }
665        } else {
666            state.add_trade(trade, clock.bar_open(trade.ts), trade.size, true);
667        }
668    }
669
670    /// The sink has already validated and allocated this execution to its chart bars.
671    pub(crate) fn push_trade_part(
672        &self,
673        trade: &Tick,
674        open: DateTime<Utc>,
675        size: f64,
676        first: bool,
677    ) {
678        let mut state = self.state();
679        state.trade_bars.forming = Some(open);
680        state.add_trade(trade, open, size, first);
681    }
682
683    pub(crate) fn set_trade_bar(&self, open: DateTime<Utc>) {
684        self.state().trade_bars.forming = Some(open);
685    }
686
687    /// Expires rolling rates and movement during an idle part of a bar with stream data.
688    /// A bar without that data remains missing. Hosts without a feed call this on a heartbeat.
689    pub fn advance_time(&self, clock: &BarClock, now: DateTime<Utc>) {
690        let mut state = self.state();
691        if state.last.is_some_and(|t| now < t) {
692            return;
693        }
694        let Some(open) = state.open(clock, now) else {
695            return;
696        };
697        let Some((row, _)) = state.bars.get(&open) else {
698            return;
699        };
700        let total = row[0];
701        let Source::Fixed(kind) = state.source else {
702            return;
703        };
704        let row = match kind {
705            OrderFlowKind::TradeRate {
706                seconds, instant, ..
707            } => {
708                let rolling = state.window.push(now, [0.0; 2], seconds)[0] / seconds as f64;
709                let fast = state.instant.push(now, [0.0; 2], instant)[0] / instant as f64;
710                [total, rolling, fast]
711            }
712            OrderFlowKind::AggressorRate { seconds, instant } => {
713                let [_, rolling, fast] = &mut state.distinct;
714                [
715                    total,
716                    rolling.push(now, None, seconds, None) / seconds as f64,
717                    fast.push(now, None, instant, None) / instant as f64,
718                ]
719            }
720            OrderFlowKind::LevelsExecuted { seconds, .. } if seconds > 0 => {
721                let [buys, sells, _] = &mut state.distinct;
722                let levels = [
723                    buys.push(now, None, seconds, None),
724                    sells.push(now, None, seconds, None),
725                ];
726                [levels[0], levels[1], levels[0] + levels[1]]
727            }
728            OrderFlowKind::LevelsTradedThrough { seconds } if seconds > 0 => {
729                let values = state.window.push(now, [0.0; 2], seconds);
730                [values[0], values[1], values[0] + values[1]]
731            }
732            _ => return,
733        };
734        state.bars.insert(open, (row, 0));
735        state.finish(now, open);
736    }
737
738    /// Rebuilds the source from the executions the chart holds, in place of what it holds. A
739    /// bar that lost an execution to the store's limit is left out, and a rolling window gives no
740    /// value while it reaches back to the loss, as when the window overflows. On a trade-based
741    /// interval it rebuilds only a source the sink has not allocated an execution to yet, from the
742    /// executions that arrived live.
743    #[cfg(feature = "widget")]
744    pub(crate) fn replace_trades(&self, clock: &BarClock, trades: &super::trades::Trades) {
745        let footprint = match self.state().source {
746            Source::Fixed(
747                OrderFlowKind::TradeRate { .. }
748                | OrderFlowKind::AggressorRate { .. }
749                | OrderFlowKind::LevelsExecuted { .. }
750                | OrderFlowKind::SizeDelta { .. }
751                | OrderFlowKind::FlowSum { .. }
752                | OrderFlowKind::FilteredCvd { .. },
753            ) => false,
754            Source::Footprint => true,
755            _ => return,
756        };
757        let trade_based = clock.timeframe().is_some_and(|tf| tf.is_trade_based());
758        // Only the sink knows how the chart split its executions into trade-based bars.
759        if trade_based && !footprint && self.state().trade_bars.forming.is_some() {
760            return;
761        }
762        let as_of = self.state().last;
763        self.clear();
764        let lost = trades.lost_until();
765        if footprint {
766            // In arrival order, as the chart's footprint is built.
767            for trade in trades.iter() {
768                self.push_trade(clock, trade);
769            }
770            let mut state = self.state();
771            if let (Some(lost), Some(flow)) = (lost, &mut state.footprint) {
772                flow.drop_through(lost, trades.iter());
773            }
774        } else {
775            {
776                let mut state = self.state();
777                state.window.lost_until = lost;
778                state.instant.lost_until = lost;
779                for distinct in &mut state.distinct {
780                    distinct.lost_until = lost;
781                }
782            }
783            // Paged executions belong to history bars, which the chart did not fold from them.
784            // shortcut: after the store evicts a live execution, a new source starts its trade
785            // bars at the oldest held one; keep the sink's allocations if hosts need it exact.
786            let mut sorted: Vec<_> = if trade_based {
787                trades.live().collect()
788            } else {
789                trades.iter().collect()
790            };
791            sorted.sort_by_key(|trade| trade.ts);
792            for trade in sorted {
793                self.push_trade(clock, trade);
794            }
795            if let Some(lost) = lost {
796                let cut = clock.bar_open(lost);
797                let mut state = self.state();
798                state.bars.retain(|open, _| *open > cut);
799                state.changes.retain(|open, _| *open > cut);
800            }
801        }
802        if let Some(now) = as_of {
803            self.advance_time(clock, now);
804        }
805    }
806
807    /// Applies a chart replay cutoff, exclusive, or removes it with `None`. Readers then stop at
808    /// the cutoff's bar. That bar holds the held executions before the cutoff for a source that
809    /// sums them, unless the store dropped one of that bar's, and a book source shows the bar's
810    /// book when the bar's last update came before the cutoff.
811    #[cfg(feature = "widget")]
812    pub(crate) fn set_replay(
813        &self,
814        clock: &BarClock,
815        cutoff: Option<DateTime<Utc>>,
816        trades: Option<&super::trades::Trades>,
817    ) {
818        let mut state = self.state();
819        let replay = cutoff.map(|cutoff| {
820            let trade_timeframe = clock.timeframe().filter(|tf| tf.is_trade_based());
821            // shortcut: scan the held executions per input per seek; index them by time if
822            // replay playback profiles show it.
823            let partial = if let Some(timeframe) = trade_timeframe
824                && let Source::Fixed(kind) = state.source
825                && let Some(trades) = trades.filter(|trades| trades.lost_until().is_none())
826            {
827                let mut part = State::new(Source::Fixed(kind));
828                let mut open = None;
829                // History pages did not form the chart's live trade bars.
830                let mut sorted: Vec<_> = trades.live().collect();
831                sorted.sort_by_key(|trade| trade.ts);
832                for trade in sorted {
833                    let Ok(parts) = part.trade_bars.push(timeframe, trade, state.tick_size) else {
834                        continue;
835                    };
836                    for (i, (bar, size)) in parts.into_iter().enumerate() {
837                        if bar.time <= cutoff {
838                            open = Some(bar.time);
839                        }
840                        if trade.ts < cutoff {
841                            part.add_trade(trade, bar.time, size, i == 0);
842                        }
843                    }
844                    // The first excluded execution can open a bar exactly at the cutoff.
845                    if trade.ts >= cutoff {
846                        break;
847                    }
848                }
849                Some((open.unwrap_or(cutoff), part))
850            } else {
851                None
852            };
853            let trade_based = trade_timeframe.is_some();
854            let open = if let Some((open, _)) = &partial {
855                *open
856            } else if trade_based {
857                state
858                    .bars
859                    .range(..=cutoff)
860                    .next_back()
861                    .map_or(cutoff, |(open, _)| *open)
862            } else {
863                clock.bar_open(cutoff)
864            };
865            let held = trades
866                .filter(|trades| {
867                    (trades.lost_until())
868                        .is_none_or(|lost| !trade_based && clock.bar_open(lost) < open)
869                })
870                .into_iter()
871                .flat_map(|trades| trades.iter())
872                .filter(|trade| (trade_based || open <= trade.ts) && trade.ts < cutoff);
873            let mut replay = Replay {
874                cutoff,
875                open,
876                row: None,
877                footprint: None,
878                book: None,
879            };
880            match state.source {
881                Source::Fixed(kind) => {
882                    if let Some((_, part)) = &partial {
883                        replay.row = part.bars.get(&open).map(|(row, _)| *row);
884                    } else if !trade_based
885                        && matches!(
886                            kind,
887                            OrderFlowKind::SizeDelta { .. }
888                                | OrderFlowKind::FlowSum { .. }
889                                | OrderFlowKind::FilteredCvd { .. }
890                        )
891                    {
892                        let part = OrderFlowInput::new(kind);
893                        held.for_each(|trade| part.push_trade(clock, trade));
894                        replay.row = part.state().bars.get(&open).map(|(row, _)| *row);
895                    }
896                }
897                Source::Footprint => {
898                    if let Some(tick_size) = state.tick_size.filter(|_| time_based(clock))
899                        && let Ok(mut part) = Footprint::new(clock.clone(), tick_size, 1)
900                    {
901                        // In arrival order, so an execution older than the one before is left out.
902                        for trade in held {
903                            let _ = part.push(trade);
904                        }
905                        replay.footprint = part.bars().get(&open).cloned();
906                    }
907                }
908                Source::Book(_) => {
909                    replay.book = (state.books.get(&open))
910                        .filter(|book| book.time < cutoff)
911                        .cloned();
912                }
913            }
914            replay
915        });
916        state.replay = replay;
917        state.revision = state.revision.wrapping_add(1);
918        state.reset_revision = state.revision;
919    }
920
921    /// Adds a synchronized book, or marks its loss with `None`. A loss clears the current
922    /// bar's book values and breaks the inside-market movement path until it is seeded again.
923    /// A book source keeps the nearest levels as its bar's book.
924    pub fn push_book(&self, clock: &BarClock, time: DateTime<Utc>, book: Option<&OrderBook>) {
925        self.book_levels(
926            clock,
927            time,
928            book.map(|b| {
929                b.bids
930                    .iter()
931                    .map(|l| (true, l))
932                    .chain(b.asks.iter().map(|l| (false, l)))
933            }),
934        );
935    }
936
937    pub(crate) fn book_levels<'a>(
938        &self,
939        clock: &BarClock,
940        time: DateTime<Utc>,
941        levels: Option<impl Iterator<Item = (bool, &'a OrderBookLevel)>>,
942    ) {
943        let mut state = self.state();
944        if levels.is_none() {
945            state.inside = None;
946        }
947        let (count, distance) = match state.source {
948            Source::Fixed(OrderFlowKind::BookSize {
949                levels: count,
950                distance,
951            }) => (count, distance),
952            Source::Book(count) => return state.push_bar_book(clock, time, count, levels),
953            _ => return,
954        };
955        if state.last.is_some_and(|t| time < t) {
956            return;
957        }
958        let mut row = [f64::NAN; 3];
959        if let Some(levels) = levels {
960            let mut sizes = [0.0; 2];
961            let mut seen = [0; 2];
962            let mut inside = [None; 2];
963            for (bid, level) in levels {
964                if !level.price.is_finite() || !level.size.is_finite() || level.size < 0.0 {
965                    return;
966                }
967                let side = usize::from(!bid);
968                let first = *inside[side].get_or_insert(level.price);
969                let tolerance = 4.0 * f64::EPSILON * level.price.abs().max(first.abs());
970                if (distance > 0.0 && (level.price - first).abs() <= distance + tolerance)
971                    || (distance == 0.0 && seen[side] < count.max(1))
972                {
973                    sizes[side] += level.size;
974                }
975                seen[side] += 1;
976            }
977            row = [sizes[0], sizes[1], sizes[0] - sizes[1]];
978        }
979        let Some(open) = state.open(clock, time) else {
980            return;
981        };
982        state.bars.insert(open, (row, 0));
983        state.finish(time, open);
984    }
985}
986
987/// The execution's size, negative for a sell, when it is classified and its size lies in
988/// `min..=max`; zero otherwise.
989fn signed_size(trade: &Tick, min: f64, max: f64, size: f64) -> f64 {
990    if !(min..=max).contains(&trade.size) {
991        return 0.0;
992    }
993    match trade.side {
994        Some(OrderSide::Buy) => size,
995        Some(OrderSide::Sell) => -size,
996        None => 0.0,
997    }
998}
999
1000impl State {
1001    fn open(&self, clock: &BarClock, time: DateTime<Utc>) -> Option<DateTime<Utc>> {
1002        if clock.timeframe().is_some_and(|tf| tf.is_trade_based()) {
1003            self.trade_bars.forming
1004        } else {
1005            Some(clock.bar_open(time))
1006        }
1007    }
1008
1009    fn add_trade(&mut self, trade: &Tick, open: DateTime<Utc>, size: f64, first: bool) {
1010        let Source::Fixed(kind) = self.source else {
1011            return;
1012        };
1013        let row = match kind {
1014            OrderFlowKind::TradeRate {
1015                volume,
1016                seconds,
1017                instant,
1018            } => {
1019                let x = if volume { size } else { 1.0 };
1020                let event = if first {
1021                    if volume { trade.size } else { 1.0 }
1022                } else {
1023                    0.0
1024                };
1025                let rolling = self.window.push(trade.ts, [event, 0.0], seconds)[0]
1026                    / seconds.clamp(1, 86400) as f64;
1027                let fast = self.instant.push(trade.ts, [event, 0.0], instant)[0]
1028                    / instant.clamp(1, 86400) as f64;
1029                let total = self.bars.get(&open).map_or(0.0, |(v, _)| v[0]) + x;
1030                [total, rolling, fast]
1031            }
1032            OrderFlowKind::AggressorRate { seconds, instant } => {
1033                let key = trade.aggressor_order_id;
1034                let [bar, rolling, fast] = &mut self.distinct;
1035                [
1036                    bar.push(trade.ts.max(open), key, 1, Some(open)),
1037                    rolling.push(trade.ts, key.filter(|_| first), seconds, None)
1038                        / seconds.clamp(1, 86400) as f64,
1039                    fast.push(trade.ts, key.filter(|_| first), instant, None)
1040                        / instant.clamp(1, 86400) as f64,
1041                ]
1042            }
1043            OrderFlowKind::LevelsExecuted { seconds, reversal } => {
1044                let Some(side) = trade.side else { return };
1045                let side = usize::from(side == OrderSide::Sell);
1046                // Buyers lift offers upward and sellers hit bids downward.
1047                let reversed = self.last_price[side].is_some_and(|last| {
1048                    if side == 0 {
1049                        trade.price < last
1050                    } else {
1051                        trade.price > last
1052                    }
1053                });
1054                self.last_price[side] = Some(trade.price);
1055                if reversal && reversed {
1056                    self.distinct[side].restart();
1057                }
1058                let bar = (seconds == 0).then_some(open);
1059                let mut levels = [f64::NAN; 2];
1060                for (other, level) in levels.iter_mut().enumerate() {
1061                    let key = (other == side).then(|| trade.price.to_bits());
1062                    *level = self.distinct[other].push(
1063                        if seconds == 0 {
1064                            trade.ts.max(open)
1065                        } else {
1066                            trade.ts
1067                        },
1068                        key,
1069                        seconds,
1070                        bar,
1071                    );
1072                }
1073                [levels[0], levels[1], levels[0] + levels[1]]
1074            }
1075            OrderFlowKind::SizeDelta { ranges } => {
1076                let mut row = self.bars.get(&open).map_or([0.0; 3], |(v, _)| *v);
1077                for (sum, [min, max]) in row.iter_mut().zip(ranges) {
1078                    *sum += signed_size(trade, min, max, size);
1079                }
1080                row
1081            }
1082            OrderFlowKind::FlowSum { .. } => {
1083                let old = self.bars.get(&open).map_or([0.0; 3], |(v, _)| *v);
1084                [
1085                    old[0] + size,
1086                    old[1] + signed_size(trade, 0.0, f64::MAX, size),
1087                    0.0,
1088                ]
1089            }
1090            OrderFlowKind::FilteredCvd { min, max } => {
1091                let old = self.bars.get(&open).map_or([0.0; 3], |(v, _)| *v);
1092                let delta = old[0] + signed_size(trade, min, max, size);
1093                [delta, old[1].max(delta), old[2].min(delta)]
1094            }
1095            _ => return,
1096        };
1097        self.bars.insert(open, (row, 0));
1098        let latest = self.last.map_or(trade.ts, |t| t.max(trade.ts));
1099        self.finish(latest, open);
1100    }
1101
1102    fn new(source: Source) -> Self {
1103        Self {
1104            source,
1105            trade_bars: super::inbox::TradeBars::default(),
1106            bars: BTreeMap::new(),
1107            last: None,
1108            inside: None,
1109            window: Window::default(),
1110            instant: Window::default(),
1111            distinct: Default::default(),
1112            last_price: [None; 2],
1113            revision: 0,
1114            reset_revision: 0,
1115            changes: BTreeMap::new(),
1116            filter: None,
1117            footprint: None,
1118            books: BTreeMap::new(),
1119            book_rows: 0,
1120            tick_size: None,
1121            replay: None,
1122        }
1123    }
1124
1125    /// The bars of `stored` that readers see from `open` on: none before `complete_from`, none
1126    /// at or after the replay cutoff's bar, and that bar as `partial` gives it.
1127    fn visible<T: Clone>(
1128        &self,
1129        stored: &BTreeMap<DateTime<Utc>, T>,
1130        complete_from: Option<DateTime<Utc>>,
1131        open: DateTime<Utc>,
1132        partial: impl FnOnce(&Replay) -> Option<T>,
1133    ) -> BTreeMap<DateTime<Utc>, T> {
1134        let Some(from) = complete_from.map(|first| first.max(open)) else {
1135            return BTreeMap::new();
1136        };
1137        let end = self.replay.as_ref().map(|replay| replay.open);
1138        let mut bars: BTreeMap<_, _> = stored
1139            .range(from..)
1140            .take_while(|(time, _)| end.is_none_or(|end| **time < end))
1141            .map(|(time, bar)| (*time, bar.clone()))
1142            .collect();
1143        if let Some(replay) = &self.replay
1144            && replay.open >= from
1145            && let Some(bar) = partial(replay)
1146        {
1147            bars.insert(replay.open, bar);
1148        }
1149        bars
1150    }
1151
1152    fn snapshot<T>(
1153        &self,
1154        bars: BTreeMap<DateTime<Utc>, T>,
1155        complete_from: Option<DateTime<Utc>>,
1156    ) -> OrderFlowBars<T> {
1157        OrderFlowBars {
1158            bars,
1159            complete_from,
1160            cutoff: self.replay.as_ref().map(|replay| replay.cutoff),
1161            tick_size: self.tick_size,
1162            revision: self.revision,
1163        }
1164    }
1165
1166    /// Adds an execution to a footprint source. Past `MAX_BARS` bars or `MAX_ROWS` rows the
1167    /// footprint's budget takes the oldest bar; the rest of the chunk then goes at once, down to
1168    /// `KEPT_BARS` and `KEPT_ROWS`, so the studies start over once per chunk.
1169    fn push_footprint(&mut self, clock: &BarClock, trade: &Tick) {
1170        let Some(tick_size) = self.tick_size.filter(|_| time_based(clock)) else {
1171            return;
1172        };
1173        if self.footprint.is_none() {
1174            let Ok(mut flow) = Footprint::new(clock.clone(), tick_size, 1) else {
1175                return;
1176            };
1177            flow.set_budget(MAX_BARS, MAX_ROWS);
1178            self.footprint = Some(flow);
1179        }
1180        let Some(flow) = &mut self.footprint else {
1181            return;
1182        };
1183        let first = flow.bars().keys().next().copied();
1184        if !matches!(flow.push(trade), Ok(true)) {
1185            return;
1186        }
1187        let dropped = first.is_some() && flow.bars().keys().next().copied() != first;
1188        if dropped {
1189            flow.set_budget(KEPT_BARS, KEPT_ROWS);
1190            flow.set_budget(MAX_BARS, MAX_ROWS);
1191        }
1192        let first = flow.bars().keys().next().copied();
1193        let latest = self.last.map_or(trade.ts, |last| last.max(trade.ts));
1194        self.finish(latest, clock.bar_open(trade.ts));
1195        if dropped {
1196            // `changes` names only held bars, so it shares their bound; readers start over.
1197            if let Some(first) = first {
1198                self.changes.retain(|bar, _| *bar >= first);
1199            }
1200            self.reset_revision = self.revision;
1201        }
1202    }
1203
1204    /// Keeps a book source's book at `time` as its bar's, or forgets the bar's book on a loss
1205    /// (`None`). An update older than the source's last, one with an invalid level, kept or not,
1206    /// and bars that do not span time are ignored. Past `MAX_BARS` bars with an update or
1207    /// `MAX_ROWS` levels, the oldest bars go at once, down to `KEPT_BARS` and `KEPT_ROWS`.
1208    fn push_bar_book<'a>(
1209        &mut self,
1210        clock: &BarClock,
1211        time: DateTime<Utc>,
1212        count: usize,
1213        levels: Option<impl Iterator<Item = (bool, &'a OrderBookLevel)>>,
1214    ) {
1215        if !time_based(clock) || self.last.is_some_and(|last| time < last) {
1216            return;
1217        }
1218        let open = clock.bar_open(time);
1219        let old = match levels {
1220            None => self.books.remove(&open),
1221            Some(levels) => {
1222                let mut book = BarBook {
1223                    time,
1224                    bids: Vec::new(),
1225                    asks: Vec::new(),
1226                };
1227                for (bid, level) in levels {
1228                    if !level.price.is_finite() || !level.size.is_finite() || level.size < 0.0 {
1229                        return;
1230                    }
1231                    let side = if bid { &mut book.bids } else { &mut book.asks };
1232                    if side.len() < count {
1233                        side.push(level.clone());
1234                    }
1235                }
1236                self.book_rows += book.bids.len() + book.asks.len();
1237                self.books.insert(open, book)
1238            }
1239        };
1240        if let Some(old) = old {
1241            self.book_rows -= old.bids.len() + old.asks.len();
1242        }
1243        self.finish(time, open);
1244        // Every bar with a book or a loss has a change record, so the records bound the bars;
1245        // dropping a record drops its bar's book.
1246        if self.changes.len() > MAX_BARS || self.book_rows > MAX_ROWS {
1247            while self.changes.len() > 1
1248                && (self.changes.len() > KEPT_BARS || self.book_rows > KEPT_ROWS)
1249                && let Some((open, _)) = self.changes.pop_first()
1250            {
1251                if let Some(book) = self.books.remove(&open) {
1252                    self.book_rows -= book.bids.len() + book.asks.len();
1253                }
1254            }
1255            self.reset_revision = self.revision;
1256        }
1257    }
1258
1259    fn finish(&mut self, time: DateTime<Utc>, open: DateTime<Utc>) {
1260        self.last = Some(time);
1261        self.revision = self.revision.wrapping_add(1);
1262        self.changes.insert(open, self.revision);
1263        if self.bars.len() > MAX_BARS {
1264            while self.bars.len() > KEPT_BARS {
1265                self.bars.pop_first();
1266            }
1267            // `changes` names only retained bars, so it shares their bound.
1268            if let Some((&first, _)) = self.bars.first_key_value() {
1269                self.changes.retain(|bar, _| *bar >= first);
1270            }
1271            self.reset_revision = self.revision;
1272        }
1273    }
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278    use super::*;
1279    use crate::model::{OrderBook, OrderBookLevel, Timeframe};
1280
1281    #[test]
1282    fn services_name_the_data_each_source_reads() {
1283        use crate::model::FeedService::*;
1284        use OrderFlowKind::*;
1285        let book = BookSize {
1286            levels: 5,
1287            distance: 0.0,
1288        };
1289        let cases: [(_, &[crate::model::FeedService]); 13] = [
1290            (OrderFlowInput::new(Spread), &[BestBidAsk]),
1291            (
1292                OrderFlowInput::new(LevelsTradedThrough { seconds: 0 }),
1293                &[BestBidAsk],
1294            ),
1295            (OrderFlowInput::new(book), &[Depth]),
1296            (
1297                OrderFlowInput::new(book).with_order_filter(OrderFilter {
1298                    min_size: 10.0,
1299                    ..Default::default()
1300                }),
1301                &[OrderDepth],
1302            ),
1303            (
1304                OrderFlowInput::new(book).with_order_filter(OrderFilter::default()),
1305                &[Depth],
1306            ),
1307            (
1308                OrderFlowInput::new(TradeRate {
1309                    volume: false,
1310                    seconds: 60,
1311                    instant: 3,
1312                }),
1313                &[LiveTrades],
1314            ),
1315            (
1316                OrderFlowInput::new(AggressorRate {
1317                    seconds: 60,
1318                    instant: 3,
1319                }),
1320                &[LiveTrades],
1321            ),
1322            (
1323                OrderFlowInput::new(LevelsExecuted {
1324                    seconds: 0,
1325                    reversal: false,
1326                }),
1327                &[LiveTrades, TradeSides],
1328            ),
1329            (
1330                OrderFlowInput::new(SizeDelta {
1331                    ranges: [[0.0, 10.0]; 3],
1332                }),
1333                &[LiveTrades, TradeSides],
1334            ),
1335            (
1336                OrderFlowInput::new(FlowSum { bars: 10 }),
1337                &[LiveTrades, TradeSides],
1338            ),
1339            (
1340                OrderFlowInput::new(FilteredCvd {
1341                    min: 0.0,
1342                    max: 10.0,
1343                }),
1344                &[LiveTrades, TradeSides],
1345            ),
1346            (OrderFlowInput::footprint(), &[LiveTrades]),
1347            (
1348                OrderFlowInput::book(5).with_order_filter(OrderFilter {
1349                    min_size: 10.0,
1350                    ..Default::default()
1351                }),
1352                &[Depth],
1353            ),
1354        ];
1355        for (input, expected) in cases {
1356            assert_eq!(input.services(), expected);
1357        }
1358    }
1359
1360    #[test]
1361    fn late_trades_do_not_clear_an_overflow_gap_early() {
1362        let clock = BarClock::new(Timeframe::Min1);
1363        let start: DateTime<Utc> = "2026-01-05T10:00:00Z".parse().unwrap();
1364        let input = OrderFlowInput::new(OrderFlowKind::TradeRate {
1365            volume: false,
1366            seconds: 3,
1367            instant: 3,
1368        });
1369        for _ in 0..100_001 {
1370            input.push_trade(
1371                &clock,
1372                &Tick::new(start + TimeDelta::seconds(1), 100.0, 1.0, None),
1373            );
1374        }
1375        input.push_trade(&clock, &Tick::new(start, 100.0, 1.0, None));
1376        input.advance_time(&clock, start + TimeDelta::milliseconds(3500));
1377        assert!(
1378            input.values()[&start][1..].iter().all(|x| x.is_nan()),
1379            "the lost trade at second 1 is still in both windows"
1380        );
1381        input.advance_time(&clock, start + TimeDelta::seconds(4));
1382        assert_eq!(&input.values()[&start][1..], &[0.0, 0.0]);
1383    }
1384
1385    #[test]
1386    fn rolling_windows_expire_without_counting_heartbeats_as_events() {
1387        let clock = BarClock::new(Timeframe::Min1);
1388        let start: DateTime<Utc> = "2026-01-05T10:00:00Z".parse().unwrap();
1389        for kind in [
1390            OrderFlowKind::TradeRate {
1391                volume: false,
1392                seconds: 60,
1393                instant: 3,
1394            },
1395            OrderFlowKind::TradeRate {
1396                volume: true,
1397                seconds: 60,
1398                instant: 3,
1399            },
1400            OrderFlowKind::LevelsTradedThrough { seconds: 3 },
1401        ] {
1402            let input = OrderFlowInput::new(kind);
1403            for second in [0, 1] {
1404                input.push_trade(
1405                    &clock,
1406                    &Tick::new(start + TimeDelta::seconds(second), 100.0, 2.0, None),
1407                );
1408                input.push_quote(
1409                    &clock,
1410                    &Level1Quote {
1411                        ts: start + TimeDelta::seconds(second),
1412                        symbol: "A".into(),
1413                        bid: 99.0 + second as f64,
1414                        ask: 101.0 + second as f64,
1415                        bid_size: 1.0,
1416                        ask_size: 1.0,
1417                    },
1418                    1.0,
1419                );
1420            }
1421            for _ in 0..100_001 {
1422                input.advance_time(&clock, start + TimeDelta::seconds(2));
1423            }
1424            assert!(
1425                input.values()[&start].iter().all(|x| x.is_finite()),
1426                "{kind:?}: heartbeats consume no event capacity"
1427            );
1428            input.advance_time(&clock, start + TimeDelta::seconds(4));
1429            let row = input.values()[&start];
1430            assert_eq!(row[2], 0.0, "{kind:?}: events at the cutoff expire");
1431            if matches!(kind, OrderFlowKind::LevelsTradedThrough { .. }) {
1432                assert_eq!(row, [0.0; 3]);
1433            }
1434        }
1435    }
1436
1437    /// Quotes and books between fills belong to the current three-tick bar, even when
1438    /// the feed sends the entire burst before the chart drains it. Paging older executions
1439    /// must keep that allocation during replay too.
1440    #[cfg(feature = "widget")]
1441    #[test]
1442    fn trade_bar_streams_share_the_sinks_allocation_before_drain() {
1443        let clock = BarClock::new("3t".parse().unwrap());
1444        let start: DateTime<Utc> = "2026-01-05T10:00:00Z".parse().unwrap();
1445        let inbox = Arc::new(super::super::inbox::Inbox::default());
1446        let sink = inbox.subscribe("A".into(), clock.clone(), 100, Some(1.0));
1447        let spread = OrderFlowInput::new(OrderFlowKind::Spread);
1448        let book = OrderFlowInput::new(OrderFlowKind::BookSize {
1449            levels: 1,
1450            distance: 0.0,
1451        });
1452        let flow = OrderFlowInput::new(OrderFlowKind::FlowSum { bars: 1 });
1453        inbox.study_inputs(sink.id, vec![spread.clone(), book.clone()]);
1454        for second in 0..6 {
1455            if second == 2 {
1456                inbox.study_inputs(sink.id, vec![spread.clone(), book.clone(), flow.clone()]);
1457            }
1458            let at = start + TimeDelta::seconds(second);
1459            sink.push_tick(Tick::new(at, 100.0, 1.0, Some(OrderSide::Buy)).with_id(second as u64));
1460            sink.push_quote(Level1Quote {
1461                ts: at + TimeDelta::milliseconds(100),
1462                symbol: "A".into(),
1463                bid: 99.0,
1464                ask: 101.0 + second as f64,
1465                bid_size: 1.0,
1466                ask_size: 1.0,
1467            });
1468            sink.push_depth_snapshot(
1469                second as u64,
1470                OrderBook {
1471                    ts: at + TimeDelta::milliseconds(200),
1472                    symbol: "A".into(),
1473                    bids: vec![OrderBookLevel {
1474                        price: 99.0,
1475                        size: second as f64 + 1.0,
1476                        order_cnt: None,
1477                    }],
1478                    asks: vec![],
1479                },
1480            );
1481        }
1482        let opens = [start, start + TimeDelta::seconds(3)];
1483        let got: Vec<_> = [&spread, &book, &flow]
1484            .iter()
1485            .map(|input| input.values().into_iter().collect::<Vec<_>>())
1486            .collect();
1487        assert_eq!(
1488            got,
1489            vec![
1490                vec![(opens[0], [4.0, 3.0, 4.0]), (opens[1], [7.0, 6.0, 7.0])],
1491                vec![(opens[0], [3.0, 0.0, 3.0]), (opens[1], [6.0, 0.0, 6.0])],
1492                vec![(opens[0], [3.0, 3.0, 0.0]), (opens[1], [3.0, 3.0, 0.0])],
1493            ]
1494        );
1495        let (_, updates) = inbox.drain().updates.pop().unwrap();
1496        // An older page belongs to history bars: it neither shifts the live bars of a connected
1497        // source nor those a new source rebuilds from the held executions.
1498        let mut held = super::super::trades::Trades::new(100);
1499        updates
1500            .trades
1501            .iter()
1502            .for_each(|trade| held.push(trade.clone()));
1503        let old = Tick::new(
1504            start - TimeDelta::seconds(1),
1505            100.0,
1506            1.0,
1507            Some(OrderSide::Buy),
1508        );
1509        let boundary = Tick::new(start, 100.0, 1.0, Some(OrderSide::Buy)).with_id(99);
1510        held.prepend(vec![old, boundary], &mut |_, _| ());
1511        let rebuilt = OrderFlowInput::new(OrderFlowKind::FlowSum { bars: 1 });
1512        for input in [&flow, &rebuilt] {
1513            input.replace_trades(&clock, &held);
1514        }
1515        let live: BTreeMap<_, _> = got[2].iter().copied().collect();
1516        assert_eq!([flow.values(), rebuilt.values()], [live.clone(), live]);
1517        let cutoff = start + TimeDelta::seconds(2);
1518        for input in [&spread, &book, &flow] {
1519            input.set_replay(&clock, Some(cutoff), Some(&held));
1520        }
1521        assert_eq!(flow.values(), BTreeMap::from([(start, [2.0, 2.0, 0.0])]));
1522        assert!(
1523            spread.values().is_empty(),
1524            "past quotes cannot be reconstructed"
1525        );
1526        assert!(
1527            book.values().is_empty(),
1528            "past depth cannot be reconstructed"
1529        );
1530        for input in [&spread, &book, &flow] {
1531            input.set_replay(&clock, Some(opens[1]), Some(&held));
1532        }
1533        assert_eq!(spread.values().keys().copied().collect::<Vec<_>>(), [start]);
1534        assert_eq!(book.values().keys().copied().collect::<Vec<_>>(), [start]);
1535        assert_eq!(flow.values(), BTreeMap::from([(start, [3.0, 3.0, 0.0])]));
1536        let mut trades = held;
1537        for second in [6, 7] {
1538            let trade = Tick::new(
1539                start + TimeDelta::seconds(second),
1540                100.0,
1541                1.0,
1542                Some(OrderSide::Buy),
1543            );
1544            sink.push_tick(trade.clone());
1545            trades.push(trade);
1546        }
1547        for input in [&spread, &book, &flow] {
1548            input.set_replay(&clock, Some(start + TimeDelta::seconds(7)), Some(&trades));
1549        }
1550        assert_eq!(spread.values().keys().copied().collect::<Vec<_>>(), opens);
1551        assert_eq!(book.values().keys().copied().collect::<Vec<_>>(), opens);
1552        assert_eq!(
1553            flow.values()[&(start + TimeDelta::seconds(6))],
1554            [1.0, 1.0, 0.0]
1555        );
1556    }
1557
1558    /// Hand counts: ids 7 and 8 fill in the first minute (7 twice, an unidentified fill
1559    /// besides), and 7 again in the next, when the first minute has left the 60 and 3 second
1560    /// windows' opening and their earlier fills have expired.
1561    #[test]
1562    fn aggressor_rate_counts_each_identified_order_once() {
1563        let clock = BarClock::new(Timeframe::Min1);
1564        let start: DateTime<Utc> = "2026-01-05T10:00:00Z".parse().unwrap();
1565        let input = OrderFlowInput::new(OrderFlowKind::AggressorRate {
1566            seconds: 60,
1567            instant: 3,
1568        });
1569        for (second, id) in [
1570            (0, Some(7)),
1571            (1, Some(7)),
1572            (2, Some(8)),
1573            (3, None),
1574            (70, Some(7)),
1575        ] {
1576            let mut fill = Tick::new(start + TimeDelta::seconds(second), 100.0, 1.0, None);
1577            fill.aggressor_order_id = id;
1578            input.push_trade(&clock, &fill);
1579            if second == 3 {
1580                let row = input.values()[&start];
1581                assert_eq!(row, [2.0, 2.0 / 60.0, 2.0 / 3.0]);
1582            }
1583        }
1584        assert_eq!(
1585            input.values()[&(start + TimeDelta::minutes(1))],
1586            [1.0, 1.0 / 60.0, 1.0 / 3.0]
1587        );
1588    }
1589
1590    /// Hand counts for one bar: buys at 100, 101, 102 and 101 again, a sell at 99.5, and a
1591    /// trade of unknown side. Without restarts the buys cover 3 prices; a restart on the
1592    /// reversal at 102 then 101 leaves the one. Quotes, even a cancellation that moves the
1593    /// bid three ticks, add nothing.
1594    #[test]
1595    fn executed_levels_count_distinct_prices_per_side_and_restart_on_reversal() {
1596        let clock = BarClock::new(Timeframe::Min1);
1597        let start: DateTime<Utc> = "2026-01-05T10:00:00Z".parse().unwrap();
1598        for (reversal, want) in [(false, [3.0, 1.0, 4.0]), (true, [1.0, 1.0, 2.0])] {
1599            let input = OrderFlowInput::new(OrderFlowKind::LevelsExecuted {
1600                seconds: 0,
1601                reversal,
1602            });
1603            let quote = |second, bid| Level1Quote {
1604                ts: start + TimeDelta::seconds(second),
1605                symbol: String::new(),
1606                bid,
1607                bid_size: 1.0,
1608                ask: 100.0,
1609                ask_size: 1.0,
1610            };
1611            input.push_quote(&clock, &quote(0, 99.0), 1.0);
1612            for (second, price, side) in [
1613                (1, 100.0, Some(OrderSide::Buy)),
1614                (2, 101.0, Some(OrderSide::Buy)),
1615                (3, 102.0, Some(OrderSide::Buy)),
1616                (4, 101.0, Some(OrderSide::Buy)),
1617                (5, 99.5, Some(OrderSide::Sell)),
1618                (6, 110.0, None),
1619            ] {
1620                let at = start + TimeDelta::seconds(second);
1621                input.push_trade(&clock, &Tick::new(at, price, 1.0, side));
1622            }
1623            input.push_quote(&clock, &quote(7, 96.0), 1.0);
1624            assert_eq!(input.values()[&start], want, "reversal {reversal}");
1625        }
1626    }
1627
1628    /// Hand values: the book of 00:20 replaces that of 00:05 in bar 10:00, and the older one of
1629    /// 00:10 is ignored; in bar 10:01 a loss removes the first book, the next keeps its two best
1630    /// bids; bar 10:02 ends on a loss, and an update with an invalid third bid changes nothing.
1631    #[test]
1632    fn book_sources_keep_each_bars_last_synchronized_book() {
1633        let clock = BarClock::new(Timeframe::Min1);
1634        let start: DateTime<Utc> = "2026-01-05T10:00:00Z".parse().unwrap();
1635        let at = |second| start + TimeDelta::seconds(second);
1636        let level = |price, size| OrderBookLevel {
1637            price,
1638            size,
1639            order_cnt: None,
1640        };
1641        let levels = |sides: &[(f64, f64)]| sides.iter().map(|&(p, s)| level(p, s)).collect();
1642        let input = OrderFlowInput::book(2);
1643        for (second, sides) in [
1644            (
1645                5,
1646                Some((
1647                    vec![(99.0, 1.0), (98.0, 2.0), (97.0, 3.0)],
1648                    vec![(101.0, 4.0), (102.0, 5.0)],
1649                )),
1650            ),
1651            (20, Some((vec![(99.0, 7.0)], vec![(101.0, 8.0)]))),
1652            (10, Some((vec![(90.0, 1.0)], vec![(110.0, 1.0)]))),
1653            (65, Some((vec![(100.0, 1.0)], vec![(102.0, 1.0)]))),
1654            (70, None),
1655            (
1656                75,
1657                Some((
1658                    vec![(100.0, 5.0), (99.0, 6.0), (98.0, 7.0)],
1659                    vec![(102.0, 5.0)],
1660                )),
1661            ),
1662            (125, None),
1663            (
1664                130,
1665                Some((
1666                    vec![(100.0, 1.0), (99.0, 1.0), (98.0, f64::NAN)],
1667                    vec![(101.0, 1.0)],
1668                )),
1669            ),
1670        ] {
1671            let book = sides.map(|(bids, asks): (Vec<_>, Vec<_>)| OrderBook {
1672                ts: at(second),
1673                symbol: "A".into(),
1674                bids: levels(&bids),
1675                asks: levels(&asks),
1676            });
1677            input.push_book(&clock, at(second), book.as_ref());
1678        }
1679        let books = input.books_from(DateTime::<Utc>::MIN_UTC);
1680        let bar = |time, bids: &[(f64, f64)], asks: &[(f64, f64)]| BarBook {
1681            time,
1682            bids: levels(bids),
1683            asks: levels(asks),
1684        };
1685        assert_eq!(
1686            books.bars,
1687            BTreeMap::from([
1688                (at(0), bar(at(20), &[(99.0, 7.0)], &[(101.0, 8.0)])),
1689                (
1690                    at(60),
1691                    bar(at(75), &[(100.0, 5.0), (99.0, 6.0)], &[(102.0, 5.0)])
1692                ),
1693            ])
1694        );
1695        assert_eq!(
1696            (books.complete_from, books.cutoff, books.tick_size),
1697            (Some(at(0)), None, None)
1698        );
1699    }
1700
1701    /// Hand counts, one bar a minute: a footprint of one row per bar drops at bar 11,000, the
1702    /// 11,001st, down to bars 1,001 to 11,000; a book of 200 levels per bar reaches 110,000 rows
1703    /// at bar 549 and drops at bar 550 down to 500 bars, 51 to 550; losses alone keep no book but
1704    /// a change record per bar, bounded the same way. Each drop recalculates in full once, and
1705    /// the next bar updates from itself.
1706    #[test]
1707    fn footprint_and_book_sources_drop_their_oldest_bars_in_chunks() {
1708        let clock = BarClock::new(Timeframe::Min1);
1709        let start: DateTime<Utc> = "2026-01-05T10:00:00Z".parse().unwrap();
1710        let minute = |bar: usize| start + TimeDelta::minutes(bar as i64);
1711        let footprint = OrderFlowInput::footprint();
1712        footprint.set_tick_size(Some(1.0));
1713        let bids: Vec<_> = (1..=100)
1714            .rev()
1715            .map(|price| OrderBookLevel {
1716                price: f64::from(price),
1717                size: 1.0,
1718                order_cnt: None,
1719            })
1720            .collect();
1721        let asks = (101..=200)
1722            .map(|price| OrderBookLevel {
1723                price: f64::from(price),
1724                size: 1.0,
1725                order_cnt: None,
1726            })
1727            .collect();
1728        let deep = OrderBook {
1729            ts: start,
1730            symbol: "A".into(),
1731            bids,
1732            asks,
1733        };
1734        let cases = [
1735            (footprint, 11_000, 1_001),
1736            (OrderFlowInput::book(100), 550, 51),
1737            (OrderFlowInput::book(1), 11_000, 1_001),
1738        ];
1739        for (case, (input, full, kept_from)) in cases.into_iter().enumerate() {
1740            let push = |bar| match case {
1741                0 => input.push_trade(
1742                    &clock,
1743                    &Tick::new(minute(bar), 100.0, 1.0, Some(OrderSide::Buy)),
1744                ),
1745                1 => input.push_book(&clock, minute(bar), Some(&deep)),
1746                _ => input.push_book(&clock, minute(bar), None),
1747            };
1748            let held = || {
1749                if case == 0 {
1750                    let copy = input.footprints_from(DateTime::<Utc>::MIN_UTC);
1751                    (copy.bars.len(), copy.complete_from)
1752                } else {
1753                    let copy = input.books_from(DateTime::<Utc>::MIN_UTC);
1754                    (copy.bars.len(), copy.complete_from)
1755                }
1756            };
1757            // Losses keep no book.
1758            let bars = |from: usize, to: usize| {
1759                if case == 2 {
1760                    (0, None)
1761                } else {
1762                    (to - from, Some(minute(from)))
1763                }
1764            };
1765            for bar in 0..full {
1766                push(bar);
1767            }
1768            assert_eq!(held(), bars(0, full), "case {case}, {full} bars");
1769            #[cfg(feature = "widget")]
1770            let before = input.revision();
1771            push(full);
1772            assert_eq!(
1773                held(),
1774                bars(kept_from, full + 1),
1775                "case {case}, drop at bar {full}"
1776            );
1777            #[cfg(feature = "widget")]
1778            assert_eq!(
1779                input.changed_since(before),
1780                None,
1781                "case {case}: full recalculation"
1782            );
1783            #[cfg(feature = "widget")]
1784            let before = input.revision();
1785            push(full + 1);
1786            assert_eq!(held(), bars(kept_from, full + 2), "case {case}, next bar");
1787            #[cfg(feature = "widget")]
1788            assert_eq!(input.changed_since(before), Some(minute(full + 1)));
1789        }
1790    }
1791
1792    /// Hand levels: 100.2 is 200.4 ticks of 0.5, level 200, and 400.8 ticks of 0.25, level 401.
1793    /// A footprint keeps nothing without a tick size or on bars that do not span time.
1794    #[test]
1795    fn footprint_sources_follow_their_tick_size_and_time_bars() {
1796        let clock = BarClock::new(Timeframe::Min1);
1797        let start: DateTime<Utc> = "2026-01-05T10:00:00Z".parse().unwrap();
1798        let input = OrderFlowInput::footprint();
1799        let push = |clock: &BarClock, second, price| {
1800            let at = start + TimeDelta::seconds(second);
1801            input.push_trade(clock, &Tick::new(at, price, 1.0, Some(OrderSide::Buy)));
1802        };
1803        let held = || {
1804            let copy = input.footprints_from(DateTime::<Utc>::MIN_UTC);
1805            let levels: Vec<_> = (copy.bars.values())
1806                .flat_map(|bar| bar.levels.keys().copied())
1807                .collect();
1808            (copy.tick_size, levels)
1809        };
1810        push(&clock, 0, 100.2);
1811        assert_eq!(held(), (None, vec![]), "no tick size");
1812        input.set_tick_size(Some(0.5));
1813        push(&clock, 1, 100.2);
1814        assert_eq!(held(), (Some(0.5), vec![200]));
1815        input.set_tick_size(Some(0.25));
1816        assert_eq!(
1817            held(),
1818            (Some(0.25), vec![]),
1819            "another size clears the levels"
1820        );
1821        push(&clock, 2, 100.2);
1822        assert_eq!(held(), (Some(0.25), vec![401]));
1823        input.set_tick_size(Some(f64::NAN));
1824        assert_eq!(held(), (None, vec![]), "NaN is no tick size");
1825        input.set_tick_size(Some(1.0));
1826        push(&BarClock::new("1t".parse().unwrap()), 3, 100.0);
1827        assert_eq!(held(), (Some(1.0), vec![]), "tick bars do not span time");
1828    }
1829
1830    /// Hand values: a store of two drops the buy of 00:10 when the sell of 01:05 arrives, so bar
1831    /// 10:00 is cut and bar 10:01 whole. The 60 second window at 01:05 reaches back to the loss;
1832    /// the 3 second window holds only the sell of 4.
1833    #[cfg(feature = "widget")]
1834    #[test]
1835    fn rebuilt_sources_leave_out_what_the_store_dropped() {
1836        let clock = BarClock::new(Timeframe::Min1);
1837        let start: DateTime<Utc> = "2026-01-05T10:00:00Z".parse().unwrap();
1838        let at = |second| start + TimeDelta::seconds(second);
1839        let mut store = super::super::trades::Trades::new(2);
1840        for (second, size, side) in [
1841            (10, 1.0, OrderSide::Buy),
1842            (50, 2.0, OrderSide::Buy),
1843            (65, 4.0, OrderSide::Sell),
1844        ] {
1845            store.push(Tick::new(at(second), 100.0, size, Some(side)));
1846        }
1847        let rate = OrderFlowInput::new(OrderFlowKind::TradeRate {
1848            volume: true,
1849            seconds: 60,
1850            instant: 3,
1851        });
1852        rate.replace_trades(&clock, &store);
1853        let values = rate.values();
1854        assert_eq!(values.keys().copied().collect::<Vec<_>>(), [at(60)]);
1855        let [total, rolling, instant] = values[&at(60)];
1856        assert_eq!((total, instant), (4.0, 4.0 / 3.0));
1857        assert!(
1858            rolling.is_nan(),
1859            "the rolling window reaches back to the loss"
1860        );
1861        let footprint = OrderFlowInput::footprint();
1862        footprint.set_tick_size(Some(1.0));
1863        footprint.replace_trades(&clock, &store);
1864        let flow = footprint.footprints_from(DateTime::<Utc>::MIN_UTC);
1865        assert_eq!(flow.complete_from, Some(at(60)));
1866        assert_eq!(flow.bars.keys().copied().collect::<Vec<_>>(), [at(60)]);
1867        assert_eq!(flow.bars[&at(60)].bid, 4.0);
1868    }
1869}