Skip to main content

digdigdig3_station/series/
key.rs

1use std::time::Duration;
2
3use digdigdig3::core::types::{AccountType, ExchangeId};
4use digdigdig3::core::websocket::KlineInterval;
5
6/// What kind of stream this series carries.
7///
8/// `Kline` carries a typed `KlineInterval` so different timeframes of the
9/// same symbol get their own series. All other kinds have no extra parameter.
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub enum Kind {
12    Trade,
13    AggTrade,
14    Kline(KlineInterval),
15    Ticker,
16    Orderbook,
17    OrderbookDelta,
18    MarkPrice,
19    FundingRate,
20    OpenInterest,
21    Liquidation,
22    // --- extended stream types ---
23    BlockTrade,
24    IndexPrice,
25    CompositeIndex,
26    OptionGreeks,
27    VolatilityIndex,
28    HistoricalVolatility,
29    LongShortRatio,
30    Basis,
31    InsuranceFund,
32    OrderbookL3,
33    SettlementEvent,
34    MarketWarning,
35    RiskLimit,
36    PredictedFunding,
37    FundingSettlement,
38    MarkPriceKline(KlineInterval),
39    IndexPriceKline(KlineInterval),
40    PremiumIndexKline(KlineInterval),
41    // --- private (auth-required) stream types ---
42    /// Order lifecycle events (create/fill/cancel/expire).  Auth-required.
43    OrderUpdate,
44    /// Account balance changes.  Auth-required.
45    BalanceUpdate,
46    /// Futures position changes.  Auth-required.
47    PositionUpdate,
48}
49
50/// Polling cadence + anti-alignment jitter for REST-only stream kinds.
51///
52/// Returned by [`Kind::is_poll_only`] for kinds that have no WS feed and
53/// must be driven by periodic REST calls.
54#[derive(Debug, Clone, Copy)]
55pub struct PollSpec {
56    /// How often to call the REST endpoint.
57    pub cadence: Duration,
58    /// Jitter applied to the FIRST tick only, expressed as percent of cadence.
59    /// Prevents N symbols × M exchanges all calling REST at the same wall-clock
60    /// second. Value 10 means first tick fires at `cadence ± (cadence * 10 / 100)`.
61    pub jitter_pct: u8,
62}
63
64impl Kind {
65    /// True for stream kinds that are computed inside Station from upstream
66    /// WS-backed streams, rather than arriving directly from an exchange WS.
67    ///
68    /// Derived kinds bypass the `ws.subscribe(req)` path in `acquire_or_spawn`
69    /// and instead use `acquire_or_spawn_derived<D>(...)`.
70    pub(crate) fn is_derived(&self) -> bool {
71        matches!(self, Kind::Basis | Kind::FundingSettlement)
72    }
73
74    /// If this kind has no WS feed and must be driven by REST polling,
75    /// returns the default cadence + jitter spec.
76    ///
77    /// Poll-only kinds bypass `ws.subscribe` entirely in `acquire_or_spawn`
78    /// and instead use the `spawn_poller` actor path.
79    pub fn is_poll_only(&self) -> Option<PollSpec> {
80        match self {
81            Kind::LongShortRatio => Some(PollSpec {
82                cadence: Duration::from_secs(5 * 60), // 5 min bucket cadence
83                jitter_pct: 10,
84            }),
85            Kind::HistoricalVolatility => Some(PollSpec {
86                cadence: Duration::from_secs(60 * 60), // 1 h Deribit update cadence
87                jitter_pct: 5,
88            }),
89            _ => None,
90        }
91    }
92
93    /// Short kebab-case label for filesystem paths.
94    pub fn slug(&self) -> String {
95        match self {
96            Kind::Trade => "trades".to_string(),
97            Kind::AggTrade => "agg_trades".to_string(),
98            Kind::Kline(iv) => format!("klines_{}", iv.as_str()),
99            Kind::Ticker => "tickers".to_string(),
100            Kind::Orderbook => "orderbook_snapshots".to_string(),
101            Kind::OrderbookDelta => "orderbook_deltas".to_string(),
102            Kind::MarkPrice => "mark_price".to_string(),
103            Kind::FundingRate => "funding_rate".to_string(),
104            Kind::OpenInterest => "open_interest".to_string(),
105            Kind::Liquidation => "liquidations".to_string(),
106            Kind::BlockTrade => "block_trades".to_string(),
107            Kind::IndexPrice => "index_price".to_string(),
108            Kind::CompositeIndex => "composite_index".to_string(),
109            Kind::OptionGreeks => "option_greeks".to_string(),
110            Kind::VolatilityIndex => "volatility_index".to_string(),
111            Kind::HistoricalVolatility => "historical_volatility".to_string(),
112            Kind::LongShortRatio => "long_short_ratio".to_string(),
113            Kind::Basis => "basis".to_string(),
114            Kind::InsuranceFund => "insurance_fund".to_string(),
115            Kind::OrderbookL3 => "orderbook_l3".to_string(),
116            Kind::SettlementEvent => "settlement_events".to_string(),
117            Kind::MarketWarning => "market_warnings".to_string(),
118            Kind::RiskLimit => "risk_limit".to_string(),
119            Kind::PredictedFunding => "predicted_funding".to_string(),
120            Kind::FundingSettlement => "funding_settlement".to_string(),
121            Kind::MarkPriceKline(iv) => format!("mark_price_klines_{}", iv.as_str()),
122            Kind::IndexPriceKline(iv) => format!("index_price_klines_{}", iv.as_str()),
123            Kind::PremiumIndexKline(iv) => format!("premium_index_klines_{}", iv.as_str()),
124            Kind::OrderUpdate => "order_updates".to_string(),
125            Kind::BalanceUpdate => "balance_updates".to_string(),
126            Kind::PositionUpdate => "position_updates".to_string(),
127        }
128    }
129}
130
131/// Canonical series identity. Matches the MLC `BarSeriesKey` shape but is
132/// generalized over data-class via `kind`.
133#[derive(Debug, Clone, PartialEq, Eq, Hash)]
134pub struct SeriesKey {
135    pub exchange: ExchangeId,
136    pub account_type: AccountType,
137    /// Exchange-native raw symbol (already normalized via `SymbolNormalizer`).
138    pub symbol: String,
139    pub kind: Kind,
140}
141
142impl SeriesKey {
143    pub fn new(
144        exchange: ExchangeId,
145        account_type: AccountType,
146        symbol: impl Into<String>,
147        kind: Kind,
148    ) -> Self {
149        Self {
150            exchange,
151            account_type,
152            symbol: symbol.into(),
153            kind,
154        }
155    }
156
157    /// Filesystem-friendly account label (matches CLI `--account` spelling).
158    pub fn account_label(&self) -> &'static str {
159        match self.account_type {
160            AccountType::Spot => "spot",
161            AccountType::Margin => "margin",
162            AccountType::FuturesCross => "futures_cross",
163            AccountType::FuturesIsolated => "futures_isolated",
164            AccountType::Earn => "earn",
165            AccountType::Lending => "lending",
166            AccountType::Options => "options",
167            AccountType::Convert => "convert",
168        }
169    }
170
171    /// Lower-case exchange label.
172    pub fn exchange_label(&self) -> String {
173        format!("{:?}", self.exchange).to_lowercase()
174    }
175}