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 let mut time = earliest;
163 let mut missing_count = 0;
164
165 while time < latest {
166 if !self.datapoints.contains_key(&time) {
167 missing_count += 1;
168 break;
169 }
170 time = time.saturating_add(interval);
171 }
172
173 if missing_count > 0 {
174 let mut missing_keys =
175 Vec::with_capacity(((latest.as_u64() - earliest.as_u64()) / interval) as usize);
176 let mut time = earliest;
177
178 while time < latest {
179 if !self.datapoints.contains_key(&time) {
180 missing_keys.push(time);
181 }
182 time = time.saturating_add(interval);
183 }
184
185 log::debug!(
186 "Integrity check failed: missing {} klines",
187 missing_keys.len()
188 );
189 return Some(missing_keys);
190 }
191
192 None
193 }
194
195 pub fn check_kline_integrity(&self, earliest: UnixMs, latest: UnixMs) -> Option<Vec<UnixMs>> {
196 if self.datapoints.is_empty() {
197 return None;
198 }
199
200 let interval = self.interval.to_milliseconds();
201 if interval == 0 {
202 return None;
203 }
204
205 let (series_earliest, series_latest) = self.timerange();
206 let phase = UnixMs::new(series_earliest.as_u64() % interval);
207
208 let check_earliest =
209 Self::align_down_to_phase(earliest.max(series_earliest), phase, interval)
210 .max(series_earliest);
211 let check_latest = Self::align_down_to_phase(latest.min(series_latest), phase, interval)
212 .min(series_latest);
213
214 if check_earliest < check_latest {
215 self.check_kline_integrity_range(check_earliest, check_latest, interval)
216 } else {
217 None
218 }
219 }
220}
221
222impl TimeSeries<KlineDataPoint> {
223 pub fn new(interval: Timeframe, tick_size: PriceStep, klines: &[Kline]) -> Self {
224 let mut timeseries = Self {
225 datapoints: BTreeMap::new(),
226 interval,
227 tick_size,
228 };
229
230 timeseries.insert_klines(klines);
231 timeseries
232 }
233
234 pub fn with_trades(&self, trades: &[Trade]) -> TimeSeries<KlineDataPoint> {
235 let mut new_series = Self {
236 datapoints: self.datapoints.clone(),
237 interval: self.interval,
238 tick_size: self.tick_size,
239 };
240
241 new_series.insert_trades_or_create_bucket(trades);
242 new_series
243 }
244
245 pub fn insert_klines(&mut self, klines: &[Kline]) {
246 for kline in klines {
247 let entry = self
248 .datapoints
249 .entry(kline.time)
250 .or_insert_with(|| KlineDataPoint {
251 kline: *kline,
252 footprint: KlineTrades::new(),
253 });
254
255 entry.kline = *kline;
256 }
257
258 self.update_poc_status();
259 }
260
261 pub fn insert_trades_or_create_bucket(&mut self, buffer: &[Trade]) {
262 if buffer.is_empty() {
263 return;
264 }
265 let mut updated_times = Vec::new();
266
267 buffer.iter().for_each(|trade| {
268 let rounded_time = trade.time.floor_to(self.interval);
269
270 if !updated_times.contains(&rounded_time) {
271 updated_times.push(rounded_time);
272 }
273
274 let entry = self
275 .datapoints
276 .entry(rounded_time)
277 .or_insert_with(|| KlineDataPoint {
278 kline: Kline {
279 time: rounded_time,
280 open: trade.price,
281 high: trade.price,
282 low: trade.price,
283 close: trade.price,
284 volume: Volume::empty_buy_sell(),
285 },
286 footprint: KlineTrades::new(),
287 });
288
289 entry.add_trade(trade, self.tick_size);
290 });
291
292 for time in updated_times {
293 if let Some(data_point) = self.datapoints.get_mut(&time) {
294 data_point.calculate_poc();
295 }
296 }
297 }
298
299 pub fn insert_trades_existing_buckets(&mut self, buffer: &[Trade]) {
300 if buffer.is_empty() {
301 return;
302 }
303 let mut updated_times: Vec<UnixMs> = Vec::new();
304
305 for trade in buffer {
306 let rounded_time = trade.time.floor_to(self.interval);
307
308 if let Some(entry) = self.datapoints.get_mut(&rounded_time) {
309 if !updated_times.contains(&rounded_time) {
310 updated_times.push(rounded_time);
311 }
312 entry.add_trade(trade, self.tick_size);
313 }
314 }
315
316 for time in updated_times {
317 if let Some(data_point) = self.datapoints.get_mut(&time) {
318 data_point.calculate_poc();
319 }
320 }
321 }
322
323 pub fn change_tick_size(&mut self, tick_size: PriceStep, raw_trades: &[Trade]) {
324 self.tick_size = tick_size;
325
326 self.clear_trades();
327
328 if !raw_trades.is_empty() {
329 self.insert_trades_existing_buckets(raw_trades);
330 }
331 }
332
333 pub fn update_poc_status(&mut self) {
334 let updates = self
335 .datapoints
336 .iter()
337 .filter_map(|(&time, dp)| dp.poc_price().map(|price| (time, price)))
338 .collect::<Vec<_>>();
339
340 for (current_time, poc_price) in updates {
341 let mut npoc = NPoc::default();
342
343 for (&next_time, next_dp) in self.datapoints.range(current_time.saturating_add(1)..) {
344 let next_dp_low = next_dp.kline.low.round_to_side_step(true, self.tick_size);
345 let next_dp_high = next_dp.kline.high.round_to_side_step(false, self.tick_size);
346
347 if next_dp_low <= poc_price && next_dp_high >= poc_price {
348 npoc.filled(next_time.as_u64());
349 break;
350 } else {
351 npoc.unfilled();
352 }
353 }
354
355 if let Some(data_point) = self.datapoints.get_mut(¤t_time) {
356 data_point.set_poc_status(npoc);
357 }
358 }
359 }
360
361 pub fn min_max_footprint_price_in_range(
362 &self,
363 earliest: UnixMs,
364 latest: UnixMs,
365 ) -> Option<(Price, Price)> {
366 if latest < earliest {
367 return None;
368 }
369
370 let mut min_price: Option<Price> = None;
371 let mut max_price: Option<Price> = None;
372
373 let mut track_price = |price: Price| {
374 min_price = Some(match min_price {
375 Some(current) => current.min(price),
376 None => price,
377 });
378 max_price = Some(match max_price {
379 Some(current) => current.max(price),
380 None => price,
381 });
382 };
383
384 self.datapoints
385 .range(earliest..=latest)
386 .for_each(|(_, dp)| {
387 track_price(dp.kline.low);
388 track_price(dp.kline.high);
389
390 for price in dp.footprint.trades.keys() {
391 track_price(*price);
392 }
393 });
394
395 match (min_price, max_price) {
396 (Some(low), Some(high)) => Some((low, high)),
397 _ => None,
398 }
399 }
400
401 pub fn suggest_trade_fetch_range(
402 &self,
403 visible_earliest: UnixMs,
404 visible_latest: UnixMs,
405 ) -> Option<(UnixMs, UnixMs)> {
406 if self.datapoints.is_empty() {
407 return None;
408 }
409
410 self.find_trade_gap()
411 .and_then(|(last_t_before_gap, first_t_after_gap)| {
412 if last_t_before_gap.is_none() && first_t_after_gap.is_none() {
413 return None;
414 }
415 let (data_earliest, data_latest) = self.timerange();
416
417 let fetch_from = last_t_before_gap
418 .map_or(data_earliest, |t| t.saturating_add(1))
419 .max(visible_earliest);
420 let fetch_to = first_t_after_gap
421 .map_or(data_latest, |t| t.saturating_sub(1))
422 .min(visible_latest);
423
424 if fetch_from < fetch_to {
425 Some((fetch_from, fetch_to))
426 } else {
427 None
428 }
429 })
430 }
431
432 fn find_trade_gap(&self) -> Option<(Option<UnixMs>, Option<UnixMs>)> {
433 let empty_kline_time = self
434 .datapoints
435 .iter()
436 .rev()
437 .find(|(_, dp)| dp.footprint.trades.is_empty())
438 .map(|(&time, _)| time);
439
440 if let Some(target_time) = empty_kline_time {
441 let last_t_before_gap = self
442 .datapoints
443 .range(..target_time)
444 .rev()
445 .find_map(|(_, dp)| dp.last_trade_time());
446
447 let first_t_after_gap = self
448 .datapoints
449 .range(target_time.saturating_add(1)..)
450 .find_map(|(_, dp)| dp.first_trade_time());
451
452 Some((last_t_before_gap, first_t_after_gap))
453 } else {
454 None
455 }
456 }
457
458 pub fn max_qty_ts_range(
459 &self,
460 cluster_kind: ClusterKind,
461 earliest: UnixMs,
462 latest: UnixMs,
463 highest: Price,
464 lowest: Price,
465 ) -> Qty {
466 let mut max_cluster_qty: Qty = Qty::default();
467
468 self.datapoints
469 .range(earliest..=latest)
470 .for_each(|(_, dp)| {
471 max_cluster_qty =
472 max_cluster_qty.max(dp.max_cluster_qty(cluster_kind, highest, lowest));
473 });
474
475 max_cluster_qty
476 }
477}
478
479impl TimeSeries<HeatmapDataPoint> {
480 pub fn new(basis: Basis, tick_size: PriceStep) -> Self {
481 let timeframe = match basis {
482 Basis::Time(interval) => interval,
483 Basis::Tick(_) => unimplemented!(),
484 };
485
486 Self {
487 datapoints: BTreeMap::new(),
488 interval: timeframe,
489 tick_size,
490 }
491 }
492
493 pub fn max_trade_qty_and_aggr_volume(&self, earliest: UnixMs, latest: UnixMs) -> (Qty, Qty) {
494 let mut max_trade_qty = Qty::ZERO;
495 let mut max_aggr_volume = Qty::ZERO;
496
497 self.datapoints
498 .range(earliest..=latest)
499 .for_each(|(_, dp)| {
500 let (mut buy_volume, mut sell_volume) = (Qty::ZERO, Qty::ZERO);
501
502 dp.grouped_trades.iter().for_each(|trade| {
503 let trade_qty = trade.qty;
504 max_trade_qty = max_trade_qty.max(trade_qty);
505
506 if trade.is_sell {
507 sell_volume += trade_qty;
508 } else {
509 buy_volume += trade_qty;
510 }
511 });
512
513 max_aggr_volume = max_aggr_volume.max(buy_volume + sell_volume);
514 });
515
516 (max_trade_qty, max_aggr_volume)
517 }
518
519 pub fn max_trade_qty_in_range(
520 &self,
521 earliest: UnixMs,
522 latest: UnixMs,
523 highest: Price,
524 lowest: Price,
525 ) -> Qty {
526 let mut max_trade_qty = Qty::default();
527
528 self.datapoints
529 .range(earliest..=latest)
530 .for_each(|(_, dp)| {
531 dp.grouped_trades.iter().for_each(|trade| {
532 if trade.price >= lowest && trade.price <= highest {
533 max_trade_qty = max_trade_qty.max(trade.qty);
534 }
535 });
536 });
537
538 max_trade_qty
539 }
540}
541
542impl From<&TimeSeries<KlineDataPoint>> for BTreeMap<UnixMs, exchange::Volume> {
543 fn from(timeseries: &TimeSeries<KlineDataPoint>) -> Self {
545 timeseries
546 .datapoints
547 .iter()
548 .map(|(time, dp)| (*time, dp.kline.volume))
549 .collect()
550 }
551}