use std::sync::Arc;
use digdigdig3::connector_manager::ExchangeHub;
use digdigdig3::core::types::{AccountType, ExchangeId, SymbolInput};
use digdigdig3::core::websocket::KlineInterval;
use crate::data::BarPoint;
use crate::error::{Result, StationError};
use crate::series::{Kind, SeriesKey};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FillPolicy {
ForwardFill,
ZeroFlow,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ScalarBar {
pub bar_open_time: i64,
pub value: f64,
pub filled: bool,
}
#[derive(Debug, Clone)]
pub enum BarAlignedSeries {
Klines(Vec<BarPoint>),
Scalar(Vec<ScalarBar>),
}
impl BarAlignedSeries {
pub fn len(&self) -> usize {
match self {
BarAlignedSeries::Klines(v) => v.len(),
BarAlignedSeries::Scalar(v) => v.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl Kind {
pub fn fill_policy(&self) -> FillPolicy {
match self {
Kind::Liquidation | Kind::AggTrade | Kind::Trade => FillPolicy::ZeroFlow,
_ => FillPolicy::ForwardFill,
}
}
}
fn interval_millis(iv: &str) -> Option<i64> {
let iv = iv.trim();
let (num, unit) = iv.split_at(iv.len().saturating_sub(1));
let n: i64 = num.parse().ok()?;
let per = match unit {
"m" => 60_000,
"h" => 60 * 60_000,
"d" | "D" => 24 * 60 * 60_000,
"w" | "W" => 7 * 24 * 60 * 60_000,
_ => return None,
};
Some(n * per)
}
fn resample(mut src: Vec<(i64, f64)>, start: i64, end: i64, step: i64, policy: FillPolicy) -> Vec<ScalarBar> {
src.sort_unstable_by_key(|(ts, _)| *ts);
let first_bar = start.div_euclid(step) * step;
let mut out = Vec::new();
match policy {
FillPolicy::ForwardFill => {
let mut idx = 0usize;
let mut last: Option<f64> = None;
let mut t = first_bar;
while t < end {
let bar_close = t + step;
let mut fresh = false;
while idx < src.len() && src[idx].0 < bar_close {
last = Some(src[idx].1);
if src[idx].0 >= t {
fresh = true;
}
idx += 1;
}
if let Some(v) = last {
out.push(ScalarBar { bar_open_time: t, value: v, filled: !fresh });
}
t += step;
}
}
FillPolicy::ZeroFlow => {
let mut idx = 0usize;
let mut t = first_bar;
while t < end {
let bar_close = t + step;
let mut sum = 0.0;
while idx < src.len() && src[idx].0 < bar_close {
if src[idx].0 >= t {
sum += src[idx].1;
}
idx += 1;
}
out.push(ScalarBar { bar_open_time: t, value: sum, filled: false });
t += step;
}
}
}
out
}
pub fn bar_align_points<T, F>(
points: &[T],
project: F,
start_ms: i64,
end_ms: i64,
interval: &KlineInterval,
policy: FillPolicy,
) -> Result<Vec<ScalarBar>>
where
T: crate::series::DataPoint,
F: Fn(&T) -> f64,
{
let step = interval_millis(interval.as_str())
.ok_or_else(|| StationError::Core(format!("interval {interval} has no fixed ms width for resample")))?;
let src: Vec<(i64, f64)> = points
.iter()
.filter(|p| {
let t = p.timestamp_ms();
t >= start_ms && t < end_ms
})
.map(|p| (p.timestamp_ms(), project(p)))
.collect();
Ok(resample(src, start_ms, end_ms, step, policy))
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn bar_align_from_disk<T, F>(
store: &crate::series::DiskStore<T>,
project: F,
start_ms: i64,
end_ms: i64,
interval: &KlineInterval,
policy: FillPolicy,
max_points: usize,
) -> Result<Vec<ScalarBar>>
where
T: crate::series::DataPoint,
F: Fn(&T) -> f64,
{
let pts = store
.read_tail(max_points)
.await
.map_err(|e| StationError::Core(format!("DiskStore read_tail failed: {e}")))?;
bar_align_points(&pts, project, start_ms, end_ms, interval, policy)
}
const KLINE_PAGE: u16 = 1000;
async fn paginate_klines<F, Fut>(start: i64, end: i64, mut fetch: F) -> Result<Vec<BarPoint>>
where
F: FnMut(i64) -> Fut,
Fut: std::future::Future<Output = Result<Vec<BarPoint>>>,
{
let mut all: Vec<BarPoint> = Vec::new();
let mut cursor = end;
for _ in 0..256 {
let page = match fetch(cursor).await {
Ok(p) => p,
Err(_) => fetch(cursor).await?,
};
if page.is_empty() {
break;
}
let oldest = page.iter().map(|b| b.open_time).min().unwrap_or(cursor);
all.extend(page);
if oldest <= start {
break;
}
let next = oldest - 1;
if next >= cursor {
break; }
cursor = next;
}
all.retain(|b| b.open_time >= start && b.open_time < end);
all.sort_unstable_by_key(|b| b.open_time);
all.dedup_by_key(|b| b.open_time);
Ok(all)
}
pub async fn load_bar_aligned(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
kind: &Kind,
interval: &KlineInterval,
start_ms: i64,
end_ms: i64,
) -> Result<BarAlignedSeries> {
let rest = hub
.rest(exchange)
.ok_or_else(|| StationError::Core(format!("{exchange:?} not connected in hub")))?;
if let Some(klines) = match kind {
Kind::Kline(iv) => Some(("kline", iv.clone())),
Kind::MarkPriceKline(iv) => Some(("mark", iv.clone())),
Kind::IndexPriceKline(iv) => Some(("index", iv.clone())),
Kind::PremiumIndexKline(iv) => Some(("premium", iv.clone())),
_ => None,
} {
let (which, iv) = klines;
let rest = rest.clone();
let sym = symbol.to_string();
let bars = paginate_klines(start_ms, end_ms, |cursor| {
let rest = rest.clone();
let sym = sym.clone();
let ivs = iv.as_str().to_string();
async move {
let res = match which {
"kline" => rest
.get_klines(SymbolInput::Raw(&sym), &ivs, Some(KLINE_PAGE), account, Some(cursor))
.await,
"mark" => rest
.get_mark_price_klines(SymbolInput::Raw(&sym), &ivs, Some(KLINE_PAGE as u32), account, Some(cursor))
.await,
"index" => rest
.get_index_price_klines(SymbolInput::Raw(&sym), &ivs, Some(KLINE_PAGE as u32), account, Some(cursor))
.await,
_ => rest
.get_premium_index_klines(SymbolInput::Raw(&sym), &ivs, Some(KLINE_PAGE as u32), account, Some(cursor))
.await,
};
res.map(|ks| ks.iter().map(BarPoint::from_kline).collect())
.map_err(|e| StationError::Core(format!("{which} klines failed: {e}")))
}
})
.await?;
return Ok(BarAlignedSeries::Klines(bars));
}
let step = interval_millis(interval.as_str())
.ok_or_else(|| StationError::Core(format!("interval {interval} has no fixed ms width for resample")))?;
let src: Vec<(i64, f64)> = match kind {
Kind::FundingRate => rest
.get_funding_rate_history(SymbolInput::Raw(symbol), Some(start_ms), Some(end_ms), Some(1000), account)
.await
.map_err(|e| StationError::Core(format!("funding history failed: {e}")))?
.into_iter()
.map(|f| (f.timestamp, f.rate))
.collect(),
Kind::OpenInterest => rest
.get_open_interest_history(SymbolInput::Raw(symbol), interval.as_str(), Some(start_ms), Some(end_ms), Some(500), account)
.await
.map_err(|e| StationError::Core(format!("open interest history failed: {e}")))?
.into_iter()
.map(|oi| (oi.timestamp, oi.open_interest))
.collect(),
Kind::Basis => rest
.get_basis_history(SymbolInput::Raw(symbol), interval.as_str(), Some(start_ms), Some(end_ms), Some(500), account)
.await
.map_err(|e| StationError::Core(format!("basis history failed: {e}")))?
.into_iter()
.map(|b| (b.timestamp, b.basis))
.collect(),
Kind::LongShortRatio => rest
.get_long_short_ratio_history(SymbolInput::Raw(symbol), interval.as_str(), Some(start_ms), Some(end_ms), Some(500), account)
.await
.map_err(|e| StationError::Core(format!("long/short ratio history failed: {e}")))?
.into_iter()
.map(|r| {
let v = r.ratio.unwrap_or_else(|| {
if r.short_ratio > 0.0 { r.long_ratio / r.short_ratio } else { r.long_ratio }
});
(r.timestamp, v)
})
.collect(),
Kind::MarkPrice | Kind::IndexPrice => {
let which = if matches!(kind, Kind::MarkPrice) { "mark" } else { "index" };
let rest2 = rest.clone();
let sym = symbol.to_string();
let ivs = interval.as_str().to_string();
let bars = paginate_klines(start_ms, end_ms, |cursor| {
let rest2 = rest2.clone();
let sym = sym.clone();
let ivs = ivs.clone();
async move {
let res = if which == "mark" {
rest2.get_mark_price_klines(SymbolInput::Raw(&sym), &ivs, Some(KLINE_PAGE as u32), account, Some(cursor)).await
} else {
rest2.get_index_price_klines(SymbolInput::Raw(&sym), &ivs, Some(KLINE_PAGE as u32), account, Some(cursor)).await
};
res.map(|ks| ks.iter().map(BarPoint::from_kline).collect())
.map_err(|e| StationError::Core(format!("{which} klines failed: {e}")))
}
})
.await?;
let scalars = bars
.into_iter()
.map(|b| ScalarBar { bar_open_time: b.open_time, value: b.close, filled: false })
.collect();
return Ok(BarAlignedSeries::Scalar(scalars));
}
Kind::Liquidation | Kind::AggTrade => {
return Err(StationError::StreamNotSupported(format!(
"{kind:?} bar-aligned history needs the recording daemon (no usable REST history)"
)));
}
other => {
return Err(StationError::StreamNotSupported(format!(
"bar-aligned loader does not yet support {other:?}"
)));
}
};
Ok(BarAlignedSeries::Scalar(resample(src, start_ms, end_ms, step, kind.fill_policy())))
}
pub async fn load_for_key(
hub: &Arc<ExchangeHub>,
key: &SeriesKey,
interval: &KlineInterval,
start_ms: i64,
end_ms: i64,
) -> Result<BarAlignedSeries> {
let iv = match &key.kind {
Kind::Kline(iv) | Kind::MarkPriceKline(iv) | Kind::IndexPriceKline(iv) | Kind::PremiumIndexKline(iv) => iv.clone(),
_ => interval.clone(),
};
load_bar_aligned(hub, key.exchange, key.account_type, &key.symbol, &key.kind, &iv, start_ms, end_ms).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interval_parsing() {
assert_eq!(interval_millis("1m"), Some(60_000));
assert_eq!(interval_millis("5m"), Some(300_000));
assert_eq!(interval_millis("1h"), Some(3_600_000));
assert_eq!(interval_millis("4h"), Some(14_400_000));
assert_eq!(interval_millis("1d"), Some(86_400_000));
assert_eq!(interval_millis("1w"), Some(604_800_000));
assert_eq!(interval_millis("1M"), None);
assert_eq!(interval_millis("xx"), None);
}
#[test]
fn forward_fill_carries_into_empty_bars() {
let step = 60_000;
let src = vec![(0, 10.0), (130_000, 20.0)];
let out = resample(src, 0, 240_000, step, FillPolicy::ForwardFill);
assert_eq!(out.len(), 4);
assert_eq!(out[0], ScalarBar { bar_open_time: 0, value: 10.0, filled: false });
assert_eq!(out[1], ScalarBar { bar_open_time: 60_000, value: 10.0, filled: true });
assert_eq!(out[2], ScalarBar { bar_open_time: 120_000, value: 20.0, filled: false });
assert_eq!(out[3], ScalarBar { bar_open_time: 180_000, value: 20.0, filled: true });
}
#[test]
fn forward_fill_omits_leading_gap() {
let step = 60_000;
let src = vec![(150_000, 5.0)];
let out = resample(src, 0, 240_000, step, FillPolicy::ForwardFill);
assert_eq!(out.len(), 2); assert_eq!(out[0].bar_open_time, 120_000);
assert_eq!(out[0].value, 5.0);
assert!(!out[0].filled);
assert_eq!(out[1], ScalarBar { bar_open_time: 180_000, value: 5.0, filled: true });
}
#[test]
fn zero_flow_buckets_and_zeros_gaps() {
let step = 60_000;
let src = vec![(1_000, 3.0), (50_000, 4.0), (125_000, 9.0)];
let out = resample(src, 0, 180_000, step, FillPolicy::ZeroFlow);
assert_eq!(out.len(), 3);
assert_eq!(out[0], ScalarBar { bar_open_time: 0, value: 7.0, filled: false });
assert_eq!(out[1], ScalarBar { bar_open_time: 60_000, value: 0.0, filled: false });
assert_eq!(out[2], ScalarBar { bar_open_time: 120_000, value: 9.0, filled: false });
}
#[test]
fn bar_align_points_flow_from_recorded() {
use crate::data::LiquidationPoint;
let pts = vec![
LiquidationPoint { ts_ms: 1_000, price: 100.0, quantity: 1.0, value: 5_000.0, side: 0 },
LiquidationPoint { ts_ms: 40_000, price: 100.0, quantity: 1.0, value: 3_000.0, side: 1 },
LiquidationPoint { ts_ms: 125_000, price: 100.0, quantity: 1.0, value: 9_000.0, side: 0 },
];
let out = bar_align_points(
&pts, |p| p.value, 0, 180_000, &KlineInterval::new("1m"), FillPolicy::ZeroFlow,
).unwrap();
assert_eq!(out.len(), 3);
assert_eq!(out[0], ScalarBar { bar_open_time: 0, value: 8_000.0, filled: false });
assert_eq!(out[1], ScalarBar { bar_open_time: 60_000, value: 0.0, filled: false });
assert_eq!(out[2], ScalarBar { bar_open_time: 120_000, value: 9_000.0, filled: false });
}
#[test]
fn grid_aligns_to_interval_boundary() {
let step = 60_000;
let src = vec![(70_000, 1.0)];
let out = resample(src, 35_000, 130_000, step, FillPolicy::ForwardFill);
assert_eq!(out[0].bar_open_time, 60_000); assert_eq!(out[0].value, 1.0);
}
}