1use std::collections::BTreeMap;
2
3use crate::chart::Basis;
4use crate::chart::heatmap::HeatmapDataPoint;
5use crate::chart::kline::{ClusterKind, KlineDataPoint, KlineTrades, NPoc};
6
7use exchange::unit::{Price, PriceStep, Qty};
8use exchange::{Kline, Timeframe, Trade, UnixMs, Volume};
9
10pub trait DataPoint {
11 fn add_trade(&mut self, trade: &Trade, step: PriceStep);
12
13 fn clear_trades(&mut self);
14
15 fn last_trade_time(&self) -> Option<UnixMs>;
16
17 fn first_trade_time(&self) -> Option<UnixMs>;
18
19 fn last_price(&self) -> Price;
20
21 fn kline(&self) -> Option<&Kline>;
22
23 fn value_high(&self) -> Price;
24
25 fn value_low(&self) -> Price;
26}
27
28pub struct TimeSeries<D: DataPoint> {
29 pub datapoints: BTreeMap<UnixMs, D>,
30 pub interval: Timeframe,
31 pub tick_size: PriceStep,
32}
33
34impl<D: DataPoint> TimeSeries<D> {
35 pub fn base_price(&self) -> Price {
36 self.datapoints
37 .values()
38 .last()
39 .map_or(Price::from_f32(0.0), DataPoint::last_price)
40 }
41
42 pub fn latest_timestamp(&self) -> Option<UnixMs> {
43 self.datapoints.keys().last().copied()
44 }
45
46 pub fn latest_kline(&self) -> Option<&Kline> {
47 self.datapoints.values().last().and_then(|dp| dp.kline())
48 }
49
50 pub fn price_scale(&self, lookback: usize) -> (Price, Price) {
51 let mut iter = self.datapoints.iter().rev().take(lookback);
52
53 if let Some((_, first)) = iter.next() {
54 let mut high = first.value_high();
55 let mut low = first.value_low();
56
57 for (_, dp) in iter {
58 let value_high = dp.value_high();
59 let value_low = dp.value_low();
60 if value_high > high {
61 high = value_high;
62 }
63 if value_low < low {
64 low = value_low;
65 }
66 }
67
68 (high, low)
69 } else {
70 (Price::from_f32(0.0), Price::from_f32(0.0))
71 }
72 }
73
74 pub fn volume_data<'a>(&'a self) -> BTreeMap<UnixMs, exchange::Volume>
75 where
76 BTreeMap<UnixMs, exchange::Volume>: From<&'a TimeSeries<D>>,
77 {
78 self.into()
79 }
80
81 pub fn timerange(&self) -> (UnixMs, UnixMs) {
82 let earliest = self
83 .datapoints
84 .keys()
85 .next()
86 .copied()
87 .unwrap_or(UnixMs::ZERO);
88 let latest = self
89 .datapoints
90 .keys()
91 .last()
92 .copied()
93 .unwrap_or(UnixMs::ZERO);
94
95 (earliest, latest)
96 }
97
98 pub fn min_max_price_in_range_prices(
99 &self,
100 earliest: UnixMs,
101 latest: UnixMs,
102 ) -> Option<(Price, Price)> {
103 let mut it = self.datapoints.range(earliest..=latest);
104
105 let (_, first) = it.next()?;
106 let mut min_price = first.value_low();
107 let mut max_price = first.value_high();
108
109 for (_, dp) in it {
110 let low = dp.value_low();
111 let high = dp.value_high();
112 if low < min_price {
113 min_price = low;
114 }
115 if high > max_price {
116 max_price = high;
117 }
118 }
119
120 Some((min_price, max_price))
121 }
122
123 pub fn min_max_price_in_range(&self, earliest: UnixMs, latest: UnixMs) -> Option<(f32, f32)> {
124 self.min_max_price_in_range_prices(earliest, latest)
125 .map(|(min_p, max_p)| (min_p.to_f32_lossy(), max_p.to_f32_lossy()))
126 }
127
128 pub fn ingest_trades_bucket(&mut self, rounded_t: UnixMs, trades: &[Trade], step: PriceStep)
130 where
131 D: Default,
132 {
133 let bucket = self.datapoints.entry(rounded_t).or_default();
134
135 for trade in trades {
136 bucket.add_trade(trade, step);
137 }
138 }
139
140 pub fn clear_trades(&mut self) {
141 for data_point in self.datapoints.values_mut() {
142 data_point.clear_trades();
143 }
144 }
145
146 fn align_down_to_phase(time: UnixMs, phase: UnixMs, interval: u64) -> UnixMs {
147 if time >= phase {
148 let t = time.as_u64();
149 let p = phase.as_u64();
150 UnixMs::new(t.saturating_sub((t - p) % interval))
151 } else {
152 phase
153 }
154 }
155
156 fn check_kline_integrity_range(
157 &self,
158 earliest: UnixMs,
159 latest: UnixMs,
160 interval: u64,
161 ) -> Option<Vec<UnixMs>> {
162 const MAX_INTEGRITY_SCAN: u64 = 1_000_000;
163 let mut time = earliest;
164 let mut missing_count = 0;
165
166 for _ in 0..MAX_INTEGRITY_SCAN {
167 if time >= latest {
168 break;
169 }
170 if !self.datapoints.contains_key(&time) {
171 missing_count += 1;
172 break;
173 }
174 time = time.saturating_add(interval);
175 }
176
177 if missing_count > 0 {
178 let mut missing_keys = Vec::new();
179 let mut time = earliest;
180
181 for _ in 0..MAX_INTEGRITY_SCAN {
182 if time >= latest {
183 break;
184 }
185 if !self.datapoints.contains_key(&time) {
186 missing_keys.push(time);
187 }
188 time = time.saturating_add(interval);
189 }
190
191 log::debug!(
192 "Integrity check failed: missing {} klines",
193 missing_keys.len()
194 );
195 return Some(missing_keys);
196 }
197
198 None
199 }
200
201 pub fn check_kline_integrity(&self, earliest: UnixMs, latest: UnixMs) -> Option<Vec<UnixMs>> {
202 if self.datapoints.is_empty() {
203 return None;
204 }
205
206 let interval = self.interval.to_milliseconds();
207 if interval == 0 {
208 return None;
209 }
210
211 let (series_earliest, series_latest) = self.timerange();
212 let phase = UnixMs::new(series_earliest.as_u64() % interval);
213
214 let check_earliest =
215 Self::align_down_to_phase(earliest.max(series_earliest), phase, interval)
216 .max(series_earliest);
217 let check_latest = Self::align_down_to_phase(latest.min(series_latest), phase, interval)
218 .min(series_latest);
219
220 if check_earliest < check_latest {
221 self.check_kline_integrity_range(check_earliest, check_latest, interval)
222 } else {
223 None
224 }
225 }
226}
227
228impl TimeSeries<KlineDataPoint> {
229 pub fn new(interval: Timeframe, tick_size: PriceStep, klines: &[Kline]) -> Self {
230 let mut timeseries = Self {
231 datapoints: BTreeMap::new(),
232 interval,
233 tick_size,
234 };
235
236 timeseries.insert_klines(klines);
237 timeseries
238 }
239
240 pub fn with_trades(&self, trades: &[Trade]) -> TimeSeries<KlineDataPoint> {
241 let mut new_series = Self {
242 datapoints: self.datapoints.clone(),
243 interval: self.interval,
244 tick_size: self.tick_size,
245 };
246
247 new_series.insert_trades_or_create_bucket(trades);
248 new_series
249 }
250
251 pub fn insert_klines(&mut self, klines: &[Kline]) {
252 for kline in klines {
253 let entry = self
254 .datapoints
255 .entry(kline.time)
256 .or_insert_with(|| KlineDataPoint {
257 kline: *kline,
258 footprint: KlineTrades::new(),
259 });
260
261 entry.kline = *kline;
262 }
263
264 self.update_poc_status();
265 }
266
267 pub fn insert_trades_or_create_bucket(&mut self, buffer: &[Trade]) {
268 if buffer.is_empty() {
269 return;
270 }
271 let mut updated_times = Vec::new();
272
273 buffer.iter().for_each(|trade| {
274 let rounded_time = trade.time.floor_to(self.interval);
275
276 if !updated_times.contains(&rounded_time) {
277 updated_times.push(rounded_time);
278 }
279
280 let entry = self
281 .datapoints
282 .entry(rounded_time)
283 .or_insert_with(|| KlineDataPoint {
284 kline: Kline {
285 time: rounded_time,
286 open: trade.price,
287 high: trade.price,
288 low: trade.price,
289 close: trade.price,
290 volume: Volume::empty_buy_sell(),
291 },
292 footprint: KlineTrades::new(),
293 });
294
295 entry.add_trade(trade, self.tick_size);
296 });
297
298 for time in updated_times {
299 if let Some(data_point) = self.datapoints.get_mut(&time) {
300 data_point.calculate_poc();
301 }
302 }
303 }
304
305 pub fn insert_trades_existing_buckets(&mut self, buffer: &[Trade]) {
306 if buffer.is_empty() {
307 return;
308 }
309 let mut updated_times: Vec<UnixMs> = Vec::new();
310
311 for trade in buffer {
312 let rounded_time = trade.time.floor_to(self.interval);
313
314 if let Some(entry) = self.datapoints.get_mut(&rounded_time) {
315 if !updated_times.contains(&rounded_time) {
316 updated_times.push(rounded_time);
317 }
318 entry.add_trade(trade, self.tick_size);
319 }
320 }
321
322 for time in updated_times {
323 if let Some(data_point) = self.datapoints.get_mut(&time) {
324 data_point.calculate_poc();
325 }
326 }
327 }
328
329 pub fn change_tick_size(&mut self, tick_size: PriceStep, raw_trades: &[Trade]) {
330 self.tick_size = tick_size;
331
332 self.clear_trades();
333
334 if !raw_trades.is_empty() {
335 self.insert_trades_existing_buckets(raw_trades);
336 }
337 }
338
339 pub fn update_poc_status(&mut self) {
340 let updates = self
341 .datapoints
342 .iter()
343 .filter_map(|(&time, dp)| dp.poc_price().map(|price| (time, price)))
344 .collect::<Vec<_>>();
345
346 for (current_time, poc_price) in updates {
347 let mut npoc = NPoc::default();
348
349 for (&next_time, next_dp) in self.datapoints.range(current_time.saturating_add(1)..) {
350 let next_dp_low = next_dp.kline.low.round_to_side_step(true, self.tick_size);
351 let next_dp_high = next_dp.kline.high.round_to_side_step(false, self.tick_size);
352
353 if next_dp_low <= poc_price && next_dp_high >= poc_price {
354 npoc.filled(next_time.as_u64());
355 break;
356 } else {
357 npoc.unfilled();
358 }
359 }
360
361 if let Some(data_point) = self.datapoints.get_mut(¤t_time) {
362 data_point.set_poc_status(npoc);
363 }
364 }
365 }
366
367 pub fn min_max_footprint_price_in_range(
368 &self,
369 earliest: UnixMs,
370 latest: UnixMs,
371 ) -> Option<(Price, Price)> {
372 if latest < earliest {
373 return None;
374 }
375
376 let mut min_price: Option<Price> = None;
377 let mut max_price: Option<Price> = None;
378
379 let mut track_price = |price: Price| {
380 min_price = Some(match min_price {
381 Some(current) => current.min(price),
382 None => price,
383 });
384 max_price = Some(match max_price {
385 Some(current) => current.max(price),
386 None => price,
387 });
388 };
389
390 self.datapoints
391 .range(earliest..=latest)
392 .for_each(|(_, dp)| {
393 track_price(dp.kline.low);
394 track_price(dp.kline.high);
395
396 for price in dp.footprint.trades.keys() {
397 track_price(*price);
398 }
399 });
400
401 match (min_price, max_price) {
402 (Some(low), Some(high)) => Some((low, high)),
403 _ => None,
404 }
405 }
406
407 pub fn suggest_trade_fetch_range(
408 &self,
409 visible_earliest: UnixMs,
410 visible_latest: UnixMs,
411 ) -> Option<(UnixMs, UnixMs)> {
412 if self.datapoints.is_empty() {
413 return None;
414 }
415
416 self.find_trade_gap()
417 .and_then(|(last_t_before_gap, first_t_after_gap)| {
418 if last_t_before_gap.is_none() && first_t_after_gap.is_none() {
419 return None;
420 }
421 let (data_earliest, data_latest) = self.timerange();
422
423 let gap_start = last_t_before_gap.map_or(data_earliest, |t| t.saturating_add(1));
424
425 let fetch_from = gap_start
428 .max(visible_earliest)
429 .floor_to(self.interval)
430 .max(gap_start);
431
432 let fetch_to = match first_t_after_gap {
439 Some(t) => t.saturating_sub(1),
440 None => data_latest.min(visible_latest),
441 };
442
443 if fetch_from < fetch_to {
444 if gap_start < visible_earliest {
451 let interval_ms = self.interval.to_milliseconds();
452 if fetch_to.as_u64().saturating_sub(fetch_from.as_u64()) < interval_ms {
453 return None;
454 }
455 }
456 Some((fetch_from, fetch_to))
457 } else {
458 None
459 }
460 })
461 }
462
463 fn find_trade_gap(&self) -> Option<(Option<UnixMs>, Option<UnixMs>)> {
464 let empty_kline_time = self
465 .datapoints
466 .iter()
467 .rev()
468 .find(|(_, dp)| dp.footprint.trades.is_empty())
469 .map(|(&time, _)| time);
470
471 if let Some(target_time) = empty_kline_time {
472 let last_t_before_gap = self
473 .datapoints
474 .range(..target_time)
475 .rev()
476 .find_map(|(_, dp)| dp.last_trade_time());
477
478 let first_t_after_gap = self
479 .datapoints
480 .range(target_time.saturating_add(1)..)
481 .find_map(|(_, dp)| dp.first_trade_time());
482
483 Some((last_t_before_gap, first_t_after_gap))
484 } else {
485 None
486 }
487 }
488
489 pub fn max_qty_ts_range(
490 &self,
491 cluster_kind: ClusterKind,
492 earliest: UnixMs,
493 latest: UnixMs,
494 highest: Price,
495 lowest: Price,
496 ) -> Qty {
497 let mut max_cluster_qty: Qty = Qty::default();
498
499 self.datapoints
500 .range(earliest..=latest)
501 .for_each(|(_, dp)| {
502 max_cluster_qty =
503 max_cluster_qty.max(dp.max_cluster_qty(cluster_kind, highest, lowest));
504 });
505
506 max_cluster_qty
507 }
508}
509
510impl TimeSeries<HeatmapDataPoint> {
511 pub fn new(basis: Basis, tick_size: PriceStep) -> Self {
512 let timeframe = match basis {
513 Basis::Time(interval) => interval,
514 Basis::Tick(_) => unimplemented!(),
515 };
516
517 Self {
518 datapoints: BTreeMap::new(),
519 interval: timeframe,
520 tick_size,
521 }
522 }
523
524 pub fn max_trade_qty_and_aggr_volume(&self, earliest: UnixMs, latest: UnixMs) -> (Qty, Qty) {
525 let mut max_trade_qty = Qty::ZERO;
526 let mut max_aggr_volume = Qty::ZERO;
527
528 self.datapoints
529 .range(earliest..=latest)
530 .for_each(|(_, dp)| {
531 let (mut buy_volume, mut sell_volume) = (Qty::ZERO, Qty::ZERO);
532
533 dp.grouped_trades.iter().for_each(|trade| {
534 let trade_qty = trade.qty;
535 max_trade_qty = max_trade_qty.max(trade_qty);
536
537 if trade.is_sell {
538 sell_volume += trade_qty;
539 } else {
540 buy_volume += trade_qty;
541 }
542 });
543
544 max_aggr_volume = max_aggr_volume.max(buy_volume + sell_volume);
545 });
546
547 (max_trade_qty, max_aggr_volume)
548 }
549
550 pub fn max_trade_qty_in_range(
551 &self,
552 earliest: UnixMs,
553 latest: UnixMs,
554 highest: Price,
555 lowest: Price,
556 ) -> Qty {
557 let mut max_trade_qty = Qty::default();
558
559 self.datapoints
560 .range(earliest..=latest)
561 .for_each(|(_, dp)| {
562 dp.grouped_trades.iter().for_each(|trade| {
563 if trade.price >= lowest && trade.price <= highest {
564 max_trade_qty = max_trade_qty.max(trade.qty);
565 }
566 });
567 });
568
569 max_trade_qty
570 }
571}
572
573impl From<&TimeSeries<KlineDataPoint>> for BTreeMap<UnixMs, exchange::Volume> {
574 fn from(timeseries: &TimeSeries<KlineDataPoint>) -> Self {
576 timeseries
577 .datapoints
578 .iter()
579 .map(|(time, dp)| (*time, dp.kline.volume))
580 .collect()
581 }
582}