1use 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#[derive(Clone, Copy, Debug, PartialEq)]
12#[non_exhaustive]
13pub enum OrderFlowKind {
14 Spread,
16 BookSize {
22 levels: usize,
24 distance: f64,
26 },
27 TradeRate {
30 volume: bool,
32 seconds: usize,
34 instant: usize,
36 },
37 AggressorRate {
42 seconds: usize,
44 instant: usize,
46 },
47 LevelsExecuted {
53 seconds: usize,
55 reversal: bool,
58 },
59 SizeDelta {
63 ranges: [[f64; 2]; 3],
65 },
66 FlowSum {
68 bars: usize,
70 },
71 FilteredCvd {
77 min: f64,
79 max: f64,
81 },
82 LevelsTradedThrough {
85 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
136fn 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
145const KEPT_BARS: usize = 10_000;
147const MAX_BARS: usize = 11_000;
150const KEPT_ROWS: usize = 100_000;
152const MAX_ROWS: usize = 110_000;
155
156#[derive(Clone, Debug, PartialEq)]
158#[non_exhaustive]
159pub struct OrderFlowBars<T> {
160 pub bars: BTreeMap<DateTime<Utc>, T>,
165 pub complete_from: Option<DateTime<Utc>>,
170 pub cutoff: Option<DateTime<Utc>>,
172 pub tick_size: Option<f64>,
175 pub revision: u64,
177}
178
179#[derive(Clone, Debug, PartialEq)]
181#[non_exhaustive]
182pub struct BarBook {
183 pub time: DateTime<Utc>,
185 pub bids: Vec<OrderBookLevel>,
187 pub asks: Vec<OrderBookLevel>,
189}
190
191#[derive(Clone, Copy)]
193enum Source {
194 Fixed(OrderFlowKind),
196 Footprint,
198 Book(usize),
200}
201
202struct Replay {
206 cutoff: DateTime<Utc>,
207 open: DateTime<Utc>,
208 row: Option<[f64; 3]>,
209 footprint: Option<FootprintBar>,
210 book: Option<BarBook>,
211}
212
213fn 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#[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 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 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: [Distinct; 3],
343 last_price: [Option<f64>; 2],
345 revision: u64,
346 reset_revision: u64,
347 changes: BTreeMap<DateTime<Utc>, u64>,
348 filter: Option<OrderFilter>,
349 footprint: Option<Footprint>,
351 books: BTreeMap<DateTime<Utc>, BarBook>,
353 book_rows: usize,
355 tick_size: Option<f64>,
356 replay: Option<Replay>,
357}
358
359#[derive(Clone)]
382pub struct OrderFlowInput(Arc<Mutex<State>>);
383
384impl OrderFlowInput {
385 pub fn new(kind: OrderFlowKind) -> Self {
387 Self::of(Source::Fixed(kind.normalized()))
388 }
389
390 pub fn footprint() -> Self {
398 Self::of(Source::Footprint)
399 }
400
401 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 pub fn with_order_filter(self, filter: OrderFilter) -> Self {
420 self.state().filter = (filter != OrderFilter::default()).then_some(filter);
421 self
422 }
423
424 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 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 pub fn clear(&self) {
473 let mut state = self.state();
474 let revision = state.revision.wrapping_add(1);
475 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 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 (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 pub fn values(&self) -> BTreeMap<DateTime<Utc>, [f64; 3]> {
550 self.values_from(DateTime::<Utc>::MIN_UTC)
551 }
552
553 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 #[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 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 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 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 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 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 #[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 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 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 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 #[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 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 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 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 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 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
987fn 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 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 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 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 if let Some(first) = first {
1198 self.changes.retain(|bar, _| *bar >= first);
1199 }
1200 self.reset_revision = self.revision;
1201 }
1202 }
1203
1204 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 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 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 #[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 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 #[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 #[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, "e(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, "e(7, 96.0), 1.0);
1624 assert_eq!(input.values()[&start], want, "reversal {reversal}");
1625 }
1626 }
1627
1628 #[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 #[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 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 #[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 #[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}