1use std::sync::Arc;
24
25use digdigdig3::connector_manager::ExchangeHub;
26use digdigdig3::core::types::{AccountType, ExchangeId, SymbolInput};
27use digdigdig3::core::websocket::KlineInterval;
28
29use crate::data::BarPoint;
30use crate::error::{Result, StationError};
31use crate::series::{Kind, SeriesKey};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum FillPolicy {
36 ForwardFill,
38 ZeroFlow,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct ScalarBar {
45 pub bar_open_time: i64,
47 pub value: f64,
49 pub filled: bool,
52}
53
54#[derive(Debug, Clone)]
57pub enum BarAlignedSeries {
58 Klines(Vec<BarPoint>),
60 Scalar(Vec<ScalarBar>),
62}
63
64impl BarAlignedSeries {
65 pub fn len(&self) -> usize {
67 match self {
68 BarAlignedSeries::Klines(v) => v.len(),
69 BarAlignedSeries::Scalar(v) => v.len(),
70 }
71 }
72 pub fn is_empty(&self) -> bool {
73 self.len() == 0
74 }
75}
76
77impl Kind {
78 pub fn fill_policy(&self) -> FillPolicy {
81 match self {
82 Kind::Liquidation | Kind::AggTrade | Kind::Trade => FillPolicy::ZeroFlow,
83 _ => FillPolicy::ForwardFill,
84 }
85 }
86}
87
88fn interval_millis(iv: &str) -> Option<i64> {
93 let iv = iv.trim();
94 let (num, unit) = iv.split_at(iv.len().saturating_sub(1));
95 let n: i64 = num.parse().ok()?;
96 let per = match unit {
97 "m" => 60_000,
98 "h" => 60 * 60_000,
99 "d" | "D" => 24 * 60 * 60_000,
100 "w" | "W" => 7 * 24 * 60 * 60_000,
101 _ => return None,
102 };
103 Some(n * per)
104}
105
106fn resample(mut src: Vec<(i64, f64)>, start: i64, end: i64, step: i64, policy: FillPolicy) -> Vec<ScalarBar> {
117 src.sort_unstable_by_key(|(ts, _)| *ts);
118 let first_bar = start.div_euclid(step) * step;
119 let mut out = Vec::new();
120
121 match policy {
122 FillPolicy::ForwardFill => {
123 let mut idx = 0usize;
124 let mut last: Option<f64> = None;
125 let mut t = first_bar;
126 while t < end {
127 let bar_close = t + step;
128 let mut fresh = false;
129 while idx < src.len() && src[idx].0 < bar_close {
130 last = Some(src[idx].1);
131 if src[idx].0 >= t {
132 fresh = true;
133 }
134 idx += 1;
135 }
136 if let Some(v) = last {
137 out.push(ScalarBar { bar_open_time: t, value: v, filled: !fresh });
138 }
139 t += step;
140 }
141 }
142 FillPolicy::ZeroFlow => {
143 let mut idx = 0usize;
144 let mut t = first_bar;
145 while t < end {
146 let bar_close = t + step;
147 let mut sum = 0.0;
148 while idx < src.len() && src[idx].0 < bar_close {
149 if src[idx].0 >= t {
150 sum += src[idx].1;
151 }
152 idx += 1;
153 }
154 out.push(ScalarBar { bar_open_time: t, value: sum, filled: false });
155 t += step;
156 }
157 }
158 }
159 out
160}
161
162pub fn bar_align_points<T, F>(
177 points: &[T],
178 project: F,
179 start_ms: i64,
180 end_ms: i64,
181 interval: &KlineInterval,
182 policy: FillPolicy,
183) -> Result<Vec<ScalarBar>>
184where
185 T: crate::series::DataPoint,
186 F: Fn(&T) -> f64,
187{
188 let step = interval_millis(interval.as_str())
189 .ok_or_else(|| StationError::Core(format!("interval {interval} has no fixed ms width for resample")))?;
190 let src: Vec<(i64, f64)> = points
191 .iter()
192 .filter(|p| {
193 let t = p.timestamp_ms();
194 t >= start_ms && t < end_ms
195 })
196 .map(|p| (p.timestamp_ms(), project(p)))
197 .collect();
198 Ok(resample(src, start_ms, end_ms, step, policy))
199}
200
201#[cfg(not(target_arch = "wasm32"))]
208pub async fn bar_align_from_disk<T, F>(
209 store: &crate::series::DiskStore<T>,
210 project: F,
211 start_ms: i64,
212 end_ms: i64,
213 interval: &KlineInterval,
214 policy: FillPolicy,
215 max_points: usize,
216) -> Result<Vec<ScalarBar>>
217where
218 T: crate::series::DataPoint,
219 F: Fn(&T) -> f64,
220{
221 let pts = store
222 .read_tail(max_points)
223 .await
224 .map_err(|e| StationError::Core(format!("DiskStore read_tail failed: {e}")))?;
225 bar_align_points(&pts, project, start_ms, end_ms, interval, policy)
226}
227
228const KLINE_PAGE: u16 = 1000;
230
231async fn paginate_klines<F, Fut>(start: i64, end: i64, mut fetch: F) -> Result<Vec<BarPoint>>
238where
239 F: FnMut(i64) -> Fut,
240 Fut: std::future::Future<Output = Result<Vec<BarPoint>>>,
241{
242 let mut all: Vec<BarPoint> = Vec::new();
243 let mut cursor = end;
244 for _ in 0..256 {
246 let page = match fetch(cursor).await {
249 Ok(p) => p,
250 Err(_) => fetch(cursor).await?,
251 };
252 if page.is_empty() {
253 break;
254 }
255 let oldest = page.iter().map(|b| b.open_time).min().unwrap_or(cursor);
256 all.extend(page);
257 if oldest <= start {
258 break;
259 }
260 let next = oldest - 1;
262 if next >= cursor {
263 break; }
265 cursor = next;
266 }
267 all.retain(|b| b.open_time >= start && b.open_time < end);
268 all.sort_unstable_by_key(|b| b.open_time);
269 all.dedup_by_key(|b| b.open_time);
270 Ok(all)
271}
272
273pub async fn load_bar_aligned(
288 hub: &Arc<ExchangeHub>,
289 exchange: ExchangeId,
290 account: AccountType,
291 symbol: &str,
292 kind: &Kind,
293 interval: &KlineInterval,
294 start_ms: i64,
295 end_ms: i64,
296) -> Result<BarAlignedSeries> {
297 let rest = hub
298 .rest(exchange)
299 .ok_or_else(|| StationError::Core(format!("{exchange:?} not connected in hub")))?;
300
301 if let Some(klines) = match kind {
303 Kind::Kline(iv) => Some(("kline", iv.clone())),
304 Kind::MarkPriceKline(iv) => Some(("mark", iv.clone())),
305 Kind::IndexPriceKline(iv) => Some(("index", iv.clone())),
306 Kind::PremiumIndexKline(iv) => Some(("premium", iv.clone())),
307 _ => None,
308 } {
309 let (which, iv) = klines;
310 let rest = rest.clone();
311 let sym = symbol.to_string();
312 let bars = paginate_klines(start_ms, end_ms, |cursor| {
313 let rest = rest.clone();
314 let sym = sym.clone();
315 let ivs = iv.as_str().to_string();
316 async move {
317 let res = match which {
318 "kline" => rest
319 .get_klines(SymbolInput::Raw(&sym), &ivs, Some(KLINE_PAGE), account, Some(cursor))
320 .await,
321 "mark" => rest
322 .get_mark_price_klines(SymbolInput::Raw(&sym), &ivs, Some(KLINE_PAGE as u32), account, Some(cursor))
323 .await,
324 "index" => rest
325 .get_index_price_klines(SymbolInput::Raw(&sym), &ivs, Some(KLINE_PAGE as u32), account, Some(cursor))
326 .await,
327 _ => rest
328 .get_premium_index_klines(SymbolInput::Raw(&sym), &ivs, Some(KLINE_PAGE as u32), account, Some(cursor))
329 .await,
330 };
331 res.map(|ks| ks.iter().map(BarPoint::from_kline).collect())
332 .map_err(|e| StationError::Core(format!("{which} klines failed: {e}")))
333 }
334 })
335 .await?;
336 return Ok(BarAlignedSeries::Klines(bars));
337 }
338
339 let step = interval_millis(interval.as_str())
341 .ok_or_else(|| StationError::Core(format!("interval {interval} has no fixed ms width for resample")))?;
342
343 let src: Vec<(i64, f64)> = match kind {
344 Kind::FundingRate => rest
345 .get_funding_rate_history(SymbolInput::Raw(symbol), Some(start_ms), Some(end_ms), Some(1000), account)
346 .await
347 .map_err(|e| StationError::Core(format!("funding history failed: {e}")))?
348 .into_iter()
349 .map(|f| (f.timestamp, f.rate))
350 .collect(),
351
352 Kind::OpenInterest => rest
353 .get_open_interest_history(SymbolInput::Raw(symbol), interval.as_str(), Some(start_ms), Some(end_ms), Some(500), account)
354 .await
355 .map_err(|e| StationError::Core(format!("open interest history failed: {e}")))?
356 .into_iter()
357 .map(|oi| (oi.timestamp, oi.open_interest))
358 .collect(),
359
360 Kind::Basis => rest
361 .get_basis_history(SymbolInput::Raw(symbol), interval.as_str(), Some(start_ms), Some(end_ms), Some(500), account)
362 .await
363 .map_err(|e| StationError::Core(format!("basis history failed: {e}")))?
364 .into_iter()
365 .map(|b| (b.timestamp, b.basis))
366 .collect(),
367
368 Kind::LongShortRatio => rest
369 .get_long_short_ratio_history(SymbolInput::Raw(symbol), interval.as_str(), Some(start_ms), Some(end_ms), Some(500), account)
370 .await
371 .map_err(|e| StationError::Core(format!("long/short ratio history failed: {e}")))?
372 .into_iter()
373 .map(|r| {
374 let v = r.ratio.unwrap_or_else(|| {
376 if r.short_ratio > 0.0 { r.long_ratio / r.short_ratio } else { r.long_ratio }
377 });
378 (r.timestamp, v)
379 })
380 .collect(),
381
382 Kind::MarkPrice | Kind::IndexPrice => {
385 let which = if matches!(kind, Kind::MarkPrice) { "mark" } else { "index" };
386 let rest2 = rest.clone();
387 let sym = symbol.to_string();
388 let ivs = interval.as_str().to_string();
389 let bars = paginate_klines(start_ms, end_ms, |cursor| {
390 let rest2 = rest2.clone();
391 let sym = sym.clone();
392 let ivs = ivs.clone();
393 async move {
394 let res = if which == "mark" {
395 rest2.get_mark_price_klines(SymbolInput::Raw(&sym), &ivs, Some(KLINE_PAGE as u32), account, Some(cursor)).await
396 } else {
397 rest2.get_index_price_klines(SymbolInput::Raw(&sym), &ivs, Some(KLINE_PAGE as u32), account, Some(cursor)).await
398 };
399 res.map(|ks| ks.iter().map(BarPoint::from_kline).collect())
400 .map_err(|e| StationError::Core(format!("{which} klines failed: {e}")))
401 }
402 })
403 .await?;
404 let scalars = bars
406 .into_iter()
407 .map(|b| ScalarBar { bar_open_time: b.open_time, value: b.close, filled: false })
408 .collect();
409 return Ok(BarAlignedSeries::Scalar(scalars));
410 }
411
412 Kind::Liquidation | Kind::AggTrade => {
413 return Err(StationError::StreamNotSupported(format!(
414 "{kind:?} bar-aligned history needs the recording daemon (no usable REST history)"
415 )));
416 }
417
418 other => {
419 return Err(StationError::StreamNotSupported(format!(
420 "bar-aligned loader does not yet support {other:?}"
421 )));
422 }
423 };
424
425 Ok(BarAlignedSeries::Scalar(resample(src, start_ms, end_ms, step, kind.fill_policy())))
426}
427
428pub async fn load_for_key(
432 hub: &Arc<ExchangeHub>,
433 key: &SeriesKey,
434 interval: &KlineInterval,
435 start_ms: i64,
436 end_ms: i64,
437) -> Result<BarAlignedSeries> {
438 let iv = match &key.kind {
439 Kind::Kline(iv) | Kind::MarkPriceKline(iv) | Kind::IndexPriceKline(iv) | Kind::PremiumIndexKline(iv) => iv.clone(),
440 _ => interval.clone(),
441 };
442 load_bar_aligned(hub, key.exchange, key.account_type, &key.symbol, &key.kind, &iv, start_ms, end_ms).await
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448
449 #[test]
450 fn interval_parsing() {
451 assert_eq!(interval_millis("1m"), Some(60_000));
452 assert_eq!(interval_millis("5m"), Some(300_000));
453 assert_eq!(interval_millis("1h"), Some(3_600_000));
454 assert_eq!(interval_millis("4h"), Some(14_400_000));
455 assert_eq!(interval_millis("1d"), Some(86_400_000));
456 assert_eq!(interval_millis("1w"), Some(604_800_000));
457 assert_eq!(interval_millis("1M"), None);
458 assert_eq!(interval_millis("xx"), None);
459 }
460
461 #[test]
462 fn forward_fill_carries_into_empty_bars() {
463 let step = 60_000;
465 let src = vec![(0, 10.0), (130_000, 20.0)];
466 let out = resample(src, 0, 240_000, step, FillPolicy::ForwardFill);
467 assert_eq!(out.len(), 4);
469 assert_eq!(out[0], ScalarBar { bar_open_time: 0, value: 10.0, filled: false });
470 assert_eq!(out[1], ScalarBar { bar_open_time: 60_000, value: 10.0, filled: true });
472 assert_eq!(out[2], ScalarBar { bar_open_time: 120_000, value: 20.0, filled: false });
474 assert_eq!(out[3], ScalarBar { bar_open_time: 180_000, value: 20.0, filled: true });
476 }
477
478 #[test]
479 fn forward_fill_omits_leading_gap() {
480 let step = 60_000;
481 let src = vec![(150_000, 5.0)];
483 let out = resample(src, 0, 240_000, step, FillPolicy::ForwardFill);
484 assert_eq!(out.len(), 2); assert_eq!(out[0].bar_open_time, 120_000);
486 assert_eq!(out[0].value, 5.0);
487 assert!(!out[0].filled);
488 assert_eq!(out[1], ScalarBar { bar_open_time: 180_000, value: 5.0, filled: true });
489 }
490
491 #[test]
492 fn zero_flow_buckets_and_zeros_gaps() {
493 let step = 60_000;
494 let src = vec![(1_000, 3.0), (50_000, 4.0), (125_000, 9.0)];
496 let out = resample(src, 0, 180_000, step, FillPolicy::ZeroFlow);
497 assert_eq!(out.len(), 3);
498 assert_eq!(out[0], ScalarBar { bar_open_time: 0, value: 7.0, filled: false });
499 assert_eq!(out[1], ScalarBar { bar_open_time: 60_000, value: 0.0, filled: false });
500 assert_eq!(out[2], ScalarBar { bar_open_time: 120_000, value: 9.0, filled: false });
501 }
502
503 #[test]
504 fn bar_align_points_flow_from_recorded() {
505 use crate::data::LiquidationPoint;
506 let pts = vec![
508 LiquidationPoint { ts_ms: 1_000, price: 100.0, quantity: 1.0, value: 5_000.0, side: 0 },
509 LiquidationPoint { ts_ms: 40_000, price: 100.0, quantity: 1.0, value: 3_000.0, side: 1 },
510 LiquidationPoint { ts_ms: 125_000, price: 100.0, quantity: 1.0, value: 9_000.0, side: 0 },
511 ];
512 let out = bar_align_points(
513 &pts, |p| p.value, 0, 180_000, &KlineInterval::new("1m"), FillPolicy::ZeroFlow,
514 ).unwrap();
515 assert_eq!(out.len(), 3);
516 assert_eq!(out[0], ScalarBar { bar_open_time: 0, value: 8_000.0, filled: false });
517 assert_eq!(out[1], ScalarBar { bar_open_time: 60_000, value: 0.0, filled: false });
518 assert_eq!(out[2], ScalarBar { bar_open_time: 120_000, value: 9_000.0, filled: false });
519 }
520
521 #[test]
522 fn grid_aligns_to_interval_boundary() {
523 let step = 60_000;
524 let src = vec![(70_000, 1.0)];
526 let out = resample(src, 35_000, 130_000, step, FillPolicy::ForwardFill);
527 assert_eq!(out[0].bar_open_time, 60_000); assert_eq!(out[0].value, 1.0);
530 }
531}