1use super::{Inbox, Live, TakenIds, Updates};
5use crate::data::depth::{Book, Lost};
6use crate::data::depth_history::Sampler;
7use crate::data::merge::{Origin, fold, valid};
8use crate::data::trades::{self, Trades};
9use crate::data::{FeedError, FeedStatus, OrderEvent, OrderQueue, RestingOrder, SubscriptionId};
10use crate::model::{Bar, BarClock, Level1Quote, OrderBook, Tick};
11use chrono::{DateTime, TimeDelta, Utc};
12use std::collections::btree_map::Entry;
13use std::collections::{HashSet, VecDeque};
14use std::sync::Arc;
15use std::sync::atomic::{AtomicU64, Ordering};
16
17const MAX_PENDING_BARS: usize = 100_000;
19
20pub fn aggregate_trades(
34 trades: &[Tick],
35 clock: impl Into<BarClock>,
36) -> Result<Vec<Bar>, FeedError> {
37 aggregate_trades_with_tick_size(trades, clock, None)
38}
39
40pub fn aggregate_trades_with_tick_size(
43 trades: &[Tick],
44 clock: impl Into<BarClock>,
45 tick_size: Option<f64>,
46) -> Result<Vec<Bar>, FeedError> {
47 let Some(last) = trades.last() else {
48 return Ok(Vec::new());
49 };
50 let clock: BarClock = clock.into();
51 if matches!(clock.timeframe(), Some(crate::model::Timeframe::Range(_)))
52 && !tick_size.is_some_and(|size| size.is_finite() && size > 0.0)
53 {
54 return Err(FeedError::InvalidData(format!(
55 "a range interval needs a finite tick size above zero, got {tick_size:?}"
56 )));
57 }
58 let inbox = Arc::new(Inbox::default());
59 let sink = inbox.subscribe(String::new(), clock, trades.len(), tick_size);
60 sink.set_server_time(last.ts);
61 {
62 let mut state = inbox.state();
64 let live = state
65 .subscriptions
66 .get_mut(&sink.id)
67 .expect("active subscription");
68 let system = Utc::now();
69 for (at, trade) in trades.iter().enumerate() {
70 let (rejected, late) = (live.updates.rejected, live.updates.late_ticks);
71 live.tick(trade.clone(), system);
72 let reason = if live.updates.late_ticks > late {
73 "is out of time order".to_owned()
74 } else if live.updates.rejected == rejected {
75 continue;
76 } else if !trade.price.is_finite() {
77 return Err(FeedError::InvalidData(format!(
78 "trade {at} price must be finite, got {:?}",
79 trade.price
80 )));
81 } else if !trade.size.is_finite() || trade.size < 0.0 {
82 return Err(FeedError::InvalidData(format!(
83 "trade {at} size must be finite and at least 0, got {:?}",
84 trade.size
85 )));
86 } else if trade.ts > last.ts {
87 format!("is later than the last trade, at {}", last.ts)
88 } else {
89 "does not fold into the bars of the interval".to_owned()
90 };
91 return Err(FeedError::InvalidData(format!(
92 "trade {at} at {} {reason}",
93 trade.ts
94 )));
95 }
96 }
97 let (_, updates) = inbox.drain().updates.pop().expect("active subscription");
98 Ok(updates.bars.into_values().map(|(bar, _)| bar).collect())
99}
100
101impl Inbox {
102 pub(crate) fn subscribe(
105 self: &Arc<Self>,
106 symbol: String,
107 clock: BarClock,
108 trade_capacity: usize,
109 tick_size: Option<f64>,
110 ) -> Sink {
111 static NEXT: AtomicU64 = AtomicU64::new(0);
112 let id = SubscriptionId(NEXT.fetch_add(1, Ordering::Relaxed));
113 let live = Live {
114 symbol,
115 clock,
116 forming: None,
117 offset: TimeDelta::zero(),
118 count_bar: None,
119 delta: 0.0,
120 event_closed: false,
121 last_trade: None,
122 last_close: None,
123 empty: false,
124 book: Book::new(tick_size),
125 orders: None,
126 orders_changed: false,
127 tick_size,
128 sampler: None,
129 study_inputs: Vec::new(),
130 taken: TakenIds {
131 order: VecDeque::new(),
132 ids: HashSet::new(),
133 capacity: trade_capacity,
134 },
135 updates: Updates::new(Trades::new(trade_capacity)),
136 };
137 self.state().subscriptions.insert(id, live);
138 Sink {
139 inbox: Arc::clone(self),
140 id,
141 }
142 }
143}
144
145#[derive(Clone)]
180pub struct Sink {
181 inbox: Arc<Inbox>,
182 pub(crate) id: SubscriptionId,
183}
184
185impl Sink {
186 pub fn reset_replay(&self, time: DateTime<Utc>) {
192 self.update(|live| {
193 live.forming = None;
194 live.count_bar = None;
195 live.delta = 0.0;
196 live.event_closed = false;
197 live.last_trade = None;
198 live.last_close = None;
199 live.empty = false;
200 live.offset = time - Utc::now();
201 live.book = Book::new(live.tick_size);
202 live.orders = None;
203 live.orders_changed = true;
204 live.sampler = live.sampler.take().map(|s| Sampler::new(s.config));
205 live.taken.order.clear();
206 live.taken.ids.clear();
207 live.updates = Updates::new(live.updates.trades.empty());
208 live.updates.reset = true;
209 live.updates.replay_depth = live.sampler.as_ref().map(|sampler| {
210 crate::data::DepthHistory::new(sampler.config.clone())
211 .expect("existing depth history configuration")
212 });
213 for input in &live.study_inputs {
214 input.clear();
215 }
216 });
217 }
218
219 pub fn push_bar(&self, bar: Bar) {
227 let system = Utc::now();
228 self.update(|live| {
229 let slot = live.clock.bar_open(bar.time);
230 if !valid(&bar) || live.ahead(slot, system) {
231 live.updates.rejected += 1;
232 return;
233 }
234 if live.clock.timeframe().is_some_and(|tf| tf.is_trade_based())
235 && live.forming.is_none_or(|last| slot >= last)
236 {
237 live.count_bar = None;
238 live.event_closed = false;
239 live.delta = 0.0;
240 live.last_trade = Some(slot);
241 for input in &live.study_inputs {
242 input.set_trade_bar(slot);
243 }
244 }
245 if live.forming.is_none_or(|last| slot >= last) {
246 if !bar.is_whitespace() {
247 live.last_close = Some(bar.close);
248 }
249 live.empty = false;
250 }
251 live.forming = live.forming.max(Some(slot));
252 live.updates.bars.insert(slot, (bar, Origin::Live));
253 });
254 }
255
256 pub fn push_tick(&self, tick: Tick) {
276 let system = Utc::now();
277 self.update(|live| live.tick(tick, system));
278 }
279
280 pub fn advance_time(&self, now: DateTime<Utc>) {
291 let system = Utc::now();
292 self.inbox.push(|state| {
293 let Some(live) = state.subscriptions.get_mut(&self.id) else {
294 return false;
295 };
296 let sampled = live.sample_book(now);
297 if live
298 .clock
299 .timeframe()
300 .is_none_or(|tf| tf.duration_ms() <= 0)
301 {
302 return sampled;
303 }
304 if live.ahead(live.clock.bar_open(now), system) {
305 return false;
306 }
307 live.updates.time = live.updates.time.max(Some(now));
308 let open = live.clock.bar_open(now);
309 let Some(close) = live.last_close else {
310 return sampled || !live.study_inputs.is_empty();
311 };
312 if !live.clock.is_open(now) || live.forming.is_none_or(|last| open <= last) {
313 return sampled || !live.study_inputs.is_empty();
314 }
315 live.updates.bars.insert(
316 open,
317 (
318 Bar::new(open, close, close, close, close, 0.0),
319 Origin::Ticks(0),
320 ),
321 );
322 live.forming = Some(open);
323 live.empty = true;
324 live.bound_bars();
325 true
326 });
327 }
328
329 pub fn push_depth_snapshot(&self, sequence: u64, book: OrderBook) {
335 self.update(|live| {
336 live.orders = None;
337 live.orders_changed = true;
338 live.depth(book.ts, |depth| depth.snapshot(sequence, book));
339 });
340 }
341
342 pub fn push_depth_delta(&self, sequence: u64, changes: &OrderBook) {
355 self.update(|live| {
356 live.orders = None;
357 live.orders_changed = true;
358 live.depth(changes.ts, |depth| depth.delta(sequence, changes));
359 });
360 }
361
362 pub fn push_order_snapshot(
368 &self,
369 sequence: u64,
370 time: DateTime<Utc>,
371 orders: Vec<RestingOrder>,
372 ) {
373 self.update(|live| {
374 live.sample_book(time);
375 let book = live
376 .orders
377 .get_or_insert_with(|| OrderQueue::new(live.tick_size));
378 let result = book.set_snapshot(sequence, time, orders);
379 let trades = book.take_executions();
380 live.sync_orders(sequence, time, result);
381 for trade in trades {
382 live.tick(trade, Utc::now());
383 }
384 });
385 }
386
387 pub fn push_order_event(&self, sequence: u64, time: DateTime<Utc>, event: OrderEvent) {
392 self.update(|live| {
393 live.sample_book(time);
394 let book = live
395 .orders
396 .get_or_insert_with(|| OrderQueue::new(live.tick_size));
397 let result = book.event(sequence, time, event);
398 let trades = book.take_executions();
399 live.sync_orders(sequence, time, result);
400 for trade in trades {
401 live.tick(trade, Utc::now());
402 }
403 });
404 }
405
406 pub fn push_quote(&self, quote: Level1Quote) {
413 let system = Utc::now();
414 self.update(|live| {
415 if quote.symbol != live.symbol {
416 return;
417 }
418 if !quote.bid.is_finite()
419 || !quote.ask.is_finite()
420 || !quote.bid_size.is_finite()
421 || !quote.ask_size.is_finite()
422 || quote.bid_size < 0.0
423 || quote.ask_size < 0.0
424 || live.ahead(live.clock.bar_open(quote.ts), system)
425 {
426 live.updates.error =
427 Some(FeedError::InvalidData("invalid best bid/ask quote".into()));
428 } else if live
429 .updates
430 .quote
431 .as_ref()
432 .is_none_or(|old| quote.ts >= old.ts)
433 {
434 if let Some(tick) = live.tick_size {
435 for input in &live.study_inputs {
436 input.push_quote(&live.clock, "e, tick);
437 }
438 }
439 live.updates.quote = Some(quote);
440 }
441 });
442 }
443
444 pub fn set_server_time(&self, now: DateTime<Utc>) {
457 let offset = now - Utc::now();
458 if let Some(live) = self.inbox.state().subscriptions.get_mut(&self.id) {
460 live.offset = offset;
461 }
462 }
463
464 pub fn set_status(&self, status: FeedStatus) {
466 self.update(|live| live.updates.status = Some(status));
467 }
468
469 pub fn report_error(&self, error: FeedError) {
471 self.update(|live| live.updates.error = Some(error));
472 }
473
474 pub fn symbols_changed(&self) {
477 self.update(|live| live.updates.symbols_changed = true);
478 }
479
480 pub fn is_active(&self) -> bool {
482 self.inbox.state().subscriptions.contains_key(&self.id)
483 }
484
485 pub(super) fn update(&self, update: impl FnOnce(&mut Live)) {
486 self.inbox
487 .push(|state| match state.subscriptions.get_mut(&self.id) {
488 Some(live) => {
489 update(live);
490 live.bound_bars();
491 true
492 }
493 None => false,
494 });
495 }
496}
497
498impl TakenIds {
499 fn insert(&mut self, id: u64) {
500 if self.ids.insert(id) {
501 self.order.push_back(id);
502 if self.order.len() > self.capacity
503 && let Some(oldest) = self.order.pop_front()
504 {
505 self.ids.remove(&oldest);
506 }
507 }
508 }
509}
510
511#[derive(Clone, Default)]
513pub(crate) struct TradeBars {
514 pub(crate) forming: Option<DateTime<Utc>>,
515 current: Option<(Bar, u64)>,
516 delta: f64,
517 closed: bool,
518 last: Option<DateTime<Utc>>,
519}
520
521impl TradeBars {
522 pub(crate) fn push(
523 &mut self,
524 timeframe: crate::model::Timeframe,
525 tick: &Tick,
526 tick_size: Option<f64>,
527 ) -> Result<Vec<(Bar, f64)>, ()> {
528 use crate::model::{OrderSide, Timeframe};
529 if self.last.is_some_and(|last| tick.ts < last) {
530 return Err(());
531 }
532 let mut forming = self.forming;
533 let mut current = self.current.clone();
534 let (mut delta, mut closed) = (self.delta, self.closed);
535 let mut remaining = tick.size;
536 let mut bars = Vec::new();
537 loop {
538 if bars.len() == 100_000 {
539 return Err(());
540 }
541 let starts = closed || current.is_none();
542 let (old, count) = if starts {
543 let time = match forming.filter(|last| *last >= tick.ts) {
544 Some(last) => last
545 .checked_add_signed(TimeDelta::nanoseconds(1))
546 .ok_or(())?,
547 None => tick.ts,
548 };
549 delta = 0.0;
550 (
551 Bar::new(time, tick.price, tick.price, tick.price, tick.price, 0.0),
552 0,
553 )
554 } else {
555 current.clone().ok_or(())?
556 };
557 let size = match timeframe {
558 Timeframe::Volume(limit) => remaining.min(limit.get() as f64 - old.volume),
559 Timeframe::Delta(limit) => match tick.side {
560 Some(OrderSide::Buy) => remaining.min(limit.get() as f64 - delta),
561 Some(OrderSide::Sell) => remaining.min(limit.get() as f64 + delta),
562 None => remaining,
563 },
564 _ => remaining,
565 };
566 let fill = Bar::new(
567 old.time, tick.price, tick.price, tick.price, tick.price, size,
568 );
569 let bar = fold(&old, &fill).ok_or(())?;
570 delta += match tick.side {
571 Some(OrderSide::Buy) => size,
572 Some(OrderSide::Sell) => -size,
573 None => 0.0,
574 };
575 closed = match timeframe {
576 Timeframe::Ticks(limit) => count + 1 >= limit.get(),
577 Timeframe::Volume(limit) => bar.volume >= limit.get() as f64,
578 Timeframe::Delta(limit) => delta.abs() >= limit.get() as f64,
579 Timeframe::Range(ticks) => {
580 let tick_size = tick_size.filter(|s| s.is_finite() && *s > 0.0).ok_or(())?;
581 let range = ticks.get() as f64 * tick_size;
582 if !range.is_finite() {
583 return Err(());
584 }
585 (bar.high / tick_size).round() - (bar.low / tick_size).round()
586 >= ticks.get() as f64
587 }
588 _ => return Err(()),
589 };
590 forming = Some(bar.time);
591 current = Some((bar.clone(), count + 1));
592 bars.push((bar, size));
593 let left = remaining - size;
594 if left <= 0.0 {
595 break;
596 }
597 if left == remaining {
598 return Err(());
599 }
600 remaining = left;
601 }
602 self.forming = forming;
603 self.current = current;
604 self.delta = delta;
605 self.closed = closed;
606 self.last = Some(tick.ts);
607 Ok(bars)
608 }
609}
610
611impl Live {
612 #[cfg(any(feature = "widget", feature = "alert-engine"))]
613 pub(super) fn connect_study_inputs(&mut self, inputs: Vec<crate::data::OrderFlowInput>) {
614 if self.clock.timeframe().is_some_and(|tf| tf.is_trade_based()) {
615 for input in &inputs {
616 if !self.study_inputs.iter().any(|old| input.same_source(old)) {
617 for trade in self.updates.trades.iter() {
619 input.push_trade(&self.clock, trade);
620 }
621 if let Some(open) = self.forming {
622 input.set_trade_bar(open);
623 }
624 }
625 }
626 }
627 self.study_inputs = inputs;
628 }
629
630 fn bound_bars(&mut self) {
631 while self.updates.bars.len() > MAX_PENDING_BARS {
632 self.updates.bars.pop_first();
633 self.updates.dropped_bars += 1;
634 }
635 }
636
637 fn ahead(&self, open: DateTime<Utc>, system: DateTime<Utc>) -> bool {
640 let Some(now) = system.checked_add_signed(self.offset) else {
641 return false;
642 };
643 let intraday = self
647 .clock
648 .timeframe()
649 .is_some_and(|tf| tf.duration_ms() < TimeDelta::days(1).num_milliseconds());
650 let allowance = if intraday {
651 TimeDelta::minutes(1)
652 } else {
653 TimeDelta::weeks(2)
654 };
655 let beyond = now
656 .checked_add_signed(allowance)
657 .is_some_and(|limit| open > limit);
658 beyond
660 && open
661 .checked_sub_signed(TimeDelta::milliseconds(1))
662 .is_some_and(|before| self.clock.bar_open(before) > self.clock.bar_open(now))
663 }
664
665 fn tick(&mut self, tick: Tick, system: DateTime<Utc>) {
666 let live = self;
667
668 if live.clock.timeframe().is_some_and(|tf| tf.is_trade_based()) {
669 if !trades::valid(&tick) || live.ahead(tick.ts, system) {
670 live.updates.rejected += 1;
671 } else if tick.id.is_some_and(|id| live.taken.ids.contains(&id)) {
672 live.updates.repeated_ticks += 1;
673 if tick.aggressor_order_id.is_some() || tick.resting_order_id.is_some() {
674 live.updates.trades.push(tick);
675 }
676 } else if live.last_trade.is_some_and(|last| tick.ts < last) {
677 live.updates.late_ticks += 1;
678 } else if live.fold_trade_bar(&tick).is_err() {
679 live.updates.rejected += 1;
680 } else {
681 live.last_trade = Some(tick.ts);
682 if let Some(id) = tick.id {
683 live.taken.insert(id);
684 }
685 live.updates.trades.push(tick);
686 }
687 return;
688 }
689 let (open, price) = (live.clock.bar_open(tick.ts), tick.price);
690 let ticks = Bar::new(open, price, price, price, price, tick.size);
691 let ahead = live.ahead(open, system);
692 let updates = &mut live.updates;
693 if !trades::valid(&tick) || ahead {
694 updates.rejected += 1;
695 } else if tick.id.is_some_and(|id| live.taken.ids.contains(&id)) {
696 updates.repeated_ticks += 1;
697 if tick.aggressor_order_id.is_some() || tick.resting_order_id.is_some() {
698 updates.trades.push(tick);
699 }
700 } else if live.last_trade.is_some_and(|last| tick.ts < last)
701 || live.forming.is_some_and(|forming| open < forming)
702 {
703 updates.late_ticks += 1;
704 } else {
705 let replace_empty = live.empty && live.forming == Some(open);
706 let origin = if replace_empty {
707 Origin::TicksAfterHeartbeat(1)
708 } else {
709 Origin::Ticks(1)
710 };
711 match updates.bars.entry(open) {
712 Entry::Occupied(mut pending) if replace_empty => {
713 pending.insert((ticks, origin));
714 }
715 Entry::Occupied(mut pending) => {
716 let (bar, origin) = pending.get_mut();
717 let Some(folded) = fold(bar, &ticks) else {
718 updates.rejected += 1;
719 return;
720 };
721 *bar = folded;
722 if let Origin::Ticks(trades) | Origin::TicksAfterHeartbeat(trades) = origin {
723 *trades += 1;
724 }
725 }
726 Entry::Vacant(slot) => {
727 slot.insert((ticks, origin));
728 }
729 }
730 live.last_trade = Some(tick.ts);
731 live.last_close = Some(price);
732 live.empty = false;
733 live.forming = Some(open);
734 if let Some(id) = tick.id {
735 live.taken.insert(id);
736 }
737 updates.trades.push(tick);
738 }
739 }
740
741 fn fold_trade_bar(&mut self, tick: &Tick) -> Result<(), ()> {
743 let mut state = TradeBars {
744 forming: self.forming,
745 current: self.count_bar.clone(),
746 delta: self.delta,
747 closed: self.event_closed,
748 last: self.last_trade,
749 };
750 let bars = state.push(self.clock.timeframe().ok_or(())?, tick, self.tick_size)?;
751 self.forming = state.forming;
752 self.count_bar = state.current;
753 self.delta = state.delta;
754 self.event_closed = state.closed;
755 for (i, (bar, size)) in bars.into_iter().enumerate() {
756 for input in &self.study_inputs {
757 input.push_trade_part(tick, bar.time, size, i == 0);
758 }
759 self.updates.bars.insert(bar.time, (bar, Origin::Live));
760 }
761 Ok(())
762 }
763
764 fn sample_book(&mut self, ts: DateTime<Utc>) -> bool {
769 let Some(sampler) = &mut self.sampler else {
770 return false;
771 };
772 if (Utc::now().checked_add_signed(self.offset + TimeDelta::minutes(1)))
773 .is_none_or(|limit| ts > limit)
774 {
775 return false;
776 }
777 let (mut summed_book, mut largest_book);
780 let (book, largest) = match &sampler.config.orders {
781 Some(filter) if sampler.due(ts) => {
782 let levels = self.orders.as_ref().and_then(|queue| {
783 let summed = queue.filtered(&self.symbol, filter).ok()?;
784 Some((summed, queue.largest_levels(filter)))
785 });
786 let Some((summed, sizes)) = levels else {
787 sampler.lose();
788 return false;
789 };
790 summed_book = Book::new(self.tick_size);
791 largest_book = Book::new(self.tick_size);
792 let _ = summed_book.snapshot(0, summed);
793 let _ = largest_book.snapshot(0, sizes);
794 (&summed_book, Some(&largest_book))
795 }
796 _ => (&self.book, None),
797 };
798 if let Some(column) = sampler.sample(ts, book, largest) {
799 if let Some(history) = &mut self.updates.replay_depth {
800 history.push(column);
801 return true;
802 }
803 let columns = &mut self.updates.depth_columns;
804 if columns.len() == sampler.config.columns {
805 columns.pop_front();
806 }
807 columns.push_back(column);
808 return true;
809 }
810 false
811 }
812
813 fn sync_orders(&mut self, sequence: u64, time: DateTime<Utc>, result: Result<(), Lost>) {
814 self.orders_changed = true;
815 let levels = self
816 .orders
817 .as_ref()
818 .expect("order book")
819 .aggregate(&self.symbol, 0.0);
820 let sequence = self
821 .orders
822 .as_ref()
823 .and_then(OrderQueue::sequence)
824 .unwrap_or(sequence);
825 let derived = self.book.snapshot(sequence, levels);
826 self.depth_result(time, result.and(derived));
827 }
828
829 fn depth(&mut self, ts: DateTime<Utc>, apply: impl FnOnce(&mut Book) -> Result<(), Lost>) {
830 self.sample_book(ts);
831 let result = apply(&mut self.book);
832 self.depth_result(ts, result);
833 }
834
835 fn depth_result(&mut self, ts: DateTime<Utc>, result: Result<(), Lost>) {
836 let lost = result.is_err();
837 match result {
838 Ok(()) => self.updates.resnapshot = false,
840 Err(lost) => {
841 self.updates.rejected_depth += u64::from(lost.invalid);
842 self.updates.resnapshot |= lost.request;
843 }
844 }
845 if (Utc::now().checked_add_signed(self.offset + TimeDelta::minutes(1)))
846 .is_some_and(|limit| ts <= limit)
847 {
848 for input in &self.study_inputs {
849 match input.order_filter() {
850 None => {
851 input.book_levels(&self.clock, ts, self.book.levels().filter(|_| !lost))
852 }
853 Some(filter) => {
854 let book = (self.orders.as_ref())
855 .and_then(|queue| queue.filtered(&self.symbol, &filter).ok())
856 .filter(|_| !lost);
857 input.push_book(&self.clock, ts, book.as_ref());
858 }
859 }
860 }
861 }
862 }
863}