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    // --- derived bar aggregators (always computed from Stream::Trade) ---
42    /// Range bar: a new bar opens when |trade.price − bar_open| ≥ range.
43    ///
44    /// `range` is expressed as a **fixed-point integer: price × 1e8**.
45    /// Example: a $1.00 range on a dollar-denominated pair = `100_000_000u64`.
46    /// This avoids carrying floats in `Hash`/`Eq` while keeping the unit
47    /// explicit and independent of the minimum exchange tick.
48    RangeBar(u64),
49    /// Tick bar: a new bar closes every `n` trades.
50    TickBar(u32),
51    /// Volume bar: a new bar closes when cumulative volume ≥ threshold.
52    ///
53    /// `threshold` is expressed as a **fixed-point integer: volume × 1e8**.
54    /// Example: 0.5 BTC threshold = `50_000_000u64`.
55    VolumeBar(u64),
56    /// Footprint bar: time-bucketed OHLCV with per-price buy/sell breakdown.
57    ///
58    /// Reuses `KlineInterval` for the time bucket (e.g. `"1m"`, `"5m"`).
59    Footprint(KlineInterval),
60    // --- private (auth-required) stream types ---
61    /// Order lifecycle events (create/fill/cancel/expire).  Auth-required.
62    OrderUpdate,
63    /// Account balance changes.  Auth-required.
64    BalanceUpdate,
65    /// Futures position changes.  Auth-required.
66    PositionUpdate,
67}
68
69/// Polling cadence + anti-alignment jitter for REST-only stream kinds.
70///
71/// Returned by [`Kind::is_poll_only`] for kinds that have no WS feed and
72/// must be driven by periodic REST calls.
73#[derive(Debug, Clone, Copy)]
74pub struct PollSpec {
75    /// How often to call the REST endpoint.
76    pub cadence: Duration,
77    /// Jitter applied to the FIRST tick only, expressed as percent of cadence.
78    /// Prevents N symbols × M exchanges all calling REST at the same wall-clock
79    /// second. Value 10 means first tick fires at `cadence ± (cadence * 10 / 100)`.
80    pub jitter_pct: u8,
81}
82
83impl Kind {
84    /// True for stream kinds that are computed inside Station from upstream
85    /// WS-backed streams, rather than arriving directly from an exchange WS.
86    ///
87    /// Derived kinds bypass the `ws.subscribe(req)` path in `acquire_or_spawn`
88    /// and instead use `acquire_or_spawn_derived<D>(...)`.
89    pub(crate) fn is_derived(&self) -> bool {
90        matches!(
91            self,
92            Kind::Basis
93            | Kind::FundingSettlement
94            | Kind::RangeBar(_)
95            | Kind::TickBar(_)
96            | Kind::VolumeBar(_)
97            | Kind::Footprint(_)
98        )
99    }
100
101    /// If this kind has no WS feed and must be driven by REST polling,
102    /// returns the default cadence + jitter spec.
103    ///
104    /// Poll-only kinds bypass `ws.subscribe` entirely in `acquire_or_spawn`
105    /// and instead use the `spawn_poller` actor path.
106    pub fn is_poll_only(&self) -> Option<PollSpec> {
107        match self {
108            Kind::LongShortRatio => Some(PollSpec {
109                cadence: Duration::from_secs(5 * 60), // 5 min bucket cadence
110                jitter_pct: 10,
111            }),
112            Kind::HistoricalVolatility => Some(PollSpec {
113                cadence: Duration::from_secs(60 * 60), // 1 h Deribit update cadence
114                jitter_pct: 5,
115            }),
116            _ => None,
117        }
118    }
119
120    /// Short kebab-case label for filesystem paths.
121    pub fn slug(&self) -> String {
122        match self {
123            Kind::Trade => "trades".to_string(),
124            Kind::AggTrade => "agg_trades".to_string(),
125            Kind::Kline(iv) => format!("klines_{}", iv.as_str()),
126            Kind::Ticker => "tickers".to_string(),
127            Kind::Orderbook => "orderbook_snapshots".to_string(),
128            Kind::OrderbookDelta => "orderbook_deltas".to_string(),
129            Kind::MarkPrice => "mark_price".to_string(),
130            Kind::FundingRate => "funding_rate".to_string(),
131            Kind::OpenInterest => "open_interest".to_string(),
132            Kind::Liquidation => "liquidations".to_string(),
133            Kind::BlockTrade => "block_trades".to_string(),
134            Kind::IndexPrice => "index_price".to_string(),
135            Kind::CompositeIndex => "composite_index".to_string(),
136            Kind::OptionGreeks => "option_greeks".to_string(),
137            Kind::VolatilityIndex => "volatility_index".to_string(),
138            Kind::HistoricalVolatility => "historical_volatility".to_string(),
139            Kind::LongShortRatio => "long_short_ratio".to_string(),
140            Kind::Basis => "basis".to_string(),
141            Kind::InsuranceFund => "insurance_fund".to_string(),
142            Kind::OrderbookL3 => "orderbook_l3".to_string(),
143            Kind::SettlementEvent => "settlement_events".to_string(),
144            Kind::MarketWarning => "market_warnings".to_string(),
145            Kind::RiskLimit => "risk_limit".to_string(),
146            Kind::PredictedFunding => "predicted_funding".to_string(),
147            Kind::FundingSettlement => "funding_settlement".to_string(),
148            Kind::MarkPriceKline(iv) => format!("mark_price_klines_{}", iv.as_str()),
149            Kind::IndexPriceKline(iv) => format!("index_price_klines_{}", iv.as_str()),
150            Kind::PremiumIndexKline(iv) => format!("premium_index_klines_{}", iv.as_str()),
151            Kind::RangeBar(r) => format!("range_bars_{r}"),
152            Kind::TickBar(n) => format!("tick_bars_{n}"),
153            Kind::VolumeBar(v) => format!("volume_bars_{v}"),
154            Kind::Footprint(iv) => format!("footprint_{}", iv.as_str()),
155            Kind::OrderUpdate => "order_updates".to_string(),
156            Kind::BalanceUpdate => "balance_updates".to_string(),
157            Kind::PositionUpdate => "position_updates".to_string(),
158        }
159    }
160}
161
162/// Canonical series identity. Matches the MLC `BarSeriesKey` shape but is
163/// generalized over data-class via `kind`.
164#[derive(Debug, Clone, PartialEq, Eq, Hash)]
165pub struct SeriesKey {
166    pub exchange: ExchangeId,
167    pub account_type: AccountType,
168    /// Exchange-native raw symbol (already normalized via `SymbolNormalizer`).
169    pub symbol: String,
170    pub kind: Kind,
171}
172
173impl SeriesKey {
174    pub fn new(
175        exchange: ExchangeId,
176        account_type: AccountType,
177        symbol: impl Into<String>,
178        kind: Kind,
179    ) -> Self {
180        Self {
181            exchange,
182            account_type,
183            symbol: symbol.into(),
184            kind,
185        }
186    }
187
188    /// Filesystem-friendly account label (matches CLI `--account` spelling).
189    pub fn account_label(&self) -> &'static str {
190        match self.account_type {
191            AccountType::Spot => "spot",
192            AccountType::Margin => "margin",
193            AccountType::FuturesCross => "futures_cross",
194            AccountType::FuturesIsolated => "futures_isolated",
195            AccountType::Earn => "earn",
196            AccountType::Lending => "lending",
197            AccountType::Options => "options",
198            AccountType::Convert => "convert",
199        }
200    }
201
202    /// Lower-case exchange label.
203    pub fn exchange_label(&self) -> String {
204        format!("{:?}", self.exchange).to_lowercase()
205    }
206}