1use super::series::PriceSeries;
4use super::{Chart, ChartLayout, SeriesId};
5use crate::data::ReplayDriver;
6use crate::data::merge::{Origin, fold, merge};
7use crate::model::{Bar, BarClock, Timeframe};
8use chrono::{DateTime, Utc};
9use std::collections::{BTreeMap, HashMap};
10use std::sync::Arc;
11
12pub(super) struct ReplayWindow {
13 pub(super) time: DateTime<Utc>,
14 pub(super) bars: HashMap<SeriesId, Vec<Bar>>,
15 source: Option<(Vec<Bar>, BarClock)>,
16 feed_paused: bool,
18 pub quote: Option<crate::model::Level1Quote>,
19 pub session_quote: Option<crate::model::quote::SessionQuote>,
20}
21
22pub(super) struct MarketReplay {
23 driver: Arc<ReplayDriver>,
24 feed: Option<super::feed::FeedLink>,
25 window: ReplayWindow,
26}
27
28impl ReplayWindow {
29 pub(super) fn remove_oldest_bars(
30 &mut self,
31 id: SeriesId,
32 count: usize,
33 clock: &BarClock,
34 ) -> Vec<Bar> {
35 let bars = self.bars.entry(id).or_default();
36 let count = count.min(bars.len());
37 if id == SeriesId::MAIN
38 && count > 0
39 && let Some((source, _)) = &mut self.source
40 {
41 let through = clock.bar_open(bars[count - 1].time);
42 let end = source.partition_point(|bar| clock.bar_open(bar.time) <= through);
43 source.drain(..end);
44 super::series::shrink_bars(source);
45 }
46 let removed = bars.drain(..count).collect();
47 super::series::shrink_bars(bars);
48 removed
49 }
50}
51
52impl Chart {
53 pub fn replay_time(&self) -> Option<DateTime<Utc>> {
55 self.market_replay
56 .as_ref()
57 .map(|replay| {
58 replay
59 .driver
60 .time()
61 .checked_add_signed(chrono::TimeDelta::nanoseconds(1))
62 .unwrap_or(chrono::DateTime::<Utc>::MAX_UTC)
63 })
64 .or_else(|| self.replay.as_ref().map(|replay| replay.time))
65 }
66
67 pub fn replay_driver(&self) -> Option<&Arc<ReplayDriver>> {
69 self.market_replay.as_ref().map(|replay| &replay.driver)
70 }
71
72 pub fn set_market_replay(&mut self, driver: Option<Arc<ReplayDriver>>) {
79 if let Some(replay) = self.market_replay.take() {
80 self.feed_link = replay.feed;
81 self.replay = Some(replay.window);
82 self.set_replay_time(None);
83 }
84 let Some(driver) = driver else { return };
85 let history = self
86 .feed_link
87 .as_ref()
88 .and_then(|link| link.depth_history.as_ref())
89 .map(|history| {
90 crate::data::DepthHistory::new(history.config().clone())
91 .expect("existing depth history configuration")
92 });
93 let view = self.order_book_view();
94 let heatmap_config = self.liquidity_heatmap_config();
95 self.set_replay_time(Some(driver.time()));
96 let window = self.replay.take().expect("replay window");
97 let feed = self.feed_link.take();
98 self.attach_feed(driver.clone(), 1);
99 self.set_liquidity_heatmap_config(heatmap_config);
100 if let (Some(live), Some(replay)) = (&feed, &mut self.feed_link) {
101 replay.heatmap = live.heatmap.as_ref().map(|_| Default::default());
102 replay.book_visible = live.book_visible;
103 replay.bubbles_config = live.bubbles_config.clone();
104 }
105 self.set_volume_bubbles(
106 feed.as_ref()
107 .and_then(|link| link.bubbles.as_ref())
108 .map(|bubbles| bubbles.config.clone()),
109 );
110 self.set_depth_history(history);
111 self.set_order_book_view(view);
112 self.replace_main_data(Vec::new());
113 self.market_replay = Some(MarketReplay {
114 driver,
115 feed,
116 window,
117 });
118 }
119
120 pub fn set_replay_time(&mut self, time: Option<DateTime<Utc>>) {
135 if let Some(replay) = &self.market_replay {
136 if let Some(time) = time {
137 let inclusive = time
138 .checked_sub_signed(chrono::TimeDelta::nanoseconds(1))
139 .unwrap_or(DateTime::<Utc>::MIN_UTC);
140 if replay.driver.time() != inclusive {
141 replay.driver.seek(inclusive);
142 }
143 } else {
144 self.set_market_replay(None);
145 }
146 return;
147 }
148 let Some(time) = time else {
149 if let Some(replay) = self.replay.take() {
150 for (id, bars) in replay.bars {
151 if id == SeriesId::MAIN {
152 self.replace_main_data(bars);
153 } else if let Some(item) = self.price_series.iter_mut().find(|s| s.id == id) {
154 item.bars = bars;
155 item.revision = crate::model::next_revision();
156 }
157 }
158 self.quote = replay.quote;
159 self.session_quote = replay.session_quote;
160 self.set_feed_paused(replay.feed_paused);
161 self.invalidate_study_data();
162 self.replay_order_flow(replay.time);
163 }
164 return;
165 };
166 let previous = self.replay.as_ref().map(|replay| replay.time);
167 let rewind = self.replay.as_ref().is_none_or(|replay| time < replay.time);
168 if let Some(replay) = &mut self.replay {
169 if replay.time == time {
170 return;
171 }
172 replay.time = time;
173 } else {
174 let mut bars: HashMap<_, _> = self
175 .price_series
176 .iter()
177 .map(|s| (s.id, s.bars.clone()))
178 .collect();
179 bars.insert(SeriesId::MAIN, self.state.data().bars.clone());
180 let feed_paused = self.feed_link.as_ref().is_some_and(|link| link.host_paused);
181 self.replay = Some(ReplayWindow {
182 time,
183 bars,
184 source: None,
185 feed_paused,
186 quote: self.quote.take(),
187 session_quote: self.session_quote.take(),
188 });
189 self.set_feed_paused(true);
190 }
191 self.apply_replay_window();
192 self.replay_order_flow(previous.map_or(time, |previous| previous.min(time)));
193 self.invalidate_study_data();
195 if rewind {
196 for study in self.studies.indicators_mut() {
198 study.update(&[], 0);
199 }
200 self.pending_from.fill(Some(0));
201 for study in self.element_studies.studies.indicators_mut() {
202 study.update(&[], 0);
203 }
204 self.element_studies.pending_from.fill(Some(0));
205 }
206 }
207
208 pub fn set_replay_source(&mut self, bars: Vec<Bar>, clock: BarClock) -> bool {
226 if self.replay.is_none() || self.timeframe.is_none() {
227 return false;
228 }
229 let (bars, rejected) = super::series::clean(bars, &clock);
230 if rejected != 0 || fold_source(&bars, &clock, &self.bar_clock()).is_none() {
231 return false;
232 }
233 if let Some(replay) = &mut self.replay {
234 replay.source = (!bars.is_empty()).then_some((bars, clock));
235 }
236 self.apply_replay_window();
237 true
238 }
239
240 fn replay_order_flow(&mut self, since: DateTime<Utc>) {
244 let time = self.replay_time();
245 let link = self.feed_link.as_ref();
246 let clock = link.map_or_else(|| self.bar_clock(), |link| link.clock.clone());
247 let trades = link.map(|link| &link.trades);
248 let open = clock.bar_open(since);
249 let from = (self.state.data().bars).partition_point(|bar| clock.bar_open(bar.time) < open);
250 for (at, study) in self.studies.indicators().iter().enumerate() {
251 let inputs = study.order_flow_inputs();
252 if inputs.is_empty() {
253 continue;
254 }
255 for input in inputs {
256 input.set_replay(&clock, time, trades);
257 }
258 self.pending_from[at] = Some(self.pending_from[at].map_or(from, |old| old.min(from)));
259 }
260 }
261
262 pub(super) fn apply_replay_window(&mut self) {
263 let Some(mut replay) = self.replay.take() else {
264 return;
265 };
266 let clock = self
267 .timeframe
268 .map_or_else(BarClock::exact, |_| self.bar_clock());
269 for (&id, &limit) in &self.bar_limits {
270 let count = replay
271 .bars
272 .get(&id)
273 .map_or(0, |bars| super::series::evicted_bars(bars.len(), limit));
274 if count > 0 {
275 replay.remove_oldest_bars(id, count, &clock);
276 if id == SeriesId::MAIN {
277 self.feed.history_complete = false;
278 }
279 }
280 }
281 if let Some(bars) = replay.bars.get(&SeriesId::MAIN) {
282 let mut visible = completed(bars, &clock, replay.time).to_vec();
283 if let Some((source, source_clock)) = &replay.source
284 && let Some(first) = source.first()
285 && let Some(last) = source.last()
286 && let Some(partial) = fold_source(
287 completed(source, source_clock, replay.time),
288 source_clock,
289 &clock,
290 )
291 {
292 let start = clock.bar_open(first.time);
293 let end = clock.bar_open(last.time);
294 visible.retain(|bar| {
295 let slot = clock.bar_open(bar.time);
296 slot < start || slot > end
297 });
298 merge(
299 &mut visible,
300 &mut BTreeMap::new(),
301 &clock,
302 partial.into_iter().map(|bar| (bar, Origin::Live)).collect(),
303 );
304 }
305 self.replace_main_data(visible);
306 }
307 for item in &mut self.price_series {
308 if let Some(bars) = replay.bars.get(&item.id) {
309 item.bars = completed(bars, &clock, replay.time).to_vec();
310 item.revision = crate::model::next_revision();
311 }
312 }
313 self.replay = Some(replay);
314 }
315
316 pub(super) fn replay_set_data(&mut self, id: SeriesId, bars: Vec<Bar>) {
317 let clock = self.clock();
318 let Some(replay) = &mut self.replay else {
319 return;
320 };
321 let (bars, rejected) = super::series::clean(bars, &clock);
322 replay.bars.insert(id, bars);
323 self.count_replay_rejections(id, rejected);
324 self.apply_replay_window();
325 }
326
327 pub(super) fn replay_update(&mut self, id: SeriesId, bar: Bar) -> bool {
329 let clock = self.clock();
330 let Some(replay) = &mut self.replay else {
331 return false;
332 };
333 let merged = merge(
334 replay.bars.entry(id).or_default(),
335 &mut BTreeMap::new(),
336 &clock,
337 vec![(bar, Origin::Live)],
338 );
339 self.count_replay_rejections(id, merged.rejected);
340 self.apply_replay_window();
341 merged.first_changed.is_some()
342 }
343
344 pub(super) fn replay_pop(&mut self, id: SeriesId, count: usize) -> Vec<Bar> {
346 let Some(replay) = &mut self.replay else {
347 return Vec::new();
348 };
349 let bars = replay.bars.entry(id).or_default();
350 let removed = bars.split_off(bars.len().saturating_sub(count));
351 self.apply_replay_window();
352 removed
353 }
354
355 pub(super) fn replay_release(&mut self, series: &mut PriceSeries) {
358 if let Some(bars) = self
359 .replay
360 .as_mut()
361 .and_then(|replay| replay.bars.remove(&series.id))
362 {
363 series.bars = bars;
364 }
365 }
366
367 pub(super) fn replay_admit(&mut self, series: &mut PriceSeries) {
369 let clock = self
370 .timeframe
371 .map_or_else(BarClock::exact, |_| self.bar_clock());
372 if let Some(replay) = &mut self.replay {
373 let bars = std::mem::take(&mut series.bars);
374 series.bars = completed(&bars, &clock, replay.time).to_vec();
375 replay.bars.insert(series.id, bars);
376 }
377 }
378
379 fn count_replay_rejections(&mut self, id: SeriesId, rejected: u64) {
380 if id == SeriesId::MAIN {
381 self.feed.rejected_bars += rejected;
382 } else if let Some(item) = self.price_series.iter_mut().find(|s| s.id == id) {
383 item.feed.rejected_bars += rejected;
384 }
385 }
386}
387
388pub(super) fn completed<'a>(bars: &'a [Bar], clock: &BarClock, time: DateTime<Utc>) -> &'a [Bar] {
389 let end = bars.partition_point(|bar| {
390 clock
391 .next_open(clock.bar_open(bar.time))
392 .map_or(bar.time < time, |end| end <= time)
393 });
394 &bars[..end]
395}
396
397fn fold_source(bars: &[Bar], source: &BarClock, chart: &BarClock) -> Option<Vec<Bar>> {
404 let instants = matches!(source.timeframe(), Some(Timeframe::Ticks(count)) if count.get() == 1)
405 && chart
406 .timeframe()
407 .is_some_and(|timeframe| !timeframe.is_trade_based());
408 if !instants {
409 return crate::data::resample(bars, source.clone(), chart.clone());
410 }
411 let mut folded = Vec::new();
412 let mut rest = bars;
413 while let Some(first) = rest.first() {
414 let slot = chart.bar_open(first.time);
415 let end = rest
417 .partition_point(|bar| chart.bar_open(bar.time) <= slot)
418 .max(1);
419 let mut bar = Bar {
421 time: slot,
422 colors: None,
423 payload: None,
424 ..first.clone()
425 };
426 for trade in &rest[1..end] {
427 bar = fold(&bar, trade)?;
428 }
429 folded.push(bar);
430 rest = &rest[end..];
431 }
432 Some(folded)
435}
436
437impl ChartLayout {
438 pub fn set_replay_time(
442 &self,
443 charts: &mut [&mut Chart],
444 source: usize,
445 time: Option<DateTime<Utc>>,
446 ) {
447 if source >= charts.len().min(16) {
448 return;
449 }
450 for (index, chart) in charts.iter_mut().take(16).enumerate() {
451 if self.sync_group(index) == self.sync_group(source) {
452 chart.set_replay_time(time);
453 }
454 }
455 }
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461 use crate::model::{BarData, Timeframe};
462
463 fn bar(minute: i64, values: [f64; 5]) -> Bar {
464 let [open, high, low, close, volume] = values;
465 Bar::new(time(minute), open, high, low, close, volume)
466 }
467
468 fn time(minute: i64) -> DateTime<Utc> {
469 DateTime::from_timestamp(1_704_067_200 + minute * 60, 0).unwrap()
470 }
471
472 #[test]
475 fn replay_windows_every_series_and_restores_retained_history() {
476 let minutes = vec![
477 bar(0, [10.0, 12.0, 9.0, 11.0, 2.0]),
478 bar(1, [11.0, 15.0, 10.0, 14.0, 3.0]),
479 bar(2, [14.0, 16.0, 8.0, 9.0, 4.0]),
480 bar(3, [9.0, 13.0, 7.0, 12.0, 5.0]),
481 bar(4, [12.0, 100.0, 1.0, 90.0, 6.0]),
482 ];
483 let coarse = vec![bar(0, [10.0, 100.0, 1.0, 90.0, 20.0])];
484 let mut a = Chart::new(BarData::from_bars(coarse.clone()));
485 a.set_timeframe(Timeframe::Min5);
486 let comparison = a.add_series(Default::default());
487 a.set_data(comparison, coarse.clone());
488 let mut b = Chart::new(BarData::from_bars(minutes.clone()));
489 b.set_timeframe(Timeframe::Min1);
490 let mut isolated = Chart::new(BarData::from_bars(minutes.clone()));
491 isolated.set_timeframe(Timeframe::Min1);
492 let mut layout = ChartLayout::default();
493 layout.set_sync_group(2, Some("other".into()));
494 layout.set_replay_time(&mut [&mut a, &mut b, &mut isolated], 0, Some(time(2)));
495 assert!(
496 a.bars(SeriesId::MAIN).is_empty(),
497 "unfinished coarse bar must be hidden"
498 );
499 assert!(a.bars(comparison).is_empty());
500 assert_eq!(b.bars(SeriesId::MAIN), &minutes[..2]);
501 assert_eq!(isolated.bars(SeriesId::MAIN), minutes);
502 assert!(a.set_replay_source(minutes.clone(), BarClock::new(Timeframe::Min1)));
503 assert_eq!(
505 a.bars(SeriesId::MAIN),
506 [bar(0, [10.0, 15.0, 9.0, 14.0, 5.0])]
507 );
508 let prior = a.bars(SeriesId::MAIN).to_vec();
509 assert!(!a.set_replay_source(minutes.clone(), BarClock::new(Timeframe::Min15)));
510 assert_eq!(a.bars(SeriesId::MAIN), prior);
511 let mut invalid = minutes.clone();
512 invalid[0].volume = -1.0;
513 assert!(!a.set_replay_source(invalid, BarClock::new(Timeframe::Min1)));
514 for (cutoff, expected) in [
515 (4, bar(0, [10.0, 16.0, 7.0, 12.0, 14.0])),
516 (1, bar(0, [10.0, 12.0, 9.0, 11.0, 2.0])),
517 (5, coarse[0].clone()),
518 ] {
519 a.set_replay_time(Some(time(cutoff)));
520 assert_eq!(a.bars(SeriesId::MAIN), [expected]);
521 }
522 a.set_replay_time(Some(time(2)));
523 let added = a.add_series(Default::default());
524 a.set_data(added, coarse.clone());
525 assert!(a.bars(added).is_empty());
526 a.remove_series(comparison);
527 let correction = bar(5, [90.0, 95.0, 80.0, 85.0, 7.0]);
528 a.update(SeriesId::MAIN, correction.clone());
529 a.update(added, correction.clone());
530 assert_eq!(a.bars(SeriesId::MAIN), prior);
531 a.set_replay_time(Some(time(10)));
532 assert_eq!(
533 a.bars(SeriesId::MAIN),
534 [coarse[0].clone(), correction.clone()],
535 "completed chart history beyond the finer source must remain available"
536 );
537 a.set_replay_time(None);
538 assert_eq!(
539 a.bars(SeriesId::MAIN),
540 [coarse[0].clone(), correction.clone()]
541 );
542 assert_eq!(a.bars(added), [coarse[0].clone(), correction]);
543 assert!(
544 a.bars(comparison).is_empty(),
545 "exit cannot resurrect a removed series"
546 );
547 b.set_timeframe(Timeframe::Min5);
548 assert!(
549 b.bars(SeriesId::MAIN).is_empty(),
550 "a paused cutoff must follow interval changes"
551 );
552 b.set_timeframe(Timeframe::Min1);
553 assert_eq!(b.bars(SeriesId::MAIN), &minutes[..2]);
554 b.set_data(SeriesId::MAIN, minutes[..3].to_vec());
555 assert_eq!(b.bars(SeriesId::MAIN), &minutes[..2]);
556 b.set_replay_time(None);
557 assert_eq!(b.bars(SeriesId::MAIN), &minutes[..3]);
558
559 for limit in [false, true] {
560 let mut chart = Chart::new(BarData::from_bars(a.bars(SeriesId::MAIN).to_vec()));
561 chart.set_timeframe(Timeframe::Min5);
562 chart.set_replay_time(Some(time(2)));
563 assert!(chart.set_replay_source(minutes.clone(), BarClock::new(Timeframe::Min1)));
564 assert_eq!(chart.bars(SeriesId::MAIN).len(), 1);
565 if limit {
566 chart.set_bar_limit(SeriesId::MAIN, std::num::NonZeroUsize::new(1));
567 } else {
568 assert_eq!(chart.remove_oldest_bars(SeriesId::MAIN, 1), coarse);
569 }
570 assert!(
571 chart.bars(SeriesId::MAIN).is_empty(),
572 "finer replay source must not rebuild evicted bars; limit={limit}"
573 );
574 chart.set_replay_time(Some(time(10)));
575 assert_eq!(chart.bars(SeriesId::MAIN), &a.bars(SeriesId::MAIN)[1..]);
576 }
577 }
578
579 #[test]
583 fn undoing_a_series_removal_across_replay_keeps_its_history_and_the_cutoff() {
584 let minutes: Vec<_> = (0..5).map(|i| bar(i, [10.0; 5])).collect();
585 for removed_in_replay in [true, false] {
586 let mut chart = Chart::new(BarData::from_bars(minutes.clone()));
587 chart.set_timeframe(Timeframe::Min1);
588 let id = chart.add_series(Default::default());
589 chart.set_data(id, minutes.clone());
590 if removed_in_replay {
591 chart.set_replay_time(Some(time(2)));
592 }
593 assert!(chart.user_action(|chart| chart.remove_series(id)));
594 if removed_in_replay {
595 chart.set_replay_time(None);
596 assert!(chart.undo());
597 } else {
598 chart.set_replay_time(Some(time(2)));
599 assert!(chart.undo());
600 assert_eq!(chart.bars(id), &minutes[..2], "undone during replay");
601 chart.set_replay_time(None);
602 }
603 assert_eq!(
604 chart.bars(id),
605 minutes,
606 "removed_in_replay={removed_in_replay}"
607 );
608 }
609 }
610
611 #[cfg(feature = "studies")]
612 #[test]
613 fn rewind_clears_future_study_values_before_rendering() {
614 let mut chart = Chart::new(BarData::from_bars(
615 (0..8).map(|i| bar(i, [10.0 + i as f64; 5])).collect(),
616 ));
617 chart.set_timeframe(Timeframe::Min1);
618 chart.add_indicator(Box::new(crate::studies::SMA::new(2)));
619 chart.add_indicator(Box::new(crate::studies::EMA::new(2)));
620 chart.set_replay_time(Some(time(4)));
621 assert!(
622 chart
623 .indicators()
624 .all(|(_, study)| study.values().is_empty())
625 );
626 chart.refresh_studies(1.0);
627 chart.refresh_studies(2.0);
628 assert_eq!(
629 chart.indicators().next().unwrap().1.values().line(0)[3],
630 12.5
631 );
632 chart.set_replay_time(Some(time(2)));
633 assert!(
634 chart
635 .indicators()
636 .all(|(_, study)| study.values().is_empty())
637 );
638 chart.refresh_studies(3.0);
639 chart.refresh_studies(4.0);
640 assert_eq!(
641 chart.indicators().next().unwrap().1.values().line(0)[1],
642 10.5
643 );
644 }
645
646 #[cfg(feature = "ui")]
652 #[test]
653 fn replay_advances_trade_by_trade() {
654 use crate::model::Tick;
655 use crate::ui::replay::{ReplayAction, ReplayController};
656 use chrono::TimeDelta;
657
658 let minute_0 = bar(0, [10.0, 12.0, 9.0, 11.0, 5.0]);
659 let mut chart = Chart::new(BarData::from_bars(vec![
660 minute_0.clone(),
661 bar(1, [11.0, 14.0, 10.0, 13.0, 6.0]),
662 ]));
663 chart.set_timeframe(Timeframe::Min1);
664 let at = |ms| time(1) + TimeDelta::milliseconds(ms);
665 let trades = [
666 Tick::new(at(0), 11.0, 1.0, None),
667 Tick::new(at(0), 13.0, 2.0, None),
668 Tick::new(at(500), 10.0, 1.0, None),
669 Tick::new(at(60_250), 12.0, 3.0, None),
670 ];
671 let one_trade = BarClock::new("1t".parse().unwrap());
672 let source = crate::data::aggregate_trades(&trades, one_trade.clone()).unwrap();
673 let mut cursor = ReplayController::new(source.clone(), Vec::new());
674 chart.set_replay_time(cursor.cutoff(&one_trade));
675 assert!(chart.set_replay_source(source, one_trade.clone()));
676 let steps = [
677 (
679 ReplayAction::None,
680 vec![bar(1, [11.0, 11.0, 11.0, 11.0, 1.0])],
681 ),
682 (
684 ReplayAction::StepForward,
685 vec![bar(1, [11.0, 13.0, 11.0, 13.0, 3.0])],
686 ),
687 (
688 ReplayAction::StepForward,
689 vec![bar(1, [11.0, 13.0, 10.0, 10.0, 4.0])],
690 ),
691 (
693 ReplayAction::StepForward,
694 vec![
695 bar(1, [11.0, 13.0, 10.0, 10.0, 4.0]),
696 bar(2, [12.0, 12.0, 12.0, 12.0, 3.0]),
697 ],
698 ),
699 (
700 ReplayAction::JumpToBar(1),
701 vec![bar(1, [11.0, 13.0, 11.0, 13.0, 3.0])],
702 ),
703 ];
704 for (action, unfinished) in steps {
705 cursor.handle_action(action.clone());
706 chart.set_replay_time(cursor.cutoff(&one_trade));
707 let mut expected = vec![minute_0.clone()];
708 expected.extend(unfinished);
709 assert_eq!(chart.bars(SeriesId::MAIN), expected, "{action:?}");
710 }
711 let shown = chart.bars(SeriesId::MAIN).to_vec();
712 let two_trades = BarClock::new("2t".parse().unwrap());
713 let pairs = crate::data::aggregate_trades(&trades, two_trades.clone()).unwrap();
714 assert!(!chart.set_replay_source(pairs, two_trades));
715 assert_eq!(chart.bars(SeriesId::MAIN), shown);
716 }
717
718 #[test]
719 fn market_replay_rebuilds_chart_state_and_restores_the_live_feed() {
720 use crate::data::{MarketEvent, ReplayDriver, ReplayEvent};
721 use crate::model::{Instrument, Tick};
722 let mut events: Vec<_> = [(0, 10.0), (1, 12.0), (2, 99.0)]
723 .into_iter()
724 .enumerate()
725 .map(|(sequence, (minute, price))| {
726 ReplayEvent::new(
727 "A",
728 time(minute),
729 sequence as u64,
730 MarketEvent::Trade(Tick::new(time(minute), price, 1.0, None)),
731 )
732 })
733 .collect();
734 events.push(ReplayEvent::new(
735 "A",
736 time(0),
737 99,
738 MarketEvent::DepthSnapshot {
739 sequence: 0,
740 book: crate::model::OrderBook {
741 ts: time(0),
742 symbol: "A".into(),
743 bids: vec![crate::model::OrderBookLevel {
744 price: 9.0,
745 size: 5.0,
746 order_cnt: None,
747 }],
748 asks: Vec::new(),
749 },
750 },
751 ));
752 let load = || {
753 Arc::new(
754 ReplayDriver::new(
755 events.clone(),
756 vec![Instrument::new("A").with_tick_size(1.0)],
757 )
758 .unwrap(),
759 )
760 };
761 let live = load();
762 live.seek(time(2));
763 let mut chart = super::super::ChartBuilder::new()
764 .with_feed(live)
765 .with_symbol("A")
766 .with_timeframe(Timeframe::Min1)
767 .build();
768 let ctx = egui::Context::default();
769 for _ in 0..4 {
770 chart.pump(&ctx);
771 }
772 assert!(chart.order_book(SeriesId::MAIN).is_some());
773 chart.set_replay_time(Some(time(1)));
774 assert!(
775 chart.order_book(SeriesId::MAIN).is_none(),
776 "bar-only replay cannot expose a live book"
777 );
778 chart.set_replay_time(None);
779 assert!(chart.order_book(SeriesId::MAIN).is_some());
780 chart.set_volume_bubbles(Some(crate::chart::overlays::VolumeBubblesConfig {
781 min_size_filter: Some(2.0),
782 ..Default::default()
783 }));
784 let heatmap_config = crate::chart::overlays::LiquidityHeatmapConfig {
785 brightness: 0.5,
786 lower_cutoff: 2.0,
787 full_intensity_size: Some(10.0),
788 };
789 chart.set_liquidity_heatmap_config(heatmap_config);
790 chart.set_liquidity_heatmap(true);
791 chart.set_chart_type(crate::model::ChartType::VolumeFootprint);
792 for _ in 0..4 {
793 chart.pump(&ctx);
794 }
795 let original = chart.bars(SeriesId::MAIN).to_vec();
796 assert_eq!(original.len(), 3);
797 let replay = load();
798 replay.set_speed(0.0).unwrap();
799 replay.seek(time(0));
800 chart.set_market_replay(Some(replay.clone()));
801 for _ in 0..4 {
802 chart.pump(&ctx);
803 }
804 assert_eq!(chart.bars(SeriesId::MAIN), &original[..1]);
805 assert_eq!(chart.liquidity_heatmap_config(), heatmap_config);
806 assert_eq!(
807 chart
808 .feed_link
809 .as_ref()
810 .unwrap()
811 .bubbles
812 .as_ref()
813 .map(|bubbles| bubbles.config.min_size_filter),
814 Some(Some(2.0)),
815 "market replay keeps the configured trade bubbles"
816 );
817 assert_eq!(
818 chart
819 .feed_link
820 .as_ref()
821 .unwrap()
822 .footprint
823 .as_ref()
824 .unwrap()
825 .bars()
826 .len(),
827 1,
828 "a seek cannot retain future footprint rows"
829 );
830 replay.seek(time(1));
831 chart.pump(&ctx);
832 assert_eq!(chart.bars(SeriesId::MAIN), &original[..2]);
833 chart.set_timeframe(Timeframe::Min5);
834 for _ in 0..4 {
835 chart.pump(&ctx);
836 }
837 assert_eq!(
838 chart.bars(SeriesId::MAIN),
839 [bar(0, [10.0, 12.0, 10.0, 12.0, 2.0])]
840 );
841 chart.set_timeframe(Timeframe::Min1);
842 for _ in 0..4 {
843 chart.pump(&ctx);
844 }
845 chart.set_market_replay(None);
846 for _ in 0..4 {
847 chart.pump(&ctx);
848 }
849 assert_eq!(chart.bars(SeriesId::MAIN), original);
850 assert!(chart.replay_time().is_none());
851 assert!(chart.replay_driver().is_none());
852 assert_eq!(chart.liquidity_heatmap_config(), heatmap_config);
853
854 let books = (0..8)
856 .map(|minute| {
857 ReplayEvent::new(
858 "A",
859 time(minute),
860 minute as u64,
861 MarketEvent::DepthSnapshot {
862 sequence: minute as u64,
863 book: crate::model::OrderBook {
864 ts: time(minute),
865 symbol: "A".into(),
866 bids: vec![crate::model::OrderBookLevel {
867 price: 99.0,
868 size: (minute + 1) as f64,
869 order_cnt: None,
870 }],
871 asks: Vec::new(),
872 },
873 },
874 )
875 })
876 .collect();
877 let driver = Arc::new(
878 ReplayDriver::new(books, vec![Instrument::new("A").with_tick_size(1.0)]).unwrap(),
879 );
880 let mut chart = super::super::ChartBuilder::new()
881 .with_feed(driver.clone())
882 .with_symbol("A")
883 .with_timeframe(Timeframe::Min1)
884 .build();
885 chart.set_depth_history(Some(
886 crate::data::DepthHistory::new(crate::data::DepthHistoryConfig {
887 columns: 2,
888 cadence: chrono::TimeDelta::minutes(1),
889 ..Default::default()
890 })
891 .unwrap(),
892 ));
893 for _ in 0..4 {
894 chart.pump(&ctx);
895 }
896 driver.advance(std::time::Duration::ZERO).unwrap();
897 chart.pump(&ctx);
898 for _ in 1..8 {
899 driver.advance(std::time::Duration::from_secs(60)).unwrap();
900 chart.pump(&ctx);
901 }
902 let expected: Vec<_> = chart
903 .depth_history(SeriesId::MAIN)
904 .unwrap()
905 .columns()
906 .cloned()
907 .collect();
908 assert!(expected.len() > 2);
909 driver.seek(time(0));
910 chart.pump(&ctx);
911 driver.seek(time(7));
912 chart.pump(&ctx);
913 assert_eq!(
914 chart
915 .depth_history(SeriesId::MAIN)
916 .unwrap()
917 .columns()
918 .cloned()
919 .collect::<Vec<_>>(),
920 expected
921 );
922 let config = chart
923 .depth_history(SeriesId::MAIN)
924 .unwrap()
925 .config()
926 .clone();
927 chart.set_depth_history(Some(crate::data::DepthHistory::new(config).unwrap()));
928 for _ in 0..4 {
929 chart.pump(&ctx);
930 }
931 assert_eq!(
932 chart
933 .depth_history(SeriesId::MAIN)
934 .unwrap()
935 .columns()
936 .cloned()
937 .collect::<Vec<_>>(),
938 expected[expected.len() - 2..],
939 "enabling history at a paused cursor must load its latest recording page"
940 );
941 }
942}