1use std::collections::BTreeMap;
2
3use rustc_hash::FxHashSet;
4
5use crate::chart::Basis;
6use crate::chart::heatmap::HeatmapDataPoint;
7use crate::chart::kline::bar::KlineDataPoint;
8use crate::chart::kline::footprint::{ClusterKind, KlineTrades};
9
10use exchange::unit::{Price, PriceStep, Qty};
11use exchange::{Kline, Timeframe, Trade, UnixMs};
12
13pub trait DataPoint {
14 fn add_trade(&mut self, trade: &Trade, step: PriceStep);
15
16 fn clear_trades(&mut self);
17}
18
19pub struct TimeSeries<D: DataPoint> {
20 pub datapoints: BTreeMap<UnixMs, D>,
21 pub interval: Timeframe,
22 pub tick_size: PriceStep,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub(crate) enum OrderedChange {
27 Tail { from: UnixMs },
28 Rebuild,
29}
30
31#[derive(Debug, Clone)]
32pub(crate) struct KlineInsertReport {
33 pub inserted_timestamps: Vec<UnixMs>,
34 pub ordered_change: OrderedChange,
35}
36
37pub(crate) struct TradeInsertReport {
38 pub earliest_updated: UnixMs,
39 pub previous_poc_prices: BTreeMap<UnixMs, Option<Price>>,
40}
41
42impl<D: DataPoint> TimeSeries<D> {
43 pub fn timerange(&self) -> (UnixMs, UnixMs) {
44 let earliest = self
45 .datapoints
46 .keys()
47 .next()
48 .copied()
49 .unwrap_or(UnixMs::ZERO);
50 let latest = self
51 .datapoints
52 .keys()
53 .last()
54 .copied()
55 .unwrap_or(UnixMs::ZERO);
56
57 (earliest, latest)
58 }
59
60 pub fn ingest_trades_bucket(&mut self, rounded_t: UnixMs, trades: &[Trade], step: PriceStep)
62 where
63 D: Default,
64 {
65 let bucket = self.datapoints.entry(rounded_t).or_default();
66
67 for trade in trades {
68 bucket.add_trade(trade, step);
69 }
70 }
71
72 pub fn clear_trades(&mut self) {
73 for data_point in self.datapoints.values_mut() {
74 data_point.clear_trades();
75 }
76 }
77
78 fn align_down_to_phase(time: UnixMs, phase: UnixMs, interval: u64) -> UnixMs {
79 if time >= phase {
80 let t = time.as_u64();
81 let p = phase.as_u64();
82 UnixMs::new(t.saturating_sub((t - p) % interval))
83 } else {
84 phase
85 }
86 }
87}
88
89impl TimeSeries<KlineDataPoint> {
90 pub fn new(interval: Timeframe, tick_size: PriceStep, klines: &[Kline]) -> Self {
91 let mut timeseries = Self {
92 datapoints: BTreeMap::new(),
93 interval,
94 tick_size,
95 };
96
97 timeseries.insert_klines(klines);
98 timeseries
99 }
100
101 pub fn insert_klines(&mut self, klines: &[Kline]) {
102 let _ = self.insert_klines_report(klines);
103 }
104
105 pub(crate) fn insert_klines_report(&mut self, klines: &[Kline]) -> Option<KlineInsertReport> {
106 if klines.is_empty() {
107 return None;
108 }
109
110 let previous_latest = self.datapoints.keys().next_back().copied();
111 let mut inserted_timestamps = Vec::new();
112 let mut changed_existing = false;
113
114 for kline in klines {
115 let entry = self.datapoints.entry(kline.time);
116 match entry {
117 std::collections::btree_map::Entry::Vacant(entry) => {
118 entry.insert(KlineDataPoint {
119 kline: *kline,
120 footprint: KlineTrades::new(),
121 });
122 inserted_timestamps.push(kline.time);
123 }
124 std::collections::btree_map::Entry::Occupied(mut entry) => {
125 entry.get_mut().kline = *kline;
126 changed_existing = true;
127 }
128 }
129 }
130
131 inserted_timestamps.sort_unstable();
132
133 let ordered_change = if !changed_existing
134 && inserted_timestamps
135 .iter()
136 .all(|timestamp| previous_latest.is_none_or(|latest| *timestamp > latest))
137 {
138 OrderedChange::Tail {
139 from: inserted_timestamps
140 .iter()
141 .copied()
142 .min()
143 .unwrap_or_else(|| previous_latest.unwrap_or(UnixMs::ZERO)),
144 }
145 } else {
146 OrderedChange::Rebuild
147 };
148
149 Some(KlineInsertReport {
150 inserted_timestamps,
151 ordered_change,
152 })
153 }
154
155 pub(crate) fn missing_kline_range(
156 &self,
157 earliest: UnixMs,
158 latest: UnixMs,
159 ) -> Option<(UnixMs, UnixMs)> {
160 if self.datapoints.is_empty() {
161 return None;
162 }
163
164 let interval = self.interval.to_milliseconds();
165 if interval == 0 {
166 return None;
167 }
168
169 let (series_earliest, series_latest) = self.timerange();
170 let phase = UnixMs::new(series_earliest.as_u64() % interval);
171 let check_earliest =
172 Self::align_down_to_phase(earliest.max(series_earliest), phase, interval)
173 .max(series_earliest);
174 let check_latest = Self::align_down_to_phase(latest.min(series_latest), phase, interval)
175 .min(series_latest);
176
177 if check_earliest >= check_latest {
178 return None;
179 }
180
181 let mut first_missing = None;
182 let mut last_missing = None;
183 let mut time = check_earliest;
184 const MAX_INTEGRITY_SCAN: u64 = 1_000_000;
185
186 for _ in 0..MAX_INTEGRITY_SCAN {
187 if time >= check_latest {
188 break;
189 }
190 if !self.datapoints.contains_key(&time) {
191 first_missing.get_or_insert(time);
192 last_missing = Some(time);
193 }
194 time = time.saturating_add(interval);
195 }
196
197 first_missing.zip(last_missing)
198 }
199
200 pub(crate) fn insert_trades_existing_buckets_report(
201 &mut self,
202 buffer: &[Trade],
203 ) -> Option<TradeInsertReport> {
204 if buffer.is_empty() {
205 return None;
206 }
207 let mut updated_times = FxHashSet::default();
208
209 for run in buffer.chunk_by(|left, right| {
210 left.time.floor_to(self.interval) == right.time.floor_to(self.interval)
211 }) {
212 let rounded_time = run[0].time.floor_to(self.interval);
213 if let Some(entry) = self.datapoints.get_mut(&rounded_time) {
214 updated_times.insert(rounded_time);
215 for trade in run {
216 entry.add_trade(trade, self.tick_size);
217 }
218 }
219 }
220
221 let earliest_updated = updated_times.iter().copied().min()?;
222 let mut previous_poc_prices = BTreeMap::new();
223 for time in updated_times {
224 if let Some(data_point) = self.datapoints.get_mut(&time) {
225 let previous = data_point.footprint.poc;
226 data_point.calculate_poc();
227 let previous_price = previous.map(|poc| poc.price);
228 if previous_price == data_point.poc_price() {
229 if let Some(previous) = previous {
230 data_point.set_poc_status(previous.status);
231 }
232 } else {
233 previous_poc_prices.insert(time, previous_price);
234 }
235 }
236 }
237 Some(TradeInsertReport {
238 earliest_updated,
239 previous_poc_prices,
240 })
241 }
242
243 pub fn backfill_new_buckets<'a>(
244 &mut self,
245 trades: impl IntoIterator<Item = &'a Trade>,
246 newborn: &[UnixMs],
247 ) {
248 if newborn.is_empty() {
249 return;
250 }
251
252 let newborn_times = newborn.iter().copied().collect::<FxHashSet<UnixMs>>();
253 let mut updated_times = FxHashSet::default();
254
255 for trade in trades {
256 let rounded_time = trade.time.floor_to(self.interval);
257 if newborn_times.contains(&rounded_time)
258 && let Some(entry) = self.datapoints.get_mut(&rounded_time)
259 {
260 updated_times.insert(rounded_time);
261 entry.add_trade(trade, self.tick_size);
262 }
263 }
264
265 for time in updated_times {
266 if let Some(data_point) = self.datapoints.get_mut(&time) {
267 data_point.calculate_poc();
268 }
269 }
270 }
271
272 pub fn change_tick_size(&mut self, tick_size: PriceStep) {
273 self.tick_size = tick_size;
274 for point in self.datapoints.values_mut() {
275 point.footprint.regroup(tick_size);
276 }
277 }
278
279 pub fn suggest_trade_fetch_range(
280 &self,
281 visible_earliest: UnixMs,
282 visible_latest: UnixMs,
283 ) -> Option<(UnixMs, UnixMs)> {
284 if self.datapoints.is_empty() {
285 return None;
286 }
287
288 self.find_trade_gap()
289 .and_then(|(last_t_before_gap, first_t_after_gap)| {
290 if last_t_before_gap.is_none() && first_t_after_gap.is_none() {
291 return None;
292 }
293 let (data_earliest, data_latest) = self.timerange();
294
295 let gap_start = last_t_before_gap.map_or(data_earliest, |t| t.saturating_add(1));
296
297 let fetch_from = gap_start
298 .max(visible_earliest)
299 .floor_to(self.interval)
300 .max(gap_start);
301
302 let fetch_to = match first_t_after_gap {
303 Some(t) => t.saturating_sub(1),
304 None => data_latest.min(visible_latest),
305 };
306
307 if fetch_from < fetch_to {
308 if gap_start < visible_earliest {
309 let interval_ms = self.interval.to_milliseconds();
310 if fetch_to.as_u64().saturating_sub(fetch_from.as_u64()) < interval_ms {
311 return None;
312 }
313 }
314 Some((fetch_from, fetch_to))
315 } else {
316 None
317 }
318 })
319 }
320
321 fn find_trade_gap(&self) -> Option<(Option<UnixMs>, Option<UnixMs>)> {
322 let empty_kline_time = self
323 .datapoints
324 .iter()
325 .rev()
326 .find(|(_, dp)| dp.footprint.trades.is_empty())
327 .map(|(&time, _)| time);
328
329 if let Some(target_time) = empty_kline_time {
330 let last_t_before_gap = self
331 .datapoints
332 .range(..target_time)
333 .rev()
334 .find_map(|(_, dp)| dp.last_trade_time());
335
336 let first_t_after_gap = self
337 .datapoints
338 .range(target_time.saturating_add(1)..)
339 .find_map(|(_, dp)| dp.first_trade_time());
340
341 Some((last_t_before_gap, first_t_after_gap))
342 } else {
343 None
344 }
345 }
346
347 pub fn max_qty_ts_range(
348 &self,
349 cluster_kind: ClusterKind,
350 earliest: UnixMs,
351 latest: UnixMs,
352 highest: Price,
353 lowest: Price,
354 ) -> Qty {
355 let mut max_cluster_qty: Qty = Qty::default();
356
357 self.datapoints
358 .range(earliest..=latest)
359 .for_each(|(_, dp)| {
360 max_cluster_qty =
361 max_cluster_qty.max(dp.max_cluster_qty(cluster_kind, highest, lowest));
362 });
363
364 max_cluster_qty
365 }
366}
367
368impl TimeSeries<HeatmapDataPoint> {
369 pub fn new(basis: Basis, tick_size: PriceStep) -> Self {
370 let timeframe = match basis {
371 Basis::Time(interval) => interval,
372 Basis::Tick(_) => unimplemented!(),
373 };
374
375 Self {
376 datapoints: BTreeMap::new(),
377 interval: timeframe,
378 tick_size,
379 }
380 }
381}