1mod pump;
9
10use super::{Chart, SeriesId};
11use crate::chart::overlays::{VolumeBubbles, VolumeBubblesConfig};
12use crate::data::inbox::Inbox;
13use crate::data::trades::Trades;
14use crate::data::{
15 Datafeed, DepthHistory, DepthRequest, FeedCapabilities, FeedError, Footprint, HistoryRequest,
16 Reply, SubscriptionId, TradeRequest,
17};
18use crate::model::{BarClock, Instrument, OrderBook, Tick, Timeframe};
19use crate::theme::ThemeExt;
20use egui::ViewportId;
21use std::sync::{Arc, Mutex, PoisonError};
22use std::time::Duration;
23
24pub(crate) const TRADE_CAPACITY: usize = 100_000;
26const FOOTPRINT_BARS: usize = 5_000;
28const FOOTPRINT_ROWS: usize = TRADE_CAPACITY / 2;
30
31const SCROLL_BACK_MARGIN: usize = 150;
33
34const MIN_SCROLL_BACK_PAGE: usize = 600;
36
37pub(crate) struct FeedLink {
39 pub(super) feed: Arc<dyn Datafeed>,
40 inbox: Arc<Inbox>,
41 pub(super) clock: BarClock,
43 initial_bars: usize,
44 pub(super) adjusted: bool,
46 wake: Arc<Mutex<Option<ViewportId>>>,
50 reload_pending: bool,
52 pub(super) paused: bool,
53 pub(super) host_paused: bool,
56 next_key: u64,
57 pending: Vec<(u64, Request, u32)>,
59 retries: Vec<(f64, Request, u32)>,
60 subscription: Option<SubscriptionId>,
61 instrument: Option<Instrument>,
62 capabilities: Option<FeedCapabilities>,
64 pub(super) trades: Trades,
65 pub(super) bubbles: Option<VolumeBubbles>,
67 pub(super) bubbles_config: VolumeBubblesConfig,
68 pub(super) footprint: Option<Footprint>,
71 pub(super) replay_footprint: Option<(chrono::DateTime<chrono::Utc>, Option<Footprint>)>,
74 order_book: Option<OrderBook>,
75 order_queue: Option<crate::data::OrderQueue>,
76 pub(super) depth_history: Option<DepthHistory>,
78 depth_complete: bool,
79 bar_cursor: Option<(
82 chrono::DateTime<chrono::Utc>,
83 Option<chrono::DateTime<chrono::Utc>>,
84 )>,
85 trade_cursor: Option<chrono::DateTime<chrono::Utc>>,
86 depth_cursor: Option<chrono::DateTime<chrono::Utc>>,
87 trades_complete: bool,
89 trades_from_start: bool,
92 pub(super) heatmap: Option<std::cell::RefCell<crate::chart::overlays::LiquidityHeatmap>>,
93 heatmap_config: crate::chart::overlays::LiquidityHeatmapConfig,
94 book_view: crate::chart::overlays::OrderBookView,
96 pub(super) book_visible: Option<bool>,
98 study_inputs: Vec<(crate::data::OrderFlowInput, u64)>,
99 time: Option<chrono::DateTime<chrono::Utc>>,
100 pub(super) paging_stopped: bool,
102}
103
104#[derive(Clone)]
105enum Request {
106 Capabilities,
107 Resolve,
108 History {
109 request: HistoryRequest,
110 scroll_back: bool,
111 },
112 Trades(TradeRequest),
113 Depth(DepthRequest),
114}
115
116impl FeedLink {
117 pub(super) fn server_time(&self) -> Option<chrono::DateTime<chrono::Utc>> {
118 self.inbox.server_time(self.subscription?)
119 }
120
121 fn reply<T: Send + 'static>(&mut self, request: Request) -> Reply<T> {
123 self.next_key += 1;
124 self.pending.push((self.next_key, request, 0));
125 self.inbox.reply(self.next_key)
126 }
127
128 fn rebuild_bubbles(&mut self) {
130 if let Some(bubbles) = self.bubbles.take() {
131 self.bubbles = Some(self.bubbles(bubbles.config));
132 }
133 if self.footprint.is_some() {
134 self.footprint = self.footprint(None);
135 }
136 }
137
138 pub(super) fn frozen(&self) -> Self {
141 Self {
142 feed: Arc::clone(&self.feed),
143 inbox: Arc::default(),
144 clock: self.clock.clone(),
145 initial_bars: self.initial_bars,
146 adjusted: self.adjusted,
147 wake: Arc::default(),
148 reload_pending: false,
149 paused: true,
150 host_paused: true,
151 next_key: 0,
152 pending: Vec::new(),
153 retries: Vec::new(),
154 subscription: None,
155 instrument: self.instrument.clone(),
156 capabilities: self.capabilities.clone(),
157 trades: Trades::new(TRADE_CAPACITY),
158 bubbles: self
159 .bubbles
160 .as_ref()
161 .map(|bubbles| self.bubbles(bubbles.config.clone())),
162 footprint: self
163 .footprint
164 .is_some()
165 .then(|| self.footprint(None))
166 .flatten(),
167 replay_footprint: self.replay_footprint.clone(),
168 order_book: self.order_book.clone(),
169 order_queue: self.order_queue.clone(),
170 depth_history: self.depth_history.clone(),
171 depth_complete: self.depth_complete,
172 bar_cursor: None,
173 trade_cursor: None,
174 depth_cursor: None,
175 trades_complete: false,
176 trades_from_start: false,
177 heatmap: self.heatmap.is_some().then(Default::default),
178 heatmap_config: self.heatmap_config,
179 book_view: self.book_view.clone(),
180 book_visible: self.book_visible,
181 bubbles_config: self.bubbles_config.clone(),
182 study_inputs: Vec::new(),
183 time: self.time,
184 paging_stopped: true,
185 }
186 }
187
188 pub(super) fn footprint(
193 &self,
194 before: Option<chrono::DateTime<chrono::Utc>>,
195 ) -> Option<Footprint> {
196 let tick_size = self.instrument.as_ref()?.tick_size?;
197 let mut flow = Footprint::new(self.clock.clone(), tick_size, 1).ok()?;
198 flow.set_budget(FOOTPRINT_BARS, FOOTPRINT_ROWS);
199 let trades = || {
200 self.trades
201 .iter()
202 .take_while(|trade| before.is_none_or(|time| trade.ts < time))
203 };
204 for trade in trades() {
205 let _ = flow.push(trade);
206 }
207 if let Some(lost) = self.trades.lost_until() {
208 flow.drop_through(lost, trades());
209 }
210 if let Some(first) = self.unconfirmed_start() {
211 flow.received_from(first);
212 }
213 Some(flow)
214 }
215
216 fn unconfirmed_start(&self) -> Option<chrono::DateTime<chrono::Utc>> {
220 if self.trades_from_start || self.trades.lost_until().is_some() {
221 return None;
222 }
223 self.trades.iter().next().map(|trade| trade.ts)
224 }
225
226 fn bubbles(&self, config: VolumeBubblesConfig) -> VolumeBubbles {
227 let tick_size = self.instrument.as_ref().and_then(|i| i.tick_size);
228 VolumeBubbles::new(config, self.clock.clone(), tick_size, self.trades.iter())
229 }
230}
231
232impl Drop for FeedLink {
233 fn drop(&mut self) {
234 *lock(&self.wake) = None;
235 if let Some(id) = self.subscription.take() {
236 self.inbox.unsubscribe(id);
237 self.feed.unsubscribe(id);
238 }
239 }
240}
241
242impl Chart {
243 pub(crate) fn attach_feed(&mut self, feed: Arc<dyn Datafeed>, initial_bars: usize) {
245 self.set_timeframe(self.timeframe.unwrap_or_default());
246 let timeframe = self.timeframe.unwrap_or_default();
247 self.feed_link = Some(FeedLink {
248 feed,
249 inbox: Arc::default(),
250 clock: BarClock::new(timeframe),
251 initial_bars,
252 adjusted: false,
253 wake: Arc::default(),
254 reload_pending: true,
255 paused: false,
256 host_paused: false,
257 next_key: 0,
258 pending: Vec::new(),
259 retries: Vec::new(),
260 subscription: None,
261 instrument: None,
262 capabilities: None,
263 trades: Trades::new(TRADE_CAPACITY),
264 bubbles: None,
265 footprint: None,
266 replay_footprint: None,
267 order_book: None,
268 order_queue: None,
269 depth_history: None,
270 depth_complete: false,
271 bar_cursor: None,
272 trade_cursor: None,
273 depth_cursor: None,
274 trades_complete: false,
275 trades_from_start: false,
276 heatmap: None,
277 heatmap_config: Default::default(),
278 book_view: Default::default(),
279 book_visible: None,
280 bubbles_config: Default::default(),
281 study_inputs: Vec::new(),
282 time: None,
283 paging_stopped: false,
284 });
285 }
286
287 pub fn feed_capabilities(&self) -> Option<&FeedCapabilities> {
291 self.feed_link.as_ref()?.capabilities.as_ref()
292 }
293
294 pub fn set_feed_paused(&mut self, paused: bool) {
300 if let Some(link) = &mut self.feed_link {
301 link.host_paused = paused;
302 }
303 let paused = paused || !self.main_series_present || self.replay.is_some();
304 if paused {
305 self.study_data.pause();
306 }
307 if let Some(link) = &mut self.feed_link
308 && link.paused != paused
309 {
310 link.paused = paused;
311 link.reload_pending = true;
312 if paused {
313 if let Some(id) = link.subscription.take() {
314 link.inbox.unsubscribe(id);
315 link.feed.unsubscribe(id);
316 }
317 link.pending.clear();
318 } else {
319 self.study_data.clear_feed();
320 }
321 }
322 }
323
324 pub fn set_symbol(&mut self, symbol: impl Into<String>) {
334 let keep_history = self.history.recording();
335 self.change_symbol(symbol.into(), keep_history);
336 }
337
338 pub(super) fn change_symbol(&mut self, symbol: String, keep_history: bool) {
340 if symbol != self.symbol {
341 self.clear_source_signals();
342 self.reload_price_targets();
343 self.set_replay_time(None);
344 }
345 if symbol != self.symbol
346 && let Some(link) = &mut self.feed_link
347 {
348 link.reload_pending = true;
349 link.instrument = None;
350 }
351 if symbol != self.symbol {
352 self.host_events
353 .push(super::ChartEvent::SymbolChanged(symbol.clone()));
354 #[cfg(feature = "drawings")]
355 self.anchor_drawing_points();
356 #[cfg(feature = "drawings")]
357 {
358 self.save_stored_drawings();
359 self.drawings.cancel_curr_drawing();
360 self.drawings.end_drag_handle();
361 self.drawings.deselect();
362 self.detach_pending_drawings();
363 let mut next = self.symbol_drawings.remove(&symbol).unwrap_or_default();
364 next.0.set_sync_enabled(self.drawings.sync_enabled);
365 next.0.registry = self.drawings.registry.clone();
366 next.0.options = self.drawings.options.clone();
367 next.0.tool_defaults = self.drawings.tool_defaults.clone();
368 next.0.set_active_tool(self.drawings.active_tool);
369 if let Some(id) = self.drawings.active_custom_tool() {
370 let _ = next.0.set_custom_tool(id);
371 }
372 next.0.stay_in_drawing_mode = self.drawings.stay_in_drawing_mode;
373 next.0.eraser_mode = self.drawings.eraser_mode;
374 next.0.curr_timeframe = self.drawings.curr_timeframe.clone();
375 std::mem::swap(&mut self.drawings, &mut next.0);
376 std::mem::swap(&mut self.marks, &mut next.1);
377 self.attach_pending_drawings();
378 if !next.0.drawings.is_empty()
379 || !next.1.is_empty()
380 || !next
381 .0
382 .pending_sync
383 .lock()
384 .unwrap_or_else(PoisonError::into_inner)
385 .is_empty()
386 {
387 self.symbol_drawings.insert(self.symbol.clone(), next);
388 }
389 }
390 #[cfg(not(feature = "drawings"))]
391 {
392 let mut next_marks = self.symbol_markers.remove(&symbol).unwrap_or_default();
393 std::mem::swap(&mut self.marks, &mut next_marks);
394 if !next_marks.is_empty() {
395 self.symbol_markers.insert(self.symbol.clone(), next_marks);
396 }
397 }
398 if !keep_history {
399 self.history.clear();
400 }
401 self.history.edits += 1;
402 self.state.price_scale_mut().reset();
403 self.state.clear_zoom_history();
404 self.pending_scroll_time = None;
405 self.legend_instrument = None;
406 self.currency_choice = None;
407 self.bar_close_time = None;
408 self.server_time_offset = None;
409 self.legend_badge = None;
410 self.legend_statuses.clear();
411 self.quote = None;
412 self.quote_action = None;
413 self.session_quote = None;
414 self.events = Default::default();
415 self.time_scale_events = Default::default();
416 self.event_click = None;
417 self.axis_hover = None;
418 self.state.price_scale_mut().set_price_precision(None);
419 self.state.price_scale_mut().set_tick_size(None);
420 }
421 self.symbol = symbol;
422 }
423
424 pub fn forget_symbol(&mut self, symbol: &str) -> bool {
430 if symbol == self.symbol {
431 return false;
432 }
433 #[cfg(feature = "drawings")]
434 {
435 if let Some(store) = &mut self.drawing_store
436 && !store.forget(symbol)
437 {
438 return false;
439 }
440 self.symbol_drawings.remove(symbol);
441 self.symbol_drawings.shrink_to_fit();
442 }
443 #[cfg(not(feature = "drawings"))]
444 {
445 self.symbol_markers.remove(symbol);
446 self.symbol_markers.shrink_to_fit();
447 }
448 true
449 }
450
451 pub fn set_timeframe(&mut self, timeframe: Timeframe) {
456 if self.timeframe != Some(timeframe) {
457 self.clear_source_signals();
458 self.host_events
459 .push(super::ChartEvent::TimeframeChanged(timeframe));
460 self.bar_close_time = None;
461 }
462 if self.timeframe != Some(timeframe)
463 && let Some(link) = &mut self.feed_link
464 {
465 link.reload_pending = true;
466 }
467 let changed = self.timeframe != Some(timeframe);
468 if changed {
469 self.pending_scroll_time = None;
470 }
471 self.timeframe = Some(timeframe);
472 for study in self.studies.indicators_mut() {
473 study.set_timeframe(timeframe);
474 }
475 #[cfg(feature = "drawings")]
476 {
477 self.drawings.curr_timeframe = timeframe.as_str().into_owned();
478 }
479 if changed {
480 self.pending_from.fill(Some(0));
481 self.next_refresh.fill(f64::NEG_INFINITY);
482 self.element_studies.dirty = true;
483 self.apply_replay_window();
484 }
485 }
486
487 pub fn set_extended_hours(&mut self, extended_hours: bool) {
491 if self.extended_hours != extended_hours {
492 self.extended_hours = extended_hours;
493 self.study_data.clear_feed();
494 if let Some(link) = &mut self.feed_link {
495 link.reload_pending = true;
496 }
497 }
498 }
499
500 pub fn set_conversion(&mut self, conversion: Option<String>) {
509 if self.conversion != conversion {
510 self.conversion = conversion;
511 self.study_data.clear_feed();
512 if let Some(link) = &mut self.feed_link {
513 link.reload_pending = true;
514 link.instrument = None;
516 }
517 }
518 }
519
520 pub fn conversion(&self) -> Option<&str> {
522 self.conversion.as_deref()
523 }
524
525 pub fn set_dividend_adjusted(&mut self, adjusted: bool) {
530 if let Some(link) = &mut self.feed_link
531 && link.adjusted != adjusted
532 {
533 link.adjusted = adjusted;
534 link.reload_pending = true;
535 self.study_data.clear_feed();
536 }
537 }
538
539 pub fn dividend_adjusted(&self) -> bool {
541 self.feed_link.as_ref().is_some_and(|link| link.adjusted)
542 }
543
544 pub fn reload_feed(&mut self) {
547 self.study_data.clear_feed();
548 if let Some(link) = &mut self.feed_link {
549 link.reload_pending = true;
550 }
551 }
552
553 pub fn extended_hours(&self) -> bool {
555 self.extended_hours
556 }
557
558 pub fn symbol(&self) -> &str {
560 &self.symbol
561 }
562
563 pub fn timeframe(&self) -> Option<Timeframe> {
566 self.timeframe
567 }
568
569 pub fn instrument(&self) -> Option<&Instrument> {
571 self.feed_link
572 .as_ref()
573 .and_then(|link| link.instrument.as_ref())
574 .or(self.legend_instrument.as_ref())
575 }
576
577 pub fn trades(&self, series: SeriesId) -> impl Iterator<Item = &Tick> {
580 let link = self.feed_link.as_ref().filter(|_| series == SeriesId::MAIN);
581 link.into_iter().flat_map(|link| link.trades.iter())
582 }
583
584 pub fn combined_trades(&self, series: SeriesId) -> Vec<crate::data::CombinedTrade> {
588 crate::data::combine_trades(self.trades(series))
589 }
590
591 pub fn set_volume_bubbles(&mut self, config: Option<VolumeBubblesConfig>) {
598 if let Some(link) = &mut self.feed_link {
599 if let Some(config) = &config {
600 link.bubbles_config = config.clone();
601 }
602 link.bubbles = config.map(|config| link.bubbles(config));
603 }
604 }
605
606 pub fn order_book(&self, series: SeriesId) -> Option<&OrderBook> {
609 let link = self
610 .feed_link
611 .as_ref()
612 .filter(|_| series == SeriesId::MAIN && self.replay.is_none())?;
613 link.order_book.as_ref()
614 }
615
616 pub fn order_queue(&self, series: SeriesId) -> Option<&crate::data::OrderQueue> {
622 self.feed_link
623 .as_ref()
624 .filter(|_| series == SeriesId::MAIN && self.replay.is_none())?
625 .order_queue
626 .as_ref()
627 }
628
629 pub fn set_order_book_view(&mut self, view: crate::chart::overlays::OrderBookView) {
640 if let Some(link) = &mut self.feed_link {
641 link.book_view = view;
642 }
643 }
644
645 pub fn set_order_book_visible(&mut self, visible: bool) {
649 if let Some(link) = &mut self.feed_link {
650 link.book_visible = Some(visible);
651 }
652 }
653
654 pub fn order_book_view(&self) -> crate::chart::overlays::OrderBookView {
657 self.feed_link
658 .as_ref()
659 .map(|link| link.book_view.clone())
660 .unwrap_or_default()
661 }
662
663 pub fn set_depth_history(&mut self, history: Option<DepthHistory>) {
673 if let Some(link) = &mut self.feed_link {
674 if let Some(id) = link.subscription {
675 let config = history.as_ref().map(|history| history.config().clone());
676 link.inbox.sample_depth(id, config);
677 }
678 link.depth_history = history;
679 link.depth_complete = false;
680 link.depth_cursor = None;
681 link.pending
682 .retain(|(_, r, _)| !matches!(r, Request::Depth(_)));
683 link.retries
684 .retain(|(_, r, _)| !matches!(r, Request::Depth(_)));
685 }
686 }
687
688 pub fn set_liquidity_heatmap(&mut self, enabled: bool) {
694 if enabled
695 && self
696 .feed_link
697 .as_ref()
698 .is_none_or(|link| link.depth_history.is_none())
699 {
700 self.set_depth_history(Some(
701 DepthHistory::new(Default::default()).expect("default depth history"),
702 ));
703 }
704 if let Some(link) = &mut self.feed_link {
705 if enabled {
706 link.heatmap.get_or_insert_with(Default::default);
707 } else {
708 link.heatmap = None;
709 }
710 }
711 }
712
713 pub fn set_liquidity_heatmap_config(
718 &mut self,
719 config: crate::chart::overlays::LiquidityHeatmapConfig,
720 ) {
721 if let Some(link) = &mut self.feed_link {
722 link.heatmap_config = config;
723 }
724 }
725
726 pub fn liquidity_heatmap_config(&self) -> crate::chart::overlays::LiquidityHeatmapConfig {
728 self.feed_link
729 .as_ref()
730 .map(|link| link.heatmap_config)
731 .unwrap_or_default()
732 }
733
734 pub(super) fn paint_liquidity(
735 &self,
736 painter: &egui::Painter,
737 mapping: &crate::chart::coords::ChartMapping,
738 ) {
739 if self.replay.is_some() {
740 return;
741 }
742 let Some(link) = &self.feed_link else { return };
743 let Some(tick) = self.instrument().and_then(|i| i.tick_size) else {
744 return;
745 };
746 let mut unavailable = None;
747 if let (Some(heatmap), Some(history)) = (&link.heatmap, &link.depth_history) {
748 let mut heatmap = heatmap.borrow_mut();
749 heatmap.config = link.heatmap_config;
750 heatmap.render(
751 painter,
752 mapping,
753 history,
754 tick * history.config().ticks_per_row as f64,
755 |time| self.time_to_index(time).and_then(|i| self.index_to_x(i)),
756 &link.book_view,
757 [self.config.bullish_color, self.config.bearish_color],
758 );
759 unavailable = (history.config().orders.is_some() && link.order_queue.is_none())
760 .then_some(crate::data::OrderFilterUnavailable::AggregateDepth);
761 }
762 if link.book_visible.unwrap_or(link.heatmap.is_some()) {
763 unavailable = crate::chart::overlays::render_book_profile(
764 painter,
765 mapping,
766 link.order_book.as_ref(),
767 link.order_queue.as_ref(),
768 tick,
769 &link.book_view,
770 [self.config.bullish_color, self.config.bearish_color],
771 )
772 .err()
773 .or(unavailable);
774 }
775 if let Some(reason) = unavailable {
776 painter.text(
777 mapping.rect.right_top() + egui::vec2(-8.0, 8.0),
778 egui::Align2::RIGHT_TOP,
779 crate::localization::format_text(
780 painter.ctx(),
781 "Order-level depth unavailable: {0}",
782 &[(
783 "0",
784 crate::localization::text(painter.ctx(), reason.to_string()),
785 )],
786 ),
787 egui::FontId::proportional(11.0),
788 painter.ctx().active_theme().ui.warning,
789 );
790 }
791 }
792
793 pub fn depth_history(&self, series: SeriesId) -> Option<&DepthHistory> {
796 let link = self
797 .feed_link
798 .as_ref()
799 .filter(|_| series == SeriesId::MAIN && self.replay.is_none())?;
800 link.depth_history.as_ref()
801 }
802
803 pub fn bar_clock(&self) -> BarClock {
805 let clock = BarClock::new(self.timeframe.unwrap_or_default());
806 let clock = match self.display_timezone() {
807 crate::scales::TimezoneMode::Local => clock.with_local_timezone(),
808 zone => clock.with_timezone(zone.to_tz().unwrap_or(crate::model::TimezoneId::utc())),
809 };
810 match self.instrument().and_then(|instrument| {
811 Some((
812 instrument
813 .time_zone
814 .clone()
815 .unwrap_or_else(crate::model::TimezoneId::utc),
816 instrument.session.clone()?,
817 ))
818 }) {
819 Some((zone, session)) => clock.with_session(zone, session, self.extended_hours()),
820 None => clock,
821 }
822 }
823
824 pub(super) fn clock(&self) -> BarClock {
826 self.feed_link
827 .as_ref()
828 .filter(|link| !link.paused)
829 .map_or_else(BarClock::exact, |link| link.clock.clone())
830 }
831}
832
833pub(super) fn resolve(
835 feed: &dyn Datafeed,
836 symbol: &str,
837 conversion: Option<&str>,
838 reply: Reply<Instrument>,
839) {
840 match conversion {
841 Some(conversion) => feed.resolve_converted(symbol, conversion, reply),
842 None => feed.resolve(symbol, reply),
843 }
844}
845
846pub(super) fn retry_delay(failures: u32, error: &FeedError) -> Duration {
849 let backoff = Duration::from_secs(1 << failures.min(6)).min(Duration::from_secs(60));
850 match error {
851 FeedError::RateLimited {
852 retry_after: Some(after),
853 } => backoff.max(*after),
854 _ => backoff,
855 }
856}
857
858fn lock(wake: &Mutex<Option<ViewportId>>) -> std::sync::MutexGuard<'_, Option<ViewportId>> {
860 wake.lock().unwrap_or_else(PoisonError::into_inner)
861}
862
863#[cfg(test)]
864mod tests;