use std::sync::Arc;
use digdigdig3::connector_manager::ExchangeHub;
use digdigdig3::core::types::{
AccountType, AggTrade, ExchangeId, FundingRate, HistoryCursor, InsuranceFund, Liquidation,
MarkPrice, OpenInterest, SymbolInput, TradeHistoryTier, TradeSide,
};
use digdigdig3::core::websocket::KlineInterval;
use crate::data::{
AggTradePoint, BarPoint, FundingRatePoint, FundingRateIndicatorsPoint, FundingRateFullPoint,
IndexPriceKlinePoint, InsuranceFundPoint,
LiquidationPoint, LiquidationIndicatorsPoint, LiquidationFullPoint,
MarkPriceKlinePoint, MarkPricePoint, MarkPriceIndicatorsPoint, MarkPriceFullPoint,
OpenInterestPoint, OpenInterestIndicatorsPoint,
PremiumIndexKlinePoint, TickerIndicatorsPoint, TickerFullPoint, TradePoint,
};
use crate::error::{Result, StationError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeedSource {
AggTradesPaginated,
RecentTradesFallback,
KlineSynthetic,
Klines,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TruncationReason {
VenueRecentOnly,
VenueWindowCap,
VenueHistoryExhausted,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SeedOutcome {
pub requested: usize,
pub achieved: usize,
pub source: SeedSource,
pub truncated_by: Option<TruncationReason>,
}
impl SeedOutcome {
pub const fn full(requested: usize, achieved: usize, source: SeedSource) -> Self {
Self { requested, achieved, source, truncated_by: None }
}
pub const fn truncated(
requested: usize,
achieved: usize,
source: SeedSource,
reason: TruncationReason,
) -> Self {
Self { requested, achieved, source, truncated_by: Some(reason) }
}
pub fn is_satisfied(&self) -> bool {
self.truncated_by.is_none() || self.achieved >= self.requested
}
}
pub async fn trades_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
limit: usize,
) -> (Vec<TradePoint>, SeedOutcome) {
let Some(rest) = hub.rest(exchange) else {
return (
Vec::new(),
SeedOutcome::truncated(limit, 0, SeedSource::RecentTradesFallback, TruncationReason::Error),
);
};
let cap = recent_only_cap(rest.trade_history_capabilities().tier_for(account));
let effective_limit = limit.min(cap.unwrap_or(1000)).min(1000).max(1);
let res = rest
.get_recent_trades(SymbolInput::Raw(symbol), Some(effective_limit as u32), account)
.await;
match res {
Ok(trades) => {
let points: Vec<TradePoint> = trades.iter().map(TradePoint::from_public).collect();
let achieved = points.len();
let outcome = if cap.is_some_and(|c| limit > c) {
SeedOutcome::truncated(limit, achieved, SeedSource::RecentTradesFallback, TruncationReason::VenueRecentOnly)
} else {
SeedOutcome::full(limit, achieved, SeedSource::RecentTradesFallback)
};
(points, outcome)
}
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, "rest backfill trades failed");
(
Vec::new(),
SeedOutcome::truncated(limit, 0, SeedSource::RecentTradesFallback, TruncationReason::Error),
)
}
}
}
fn recent_only_cap(tier: TradeHistoryTier) -> Option<usize> {
match tier {
TradeHistoryTier::RecentOnly { max_trades } => Some(max_trades as usize),
_ => None,
}
}
pub async fn fetch_history(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
symbol: &str,
account_type: AccountType,
interval: &KlineInterval,
end_time_ms: i64,
limit: u16,
) -> Result<Vec<BarPoint>> {
let rest = hub
.rest(exchange)
.ok_or_else(|| StationError::Core(format!("{exchange:?} not connected in hub")))?;
let limit = limit.min(1000).max(1);
let bars = rest
.get_klines(
SymbolInput::Raw(symbol),
interval.as_str(),
Some(limit),
account_type,
Some(end_time_ms),
)
.await
.map_err(|e| StationError::Core(format!("get_klines failed: {e}")))?;
let mut points: Vec<BarPoint> = bars.iter().map(BarPoint::from_kline).collect();
points.sort_unstable_by_key(|p| p.open_time);
Ok(points)
}
pub async fn klines_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
interval: &str,
limit: usize,
) -> Vec<BarPoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
let limit = limit.min(1000).max(1) as u16;
let res = rest
.get_klines(
SymbolInput::Raw(symbol),
interval,
Some(limit),
account,
None,
)
.await;
match res {
Ok(bars) => bars.iter().map(BarPoint::from_kline).collect(),
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, interval, "rest backfill klines failed");
Vec::new()
}
}
}
pub async fn klines_paginated(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
interval: &str,
page_size: usize,
n_pages: usize,
) -> (Vec<BarPoint>, SeedOutcome) {
use std::collections::BTreeMap;
let requested = page_size.saturating_mul(n_pages);
if n_pages == 0 || page_size == 0 {
return (Vec::new(), SeedOutcome::full(0, 0, SeedSource::Klines));
}
let Some(rest) = hub.rest(exchange) else {
return (Vec::new(), SeedOutcome::truncated(requested, 0, SeedSource::Klines, TruncationReason::Error));
};
let limit = page_size.min(1000).max(1) as u16;
let kline_backpage = rest.trade_history_capabilities().kline_backpage;
let effective_n_pages = if kline_backpage { n_pages } else { 1 };
let mut by_open_time: BTreeMap<i64, BarPoint> = BTreeMap::new();
let mut next_end_time: Option<i64> = Some(chrono::Utc::now().timestamp_millis());
let mut pages_done = 0usize;
let mut had_error = false;
while pages_done < effective_n_pages {
let res = rest
.get_klines(SymbolInput::Raw(symbol), interval, Some(limit), account, next_end_time)
.await;
let page: Vec<BarPoint> = match res {
Ok(bars) => bars.iter().map(BarPoint::from_kline).collect(),
Err(e) => {
tracing::debug!(
?e,
exchange = ?exchange,
interval,
page = pages_done,
"klines_paginated page failed; stopping at partial result"
);
had_error = true;
break;
}
};
if page.is_empty() {
break;
}
let min_open_time = page.iter().map(|p| p.open_time).min().unwrap_or(0);
let was_partial = page.len() < limit as usize;
for p in page {
by_open_time.entry(p.open_time).or_insert(p);
}
pages_done += 1;
if was_partial {
break;
}
next_end_time = Some(min_open_time);
}
let achieved = by_open_time.len();
let outcome = if had_error {
SeedOutcome::truncated(requested, achieved, SeedSource::Klines, TruncationReason::Error)
} else if !kline_backpage && n_pages > 1 {
SeedOutcome::truncated(requested, achieved, SeedSource::Klines, TruncationReason::VenueWindowCap)
} else if pages_done < effective_n_pages {
SeedOutcome::truncated(requested, achieved, SeedSource::Klines, TruncationReason::VenueHistoryExhausted)
} else {
SeedOutcome::full(requested, achieved, SeedSource::Klines)
};
(by_open_time.into_values().collect(), outcome)
}
pub async fn agg_trades_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
limit: usize,
) -> (Vec<AggTradePoint>, SeedOutcome) {
let Some(rest) = hub.rest(exchange) else {
return (Vec::new(), SeedOutcome::truncated(limit, 0, SeedSource::AggTradesPaginated, TruncationReason::Error));
};
let effective_limit = limit.min(1000).max(1) as u32;
let res = rest
.get_agg_trades(SymbolInput::Raw(symbol), Some(effective_limit), None, account)
.await;
match res {
Ok(trades) => {
let points: Vec<AggTradePoint> = trades.iter().map(agg_trade_point_from).collect();
let achieved = points.len();
let outcome = SeedOutcome::full(limit, achieved, SeedSource::AggTradesPaginated);
(points, outcome)
}
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, "rest backfill agg_trades failed");
(Vec::new(), SeedOutcome::truncated(limit, 0, SeedSource::AggTradesPaginated, TruncationReason::Error))
}
}
}
pub async fn agg_trades_paginated(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
page_size: usize,
n_pages: usize,
) -> (Vec<AggTradePoint>, SeedOutcome) {
use std::collections::BTreeMap;
let requested = page_size.saturating_mul(n_pages);
if n_pages == 0 || page_size == 0 {
return (Vec::new(), SeedOutcome::full(0, 0, SeedSource::AggTradesPaginated));
}
let Some(rest) = hub.rest(exchange) else {
return (Vec::new(), SeedOutcome::truncated(requested, 0, SeedSource::AggTradesPaginated, TruncationReason::Error));
};
let limit = page_size.min(1000).max(1) as u32;
let tier = rest.trade_history_capabilities().tier_for(account);
if let TradeHistoryTier::RecentOnly { max_trades } = tier {
let cap = (max_trades as usize).min(limit as usize).max(1);
let (trades, _inner_outcome) = trades_recent(hub, exchange, account, symbol, cap).await;
let achieved = trades.len();
let points: Vec<AggTradePoint> = trades
.into_iter()
.enumerate()
.map(|(i, t)| AggTradePoint {
ts_ms: t.ts_ms,
price: t.price,
quantity: t.quantity,
side: t.side,
agg_id: i as u64,
})
.collect();
return (
points,
SeedOutcome::truncated(requested, achieved, SeedSource::RecentTradesFallback, TruncationReason::VenueRecentOnly),
);
}
let window_wall_ms: Option<i64> = match tier {
TradeHistoryTier::RestWindow { max_back_ms, .. } if max_back_ms > 0 => {
Some(chrono::Utc::now().timestamp_millis() - max_back_ms as i64)
}
_ => None,
};
let cursor_kind = match tier {
TradeHistoryTier::RestDeep { cursor } => cursor,
TradeHistoryTier::RestWindow { cursor, .. } => cursor,
_ => HistoryCursor::FromId,
};
let is_ts_window = cursor_kind == HistoryCursor::TsWindow;
let mut by_id: BTreeMap<u64, AggTradePoint> = BTreeMap::new();
let mut next_from_id: Option<u64> = None;
let mut pages_done = 0usize;
let mut agg_fallback = false;
let mut hit_window_wall = false;
while pages_done < n_pages {
let res = rest
.get_agg_trades(SymbolInput::Raw(symbol), Some(limit), next_from_id, account)
.await;
let page: Vec<AggTradePoint> = match res {
Ok(trades) => trades.iter().map(agg_trade_point_from).collect(),
Err(e) => {
if pages_done == 0 {
tracing::debug!(
?e,
exchange = ?exchange,
"agg_trades_paginated first page failed; falling back to trades_recent"
);
agg_fallback = true;
} else {
tracing::debug!(
?e,
exchange = ?exchange,
page = pages_done,
"agg_trades_paginated page failed; stopping at partial result"
);
}
break;
}
};
if page.is_empty() {
break;
}
let min_id = page.iter().map(|p| p.agg_id).min().unwrap_or(0);
let min_ts = page.iter().map(|p| p.ts_ms).min().unwrap_or(i64::MAX);
let was_partial = page.len() < limit as usize;
for p in page {
by_id.entry(p.agg_id).or_insert(p);
}
pages_done += 1;
if let Some(wall) = window_wall_ms {
if min_ts <= wall {
hit_window_wall = true;
break;
}
}
if was_partial {
break;
}
if is_ts_window {
if min_id == 0 {
break;
}
next_from_id = Some(min_id - 1);
} else {
if min_id <= limit as u64 {
break;
}
next_from_id = Some(min_id.saturating_sub(limit as u64));
}
}
if agg_fallback {
let (trades, _inner_outcome) = trades_recent(hub, exchange, account, symbol, limit as usize).await;
let achieved = trades.len();
let points: Vec<AggTradePoint> = trades
.into_iter()
.enumerate()
.map(|(i, t)| AggTradePoint {
ts_ms: t.ts_ms,
price: t.price,
quantity: t.quantity,
side: t.side,
agg_id: i as u64,
})
.collect();
return (
points,
SeedOutcome::truncated(requested, achieved, SeedSource::RecentTradesFallback, TruncationReason::Error),
);
}
let achieved = by_id.len();
let outcome = if hit_window_wall {
SeedOutcome::truncated(requested, achieved, SeedSource::AggTradesPaginated, TruncationReason::VenueWindowCap)
} else if pages_done < n_pages {
SeedOutcome::truncated(requested, achieved, SeedSource::AggTradesPaginated, TruncationReason::VenueHistoryExhausted)
} else {
SeedOutcome::full(requested, achieved, SeedSource::AggTradesPaginated)
};
(by_id.into_values().collect(), outcome)
}
fn agg_trade_point_from(t: &AggTrade) -> AggTradePoint {
let side = if t.is_buy { 0u8 } else { 1u8 };
AggTradePoint {
ts_ms: t.timestamp,
price: t.price,
quantity: t.quantity,
side,
agg_id: t.aggregate_id as u64,
}
}
pub async fn open_interest_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
limit: usize,
) -> Vec<OpenInterestPoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
let limit = limit.min(1000).max(1) as u32;
let res = rest
.get_open_interest_history(
SymbolInput::Raw(symbol),
"5m",
None,
None,
Some(limit),
account,
)
.await;
match res {
Ok(items) => items.iter().map(oi_point_from).collect(),
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, "rest backfill open_interest failed");
Vec::new()
}
}
}
fn oi_point_from(oi: &OpenInterest) -> OpenInterestPoint {
OpenInterestPoint {
ts_ms: oi.timestamp,
open_interest: oi.open_interest,
open_interest_value: oi.open_interest_value.unwrap_or(f64::NAN),
}
}
pub async fn mark_price_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
_limit: usize,
) -> Vec<MarkPricePoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
let res = rest
.get_premium_index(Some(SymbolInput::Raw(symbol)), account)
.await;
match res {
Ok(items) => items.iter().map(mark_price_point_from).collect(),
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, "rest backfill mark_price failed");
Vec::new()
}
}
}
fn mark_price_point_from(mp: &MarkPrice) -> MarkPricePoint {
MarkPricePoint {
ts_ms: mp.timestamp,
mark: mp.mark_price,
index: mp.index_price.unwrap_or(f64::NAN),
}
}
pub async fn funding_rate_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
limit: usize,
) -> Vec<FundingRatePoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
let limit = limit.min(1000).max(1) as u32;
let res = rest
.get_funding_rate_history(SymbolInput::Raw(symbol), None, None, Some(limit), account)
.await;
match res {
Ok(items) => items.iter().map(funding_rate_point_from).collect(),
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, "rest backfill funding_rate failed");
Vec::new()
}
}
}
fn funding_rate_point_from(fr: &FundingRate) -> FundingRatePoint {
FundingRatePoint {
ts_ms: fr.timestamp,
rate: fr.rate,
next_funding_time_ms: fr.next_funding_time.unwrap_or(0),
}
}
pub async fn liquidations_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
limit: usize,
) -> Vec<LiquidationPoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
let limit = limit.min(1000).max(1) as u32;
let res = rest
.get_liquidation_history(
Some(SymbolInput::Raw(symbol)),
None,
None,
Some(limit),
account,
)
.await;
match res {
Ok(items) => items.iter().map(liquidation_point_from).collect(),
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, "rest backfill liquidations failed");
Vec::new()
}
}
}
fn liquidation_point_from(liq: &Liquidation) -> LiquidationPoint {
let side = match liq.side {
TradeSide::Buy => 0u8,
TradeSide::Sell => 1u8,
};
LiquidationPoint {
ts_ms: liq.timestamp,
price: liq.price,
quantity: liq.quantity,
value: liq.value.unwrap_or(f64::NAN),
side,
}
}
pub async fn insurance_fund_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
_limit: usize,
) -> Vec<InsuranceFundPoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
let res = rest
.get_insurance_fund(Some(SymbolInput::Raw(symbol)), account)
.await;
match res {
Ok(items) => items.iter().map(insurance_fund_point_from).collect(),
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, "rest backfill insurance_fund failed");
Vec::new()
}
}
}
fn insurance_fund_point_from(fund: &InsuranceFund) -> InsuranceFundPoint {
InsuranceFundPoint {
ts_ms: fund.timestamp,
balance: fund.balance,
}
}
pub async fn mark_price_klines_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
interval: &str,
limit: usize,
) -> Vec<MarkPriceKlinePoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
let limit = limit.min(1000).max(1) as u32;
let res = rest
.get_mark_price_klines(SymbolInput::Raw(symbol), interval, Some(limit), account, None)
.await;
match res {
Ok(bars) => bars.iter().map(|k| MarkPriceKlinePoint {
open_time: k.open_time,
open: k.open,
high: k.high,
low: k.low,
close: k.close,
volume: k.volume,
quote_volume: k.quote_volume.unwrap_or(f64::NAN),
trades_count: k.trades.unwrap_or(0),
}).collect(),
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, interval, "rest backfill mark_price_klines failed");
Vec::new()
}
}
}
pub async fn index_price_klines_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
interval: &str,
limit: usize,
) -> Vec<IndexPriceKlinePoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
let limit = limit.min(1000).max(1) as u32;
let res = rest
.get_index_price_klines(SymbolInput::Raw(symbol), interval, Some(limit), account, None)
.await;
match res {
Ok(bars) => bars.iter().map(|k| IndexPriceKlinePoint {
open_time: k.open_time,
open: k.open,
high: k.high,
low: k.low,
close: k.close,
volume: k.volume,
quote_volume: k.quote_volume.unwrap_or(f64::NAN),
trades_count: k.trades.unwrap_or(0),
}).collect(),
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, interval, "rest backfill index_price_klines failed");
Vec::new()
}
}
}
pub async fn premium_index_klines_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
interval: &str,
limit: usize,
) -> Vec<PremiumIndexKlinePoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
let limit = limit.min(1000).max(1) as u32;
let res = rest
.get_premium_index_klines(SymbolInput::Raw(symbol), interval, Some(limit), account, None)
.await;
match res {
Ok(bars) => bars.iter().map(|k| PremiumIndexKlinePoint {
open_time: k.open_time,
open: k.open,
high: k.high,
low: k.low,
close: k.close,
volume: k.volume,
quote_volume: k.quote_volume.unwrap_or(f64::NAN),
trades_count: k.trades.unwrap_or(0),
}).collect(),
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, interval, "rest backfill premium_index_klines failed");
Vec::new()
}
}
}
pub async fn tickers_indicators_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
_limit: usize,
) -> Vec<TickerIndicatorsPoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
let sym = SymbolInput::Raw(symbol);
match rest.get_ticker(sym, account).await {
Ok(t) => vec![TickerIndicatorsPoint::from_ticker(&t)],
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, "rest backfill ticker_indicators failed");
Vec::new()
}
}
}
pub async fn tickers_full_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
_limit: usize,
) -> Vec<TickerFullPoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
let sym = SymbolInput::Raw(symbol);
match rest.get_ticker(sym, account).await {
Ok(t) => vec![TickerFullPoint::from_ticker(&t)],
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, "rest backfill ticker_full failed");
Vec::new()
}
}
}
pub async fn mark_price_indicators_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
_limit: usize,
) -> Vec<MarkPriceIndicatorsPoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
match rest.get_premium_index(Some(SymbolInput::Raw(symbol)), account).await {
Ok(items) => items.iter().map(MarkPriceIndicatorsPoint::from_mark_price).collect(),
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, "rest backfill mark_price_indicators failed");
Vec::new()
}
}
}
pub async fn mark_price_full_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
_limit: usize,
) -> Vec<MarkPriceFullPoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
match rest.get_premium_index(Some(SymbolInput::Raw(symbol)), account).await {
Ok(items) => items.iter().map(MarkPriceFullPoint::from_mark_price).collect(),
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, "rest backfill mark_price_full failed");
Vec::new()
}
}
}
pub async fn funding_rate_indicators_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
limit: usize,
) -> Vec<FundingRateIndicatorsPoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
let limit = limit.min(1000).max(1) as u32;
match rest.get_funding_rate_history(SymbolInput::Raw(symbol), None, None, Some(limit), account).await {
Ok(items) => items.iter().map(FundingRateIndicatorsPoint::from_funding_rate).collect(),
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, "rest backfill funding_rate_indicators failed");
Vec::new()
}
}
}
pub async fn funding_rate_full_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
limit: usize,
) -> Vec<FundingRateFullPoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
let limit = limit.min(1000).max(1) as u32;
match rest.get_funding_rate_history(SymbolInput::Raw(symbol), None, None, Some(limit), account).await {
Ok(items) => items.iter().map(FundingRateFullPoint::from_funding_rate).collect(),
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, "rest backfill funding_rate_full failed");
Vec::new()
}
}
}
pub async fn open_interest_indicators_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
limit: usize,
) -> Vec<OpenInterestIndicatorsPoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
let limit = limit.min(1000).max(1) as u32;
match rest.get_open_interest_history(
SymbolInput::Raw(symbol),
"5m",
None,
None,
Some(limit),
account,
).await {
Ok(items) => items.iter().map(OpenInterestIndicatorsPoint::from_open_interest).collect(),
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, "rest backfill open_interest_indicators failed");
Vec::new()
}
}
}
pub async fn liquidation_indicators_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
limit: usize,
) -> Vec<LiquidationIndicatorsPoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
let limit = limit.min(1000).max(1) as u32;
match rest.get_liquidation_history(
Some(SymbolInput::Raw(symbol)),
None,
None,
Some(limit),
account,
).await {
Ok(items) => items.iter().map(LiquidationIndicatorsPoint::from_liquidation).collect(),
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, "rest backfill liquidation_indicators failed");
Vec::new()
}
}
}
pub async fn liquidation_full_recent(
hub: &Arc<ExchangeHub>,
exchange: ExchangeId,
account: AccountType,
symbol: &str,
limit: usize,
) -> Vec<LiquidationFullPoint> {
let Some(rest) = hub.rest(exchange) else { return Vec::new(); };
let limit = limit.min(1000).max(1) as u32;
match rest.get_liquidation_history(
Some(SymbolInput::Raw(symbol)),
None,
None,
Some(limit),
account,
).await {
Ok(items) => items.iter().map(LiquidationFullPoint::from_liquidation).collect(),
Err(e) => {
tracing::debug!(?e, exchange = ?exchange, "rest backfill liquidation_full failed");
Vec::new()
}
}
}
#[cfg(test)]
mod seed_outcome_tests {
use super::*;
use digdigdig3::core::types::HistoryCursor;
use std::sync::Arc;
#[test]
fn seed_outcome_full_reports_requested_and_achieved() {
let o = SeedOutcome::full(1000, 1000, SeedSource::AggTradesPaginated);
assert_eq!(o.requested, 1000);
assert_eq!(o.achieved, 1000);
assert_eq!(o.source, SeedSource::AggTradesPaginated);
assert!(o.truncated_by.is_none());
assert!(o.is_satisfied());
}
#[test]
fn seed_outcome_truncated_reports_short_achieved_and_reason() {
let o = SeedOutcome::truncated(50_000, 60, SeedSource::RecentTradesFallback, TruncationReason::VenueRecentOnly);
assert_eq!(o.requested, 50_000);
assert_eq!(o.achieved, 60);
assert_eq!(o.truncated_by, Some(TruncationReason::VenueRecentOnly));
assert!(!o.is_satisfied());
}
#[test]
fn seed_outcome_truncated_but_fully_achieved_is_satisfied() {
let o = SeedOutcome::truncated(1000, 1000, SeedSource::AggTradesPaginated, TruncationReason::VenueHistoryExhausted);
assert!(o.is_satisfied());
}
#[test]
fn recent_only_cap_returns_max_trades_for_recent_only_tier() {
let tier = TradeHistoryTier::RecentOnly { max_trades: 60 };
assert_eq!(recent_only_cap(tier), Some(60));
}
#[test]
fn recent_only_cap_is_none_for_paginating_tiers() {
assert_eq!(recent_only_cap(TradeHistoryTier::RestDeep { cursor: HistoryCursor::FromId }), None);
assert_eq!(
recent_only_cap(TradeHistoryTier::RestWindow { cursor: HistoryCursor::FromId, max_back_ms: 86_400_000 }),
None,
);
assert_eq!(
recent_only_cap(TradeHistoryTier::FileDump { url_pattern: "https://example.test/%s.csv.gz", since_ms: 0 }),
None,
);
}
#[test]
fn tier_for_dispatches_futures_account_types_to_futures_tier() {
use digdigdig3::core::types::{AccountType, TradeHistoryCapabilities};
let caps = TradeHistoryCapabilities {
spot: TradeHistoryTier::RestDeep { cursor: HistoryCursor::FromId },
futures: TradeHistoryTier::RestWindow { cursor: HistoryCursor::FromId, max_back_ms: 86_400_000 },
kline_backpage: true,
};
assert_eq!(caps.tier_for(AccountType::FuturesCross), caps.futures);
assert_eq!(caps.tier_for(AccountType::FuturesIsolated), caps.futures);
assert_eq!(caps.tier_for(AccountType::Spot), caps.spot);
assert_eq!(caps.tier_for(AccountType::Margin), caps.spot);
}
#[tokio::test]
async fn trades_recent_on_unconnected_hub_reports_error_truncation() {
let hub = Arc::new(ExchangeHub::new());
let (points, outcome) = trades_recent(&hub, ExchangeId::Binance, AccountType::Spot, "BTCUSDT", 500).await;
assert!(points.is_empty());
assert_eq!(outcome.requested, 500);
assert_eq!(outcome.achieved, 0);
assert_eq!(outcome.truncated_by, Some(TruncationReason::Error));
}
#[tokio::test]
async fn agg_trades_paginated_on_unconnected_hub_reports_error_truncation() {
let hub = Arc::new(ExchangeHub::new());
let (points, outcome) = agg_trades_paginated(&hub, ExchangeId::Binance, AccountType::FuturesCross, "BTCUSDT", 1000, 5).await;
assert!(points.is_empty());
assert_eq!(outcome.requested, 5000);
assert_eq!(outcome.achieved, 0);
assert_eq!(outcome.truncated_by, Some(TruncationReason::Error));
}
#[tokio::test]
async fn klines_paginated_on_unconnected_hub_reports_error_truncation() {
let hub = Arc::new(ExchangeHub::new());
let (points, outcome) = klines_paginated(&hub, ExchangeId::Binance, AccountType::FuturesCross, "BTCUSDT", "1m", 1000, 3).await;
assert!(points.is_empty());
assert_eq!(outcome.requested, 3000);
assert_eq!(outcome.achieved, 0);
assert_eq!(outcome.truncated_by, Some(TruncationReason::Error));
}
#[tokio::test]
async fn klines_paginated_zero_pages_is_full_empty_not_error() {
let hub = Arc::new(ExchangeHub::new());
let (points, outcome) = klines_paginated(&hub, ExchangeId::Binance, AccountType::Spot, "BTCUSDT", "1m", 1000, 0).await;
assert!(points.is_empty());
assert_eq!(outcome.requested, 0);
assert!(outcome.truncated_by.is_none());
}
}