Skip to main content

egui_charts/data/inbox/
sink.rs

1//! The producer side of the inbox: the sink a feed pushes through and the folding of ticks, trades
2//! and books into what the chart drains.
3
4use 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
17/// Maximum undrained bar slots per live subscription, independent of raw trade retention.
18const MAX_PENDING_BARS: usize = 100_000;
19
20/// Rebuilds bars from chronological executions using the live sink's aggregation rules.
21/// Count intervals start at the first accepted trade in `trades`; retain that grouping anchor
22/// when joining history to live data. Duplicate ids are ignored. Invalid or late trades and
23/// overflowing volumes return an error. Periods without executions are omitted.
24///
25/// ```
26/// use egui_charts::data::aggregate_trades;
27/// use egui_charts::model::Tick;
28/// use chrono::Utc;
29/// let trades = vec![Tick::new(Utc::now(), 100.0, 2.0, None)];
30/// let bars = aggregate_trades(&trades, "10t".parse::<egui_charts::model::Timeframe>().unwrap()).unwrap();
31/// assert_eq!(bars[0].volume, 2.0);
32/// ```
33pub 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
40/// Rebuilds trade bars as `aggregate_trades` does, with an instrument tick size for range bars.
41/// The tick size must be positive and finite for a range interval.
42pub 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        // A finite historical batch owns its output; only live pushes need the pending limit.
63        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    /// A sink for a new subscription, whose ticks fold into bars of `clock` and wait in a store of
103    /// `trade_capacity`, and whose book keys prices by integer ticks of `tick_size`.
104    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/// Live updates for one subscription. Cheap to clone and usable from any thread; pushes never
146/// wait for the chart to catch up.
147///
148/// ```
149/// use chrono::Utc;
150/// use egui_charts::data::{Datafeed, HistoryPage, HistoryRequest, Reply, Subscription};
151/// use egui_charts::model::{OrderBook, OrderBookLevel, Tick, OrderSide};
152///
153/// struct Exchange;
154///
155/// impl Datafeed for Exchange {
156///     fn history(&self, _: HistoryRequest, reply: Reply<HistoryPage>) {
157///         reply.send(Ok(HistoryPage::default()));
158///     }
159///
160///     /// A real feed pushes from its connection's thread or task.
161///     fn subscribe(&self, subscription: Subscription) {
162///         let sink = subscription.sink;
163///         let level = |price, size| OrderBookLevel { price, size, order_cnt: None };
164///         let symbol = subscription.symbol;
165///         let book = |bids, asks| OrderBook { ts: Utc::now(), symbol: symbol.clone(), bids, asks };
166///
167///         sink.push_depth_snapshot(7, book(vec![level(99.75, 3.0)], vec![level(100.0, 2.0)]));
168///         let side = Some(OrderSide::Buy);
169///         sink.push_tick(Tick::new(Utc::now(), 100.0, 2.0, side).with_id(41_207));
170///         // The buy took the whole offer at 100.
171///         sink.push_depth_delta(8, &book(vec![], vec![level(100.0, 0.0)]));
172///     }
173/// }
174/// ```
175///
176/// Pending bars are capped at 100,000 slots independently of raw trades. Overflow discards
177/// the oldest slots and reports `FeedError::InvalidData` on the next drain; the host reloads
178/// history to recover the missing bars.
179#[derive(Clone)]
180pub struct Sink {
181    inbox: Arc<Inbox>,
182    pub(crate) id: SubscriptionId,
183}
184
185impl Sink {
186    /// Starts a new replay position on this subscription. The next chart drain clears prior
187    /// bars, trades, depth, quote and sampled history before applying subsequent pushes. Resets
188    /// trade ids and partial event bars too, so rewinding cannot double-count or retain future
189    /// state. Hosts coordinate outstanding history replies and call this on every symbol's sink
190    /// before dispatching `ReplayClock::seek`. Keep using the same subscription and sampler grid.
191    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    /// A bar of the subscribed timeframe, stamped with its open or any instant inside it:
220    /// replaces the bar it shares a slot with, appends a newer one, corrects an older one. A bar
221    /// of a day or longer is stamped inside its calendar period, as [`HistoryPage::bars`] describes. A
222    /// bar stamped too far ahead of the feed's current time is rejected, as
223    /// [`Sink::set_server_time`] describes.
224    ///
225    /// [`HistoryPage::bars`]: crate::data::HistoryPage::bars
226    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    /// A trade, folded into the bar of its slot and kept for order flow, the oldest trades making
257    /// room for new ones. Its size is added to that bar's volume, also when the bar came from a
258    /// history page. Trades that arrive before history extend only the page's newest bar, which
259    /// must count only trades the stream did not send, or the feed must hold those trades until
260    /// the page is answered. Completed page bars replace bars built from ticks. A feed that cannot
261    /// tell pushes bars instead. Trades older than the last
262    /// accepted execution or forming bar are dropped, and so is a trade whose id is among those
263    /// of the last trades the subscription folded, as many as its trade store holds. A trade
264    /// stamped too far ahead of the feed's current time is rejected, as [`Sink::set_server_time`]
265    /// describes.
266    /// Repeated ids can complete missing aggressor and resting order ids without adding volume.
267    ///
268    /// Trade-based intervals start at the first accepted execution of the subscription. Replay the
269    /// retained executions on resubscription to rebuild a partial bar. Count bars use the first
270    /// trade's time; if that would duplicate a bar time, it advances by one nanosecond. Original
271    /// execution times remain unchanged. Volume and delta fills split exactly at the threshold;
272    /// range bars need the instrument tick size and close at the first real fill reaching it.
273    /// A fill producing more than 100,000 bars is rejected atomically. A pushed trade-based bar must be complete: the next trade
274    /// starts a fresh bar. History pages should contain complete trade-based bars only.
275    pub fn push_tick(&self, tick: Tick) {
276        let system = Utc::now();
277        self.update(|live| live.tick(tick, system));
278    }
279
280    /// Advances a time-based stream through `now`, opening an empty forming candle at the
281    /// current clock boundary when there has been no execution. Empty candles have zero volume
282    /// and carry the close of the newest bar before them that is not whitespace, whether this
283    /// stream or a history page delivered it, before or after the heartbeat; their first trade
284    /// sets the real open, high and low. Count bars never advance without trades. A clock with a
285    /// session opens a candle only inside the windows the subscription selected: a heartbeat
286    /// outside them only advances the stream's time, and the first one inside the next session
287    /// opens that session's slot at the last close. Call from a trusted feed heartbeat, after
288    /// seeding a bar or execution; invalid or future clock values obey the same rule as trade
289    /// timestamps. Heartbeats also close due depth-history periods, including on count intervals.
290    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    /// The whole order book, numbered `sequence`, at most 10,000 levels per side: a deeper book is
330    /// rejected. It replaces the book; the next delta carries `sequence + 1`. Levels are keyed by
331    /// the nearest integer tick of the instrument's [`tick_size`](crate::model::Instrument::tick_size),
332    /// when it has one, so prices that differ by a rounding error are one level and the latest
333    /// size pushed for it holds. A crossed book is kept as pushed.
334    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    /// The levels that changed since the update numbered `sequence - 1`, each with its new size; a
343    /// size of 0 removes the level. The `symbol` of `changes` is ignored. Taken by reference, so a
344    /// feed can clear and refill one `OrderBook` for every delta instead of allocating its levels
345    /// per message. Changes are sorted by price and merged in one pass, with the last entry for
346    /// each price winning. A delta numbered at or below the last one applied is ignored. After a
347    /// gap, before the first snapshot, or when a side would exceed 10,000 levels, the book is
348    /// cleared and the chart calls [`Datafeed::request_depth_snapshot`] once; a snapshot pushed
349    /// before the chart sees the loss cancels the call. Keep pushing deltas while the snapshot is
350    /// fetched: those pushed while the book waits are held, the oldest dropped beyond 10,000
351    /// levels in all, and the snapshot applies the ones numbered above its own sequence.
352    ///
353    /// [`Datafeed::request_depth_snapshot`]: crate::data::Datafeed::request_depth_snapshot
354    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    /// Replaces the per-order book, at most 10,000 orders per side, listed in queue order.
363    /// The next event has `sequence + 1`; events buffered during resync above the snapshot's
364    /// sequence apply afterwards. Gaps and invalid events clear both books and ask the feed for
365    /// a new snapshot once through `Datafeed::request_depth_snapshot`. Use either order events
366    /// or aggregated deltas for a subscription; an aggregated push discards the order queues.
367    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    /// Applies one order event with consecutive sequencing. Repeated sequences are ignored;
388    /// while waiting for a snapshot at most 10,000 consecutive events are kept. Unknown cancels,
389    /// duplicate adds, invalid sizes and oversized executions lose the book. An execution's
390    /// optional trade enters the raw trade store and carries the resting order id it filled.
391    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    /// Latest best bid and ask. Bursts coalesce to the newest timestamp (last arrival on a tie).
407    /// Non-finite prices and negative or non-finite sizes are rejected without replacing the
408    /// pending quote. A crossed quote, its bid above its ask, is kept, as the top of a crossed
409    /// book (see [`OrderBook::is_crossed`]). Times obey the feed-clock rule of [`Self::set_server_time`].
410    /// The subscription supplies the instrument identity; the
411    /// sink ignores a quote whose symbol differs from its subscribed symbol.
412    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, &quote, tick);
437                    }
438                }
439                live.updates.quote = Some(quote);
440            }
441        });
442    }
443
444    /// The feed's current time, from a clock other than the times it pushes, such as its server's
445    /// heartbeat or time message: reporting the time of the trade or bar about to be pushed would
446    /// let a wrong time through. The time runs on with the system clock until the next call; until
447    /// the first, the system clock stands in.
448    ///
449    /// A bar or trade is rejected when its slot opens more than one slot after the slot of this
450    /// time and more than a minute after this time, or more than two weeks for bars of a day or
451    /// longer, which a feed stamps on their trading date. A time in the wrong unit, such as
452    /// milliseconds read as seconds, therefore cannot make every later trade late, while a system
453    /// clock a few seconds slow rejects nothing. A replay or simulation calls this before its first
454    /// push, whenever its clock jumps, and at least once per bar while its clock runs at another
455    /// speed.
456    pub fn set_server_time(&self, now: DateTime<Utc>) {
457        let offset = now - Utc::now();
458        // Not a push: the chart shows nothing new, so it is not woken.
459        if let Some(live) = self.inbox.state().subscriptions.get_mut(&self.id) {
460            live.offset = offset;
461        }
462    }
463
464    /// The connection status. The latest one wins.
465    pub fn set_status(&self, status: FeedStatus) {
466        self.update(|live| live.updates.status = Some(status));
467    }
468
469    /// An error of the live stream. The latest one wins.
470    pub fn report_error(&self, error: FeedError) {
471        self.update(|live| live.updates.error = Some(error));
472    }
473
474    /// Notifies the host that its symbol search catalog should be refreshed. Coalesces within
475    /// a frame; the chart exposes arrivals through `FeedState::symbol_catalog_revision`.
476    pub fn symbols_changed(&self) {
477        self.update(|live| live.updates.symbols_changed = true);
478    }
479
480    /// False once the subscription ended. Pushes are ignored from then on.
481    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/// Exact execution allocations shared by chart bars and standalone stream inputs.
512#[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                    // The chart rebuilt drained history; include the executions still queued.
618                    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    /// Whether the slot opening at `open` is too far ahead of the feed's current time, `system`
638    /// being the system clock's, by the rule of [`Sink::set_server_time`].
639    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        // A system clock may lag the exchange by seconds, and a bar of a day or longer is stamped
644        // on its trading date, which can be days ahead. A clock of unknown timeframe gets the
645        // longer allowance.
646        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        // The slot before `open`'s is later than now's: `open` is two or more slots ahead.
659        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    /// Folds exact thresholds before publishing any execution allocation to studies.
742    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    /// Applies a depth update stamped `ts`, sampling the book as it stood before when the update
765    /// opens a new period of the depth history. An update stamped more than a minute after the
766    /// feed's time samples nothing: it would hold the open period, and every later column, until
767    /// that time passed.
768    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        // The orders a filtered history samples, summed and the largest at each price, as they
778        // stood before this update, only when the update opens a period.
779        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            // The book holds a snapshot newer than any loss, so a pending request is moot.
839            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}