Skip to main content

afintech_sdk/
marketdata.rs

1use chrono::{DateTime, Utc};
2use rust_decimal::Decimal;
3use serde::{Deserialize, Serialize};
4use serde_repr::{Deserialize_repr, Serialize_repr};
5use std::fmt;
6
7// ---------------------------------------------------------------------------
8// Shared primitives (architect-compatible)
9// ---------------------------------------------------------------------------
10
11/// Trade/order direction.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
14pub enum Dir {
15    #[serde(alias = "Buy", alias = "buy")]
16    Buy,
17    #[serde(alias = "Sell", alias = "sell")]
18    Sell,
19}
20
21/// Sequence tracking for L2 book updates.
22#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23pub struct SequenceIdAndNumber {
24    #[serde(rename = "sid")]
25    pub sequence_id: u64,
26    #[serde(rename = "sn")]
27    pub sequence_number: u64,
28}
29
30impl SequenceIdAndNumber {
31    pub fn advance(&mut self) {
32        self.sequence_number += 1;
33    }
34}
35
36/// Candle bar width. Serializes as the integer number of seconds.
37#[derive(
38    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize_repr, Deserialize_repr,
39)]
40#[repr(i32)]
41pub enum CandleWidth {
42    OneSecond = 1,
43    FiveSecond = 5,
44    OneMinute = 60,
45    TwoMinute = 120,
46    ThreeMinute = 180,
47    FifteenMinute = 900,
48    OneHour = 3600,
49    OneDay = 86400,
50}
51
52impl CandleWidth {
53    pub fn as_seconds(&self) -> i64 {
54        *self as i64
55    }
56
57    pub fn as_str(&self) -> &'static str {
58        match self {
59            Self::OneSecond => "1s",
60            Self::FiveSecond => "5s",
61            Self::OneMinute => "1m",
62            Self::TwoMinute => "2m",
63            Self::ThreeMinute => "3m",
64            Self::FifteenMinute => "15m",
65            Self::OneHour => "1h",
66            Self::OneDay => "1d",
67        }
68    }
69
70    #[allow(clippy::should_implement_trait)]
71    pub fn from_str(s: &str) -> Option<Self> {
72        match s {
73            "1s" => Some(Self::OneSecond),
74            "5s" => Some(Self::FiveSecond),
75            "1m" => Some(Self::OneMinute),
76            "2m" => Some(Self::TwoMinute),
77            "3m" => Some(Self::ThreeMinute),
78            "15m" => Some(Self::FifteenMinute),
79            "1h" => Some(Self::OneHour),
80            "1d" => Some(Self::OneDay),
81            _ => None,
82        }
83    }
84}
85
86// ---------------------------------------------------------------------------
87// Marketdata wire types (architect-compatible)
88// ---------------------------------------------------------------------------
89
90/// Top-of-book snapshot.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct L1BookSnapshot {
93    #[serde(rename = "s")]
94    pub symbol: String,
95    #[serde(rename = "ts")]
96    pub timestamp: i64,
97    #[serde(rename = "tn")]
98    pub timestamp_ns: u32,
99    #[serde(default, rename = "rt", skip_serializing_if = "Option::is_none")]
100    pub recv_time: Option<i64>,
101    #[serde(default, rename = "rtn", skip_serializing_if = "Option::is_none")]
102    pub recv_time_ns: Option<u32>,
103    /// (price, quantity)
104    #[serde(rename = "b")]
105    pub best_bid: Option<(Decimal, Decimal)>,
106    /// (price, quantity)
107    #[serde(rename = "a")]
108    pub best_ask: Option<(Decimal, Decimal)>,
109}
110
111/// Full L2 book snapshot.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct L2BookSnapshot {
114    #[serde(rename = "ts")]
115    pub timestamp: i64,
116    #[serde(rename = "tn")]
117    pub timestamp_ns: u32,
118    #[serde(flatten)]
119    pub sequence: SequenceIdAndNumber,
120    /// Vec of (price, quantity) levels.
121    #[serde(rename = "b")]
122    pub bids: Vec<(Decimal, Decimal)>,
123    #[serde(rename = "a")]
124    pub asks: Vec<(Decimal, Decimal)>,
125}
126
127/// Incremental L2 book update. A size of 0 at a price level means remove.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct L2BookDiff {
130    #[serde(rename = "ts")]
131    pub timestamp: i64,
132    #[serde(rename = "tn")]
133    pub timestamp_ns: u32,
134    #[serde(flatten)]
135    pub sequence: SequenceIdAndNumber,
136    /// (price, quantity) updates. Quantity 0 = remove level.
137    #[serde(rename = "b")]
138    pub bids: Vec<(Decimal, Decimal)>,
139    #[serde(rename = "a")]
140    pub asks: Vec<(Decimal, Decimal)>,
141}
142
143/// L2 book update — either a full snapshot or an incremental diff.
144#[derive(Debug, Clone, Serialize, Deserialize)]
145#[serde(tag = "t")]
146pub enum L2BookUpdate {
147    #[serde(rename = "s")]
148    Snapshot(L2BookSnapshot),
149    #[serde(rename = "d")]
150    Diff(L2BookDiff),
151}
152
153/// A single trade execution.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct Trade {
156    #[serde(rename = "s")]
157    pub symbol: String,
158    #[serde(rename = "ts")]
159    pub timestamp: i64,
160    #[serde(rename = "tn")]
161    pub timestamp_ns: u32,
162    #[serde(rename = "d")]
163    pub direction: Option<Dir>,
164    #[serde(rename = "p")]
165    pub price: Decimal,
166    #[serde(rename = "q")]
167    pub size: Decimal,
168}
169
170/// OHLCV candle bar.
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172pub struct Candle {
173    #[serde(rename = "ts")]
174    pub timestamp: i64,
175    #[serde(rename = "tn")]
176    pub timestamp_ns: u32,
177    #[serde(rename = "w")]
178    pub width: CandleWidth,
179    #[serde(rename = "s")]
180    pub symbol: String,
181    #[serde(rename = "o")]
182    pub open: Option<Decimal>,
183    #[serde(rename = "h")]
184    pub high: Option<Decimal>,
185    #[serde(rename = "l")]
186    pub low: Option<Decimal>,
187    #[serde(rename = "c")]
188    pub close: Option<Decimal>,
189    #[serde(rename = "v")]
190    pub volume: Decimal,
191    #[serde(rename = "bv")]
192    pub buy_volume: Decimal,
193    #[serde(rename = "av")]
194    pub sell_volume: Decimal,
195    #[serde(default, rename = "mo", skip_serializing_if = "Option::is_none")]
196    pub mid_open: Option<Decimal>,
197    #[serde(default, rename = "mc", skip_serializing_if = "Option::is_none")]
198    pub mid_close: Option<Decimal>,
199    #[serde(default, rename = "mh", skip_serializing_if = "Option::is_none")]
200    pub mid_high: Option<Decimal>,
201    #[serde(default, rename = "ml", skip_serializing_if = "Option::is_none")]
202    pub mid_low: Option<Decimal>,
203    #[serde(default, rename = "bo", skip_serializing_if = "Option::is_none")]
204    pub bid_open: Option<Decimal>,
205    #[serde(default, rename = "bc", skip_serializing_if = "Option::is_none")]
206    pub bid_close: Option<Decimal>,
207    #[serde(default, rename = "bh", skip_serializing_if = "Option::is_none")]
208    pub bid_high: Option<Decimal>,
209    #[serde(default, rename = "bl", skip_serializing_if = "Option::is_none")]
210    pub bid_low: Option<Decimal>,
211    #[serde(default, rename = "ao", skip_serializing_if = "Option::is_none")]
212    pub ask_open: Option<Decimal>,
213    #[serde(default, rename = "ac", skip_serializing_if = "Option::is_none")]
214    pub ask_close: Option<Decimal>,
215    #[serde(default, rename = "ah", skip_serializing_if = "Option::is_none")]
216    pub ask_high: Option<Decimal>,
217    #[serde(default, rename = "al", skip_serializing_if = "Option::is_none")]
218    pub ask_low: Option<Decimal>,
219}
220
221impl Candle {
222    pub fn timestamp(&self) -> Option<DateTime<Utc>> {
223        DateTime::<Utc>::from_timestamp(self.timestamp, self.timestamp_ns)
224    }
225}
226
227// ---------------------------------------------------------------------------
228// Timestamp helpers
229// ---------------------------------------------------------------------------
230
231pub fn split_timestamp(dt: DateTime<Utc>) -> (i64, u32) {
232    (dt.timestamp(), dt.timestamp_subsec_nanos())
233}
234
235// ---------------------------------------------------------------------------
236// WebSocket protocol types (brokerage-specific, not architect)
237// ---------------------------------------------------------------------------
238
239/// WebSocket channel identifier.
240#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
241#[serde(rename_all = "snake_case")]
242pub enum Channel {
243    L1,
244    L2,
245    Trades,
246    Candles,
247}
248
249/// Messages sent from the client to the server over WebSocket.
250#[derive(Debug, Clone, Serialize, Deserialize)]
251#[serde(tag = "type", rename_all = "snake_case")]
252pub enum ClientMessage {
253    Subscribe {
254        channel: Channel,
255        #[serde(skip_serializing_if = "Option::is_none")]
256        symbols: Option<Vec<String>>,
257        /// Required when `channel` is `Candles`.
258        #[serde(skip_serializing_if = "Option::is_none")]
259        width: Option<CandleWidth>,
260    },
261    Unsubscribe {
262        channel: Channel,
263        #[serde(skip_serializing_if = "Option::is_none")]
264        symbols: Option<Vec<String>>,
265        #[serde(skip_serializing_if = "Option::is_none")]
266        width: Option<CandleWidth>,
267    },
268}
269
270/// Messages sent from the server to the client over WebSocket.
271/// Wraps the architect-compatible wire types with a `type` discriminator
272/// and adds a `symbol` field for L2 messages (which don't carry one natively).
273#[derive(Debug, Clone, Serialize, Deserialize)]
274#[serde(tag = "type", rename_all = "snake_case")]
275pub enum ServerMessage {
276    L1(L1BookSnapshot),
277    L2Diff {
278        #[serde(rename = "s")]
279        symbol: String,
280        #[serde(flatten)]
281        diff: L2BookDiff,
282    },
283    L2Snapshot {
284        #[serde(rename = "s")]
285        symbol: String,
286        #[serde(flatten)]
287        snapshot: L2BookSnapshot,
288    },
289    Trade(Trade),
290    Candle(Box<Candle>),
291}
292
293impl ServerMessage {
294    pub fn symbol(&self) -> &str {
295        match self {
296            Self::L1(l1) => &l1.symbol,
297            Self::L2Diff { symbol, .. } => symbol,
298            Self::L2Snapshot { symbol, .. } => symbol,
299            Self::Trade(t) => &t.symbol,
300            Self::Candle(c) => &c.symbol,
301        }
302    }
303}
304
305// ---------------------------------------------------------------------------
306// gRPC request/response types (architect-compatible)
307// ---------------------------------------------------------------------------
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
310#[serde(transparent)]
311pub struct MarketdataVenue(pub String);
312
313impl fmt::Display for MarketdataVenue {
314    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315        f.write_str(&self.0)
316    }
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct L1BookSnapshotRequest {
321    #[serde(default, skip_serializing_if = "Option::is_none")]
322    pub venue: Option<MarketdataVenue>,
323    pub symbol: String,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct L1BookSnapshotsRequest {
328    #[serde(default, skip_serializing_if = "Option::is_none")]
329    pub venue: Option<MarketdataVenue>,
330    #[serde(default, skip_serializing_if = "Option::is_none")]
331    pub symbols: Option<Vec<String>>,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct SubscribeL1BookSnapshotsRequest {
336    #[serde(default, skip_serializing_if = "Option::is_none")]
337    pub venue: Option<MarketdataVenue>,
338    #[serde(default, skip_serializing_if = "Option::is_none")]
339    pub symbols: Option<Vec<String>>,
340    #[serde(default)]
341    pub send_initial_snapshots: bool,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct L2BookSnapshotRequest {
346    #[serde(default, skip_serializing_if = "Option::is_none")]
347    pub venue: Option<MarketdataVenue>,
348    pub symbol: String,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct SubscribeL2BookUpdatesRequest {
353    #[serde(default, skip_serializing_if = "Option::is_none")]
354    pub venue: Option<MarketdataVenue>,
355    pub symbol: String,
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct SubscribeTradesRequest {
360    #[serde(default, skip_serializing_if = "Option::is_none")]
361    pub venue: Option<MarketdataVenue>,
362    #[serde(default, skip_serializing_if = "Option::is_none")]
363    pub symbol: Option<String>,
364}
365
366#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct SubscribeCandlesRequest {
368    #[serde(default, skip_serializing_if = "Option::is_none")]
369    pub venue: Option<MarketdataVenue>,
370    pub symbol: String,
371    #[serde(default, skip_serializing_if = "Option::is_none")]
372    pub candle_widths: Option<Vec<CandleWidth>>,
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct SubscribeManyCandlesRequest {
377    #[serde(default, skip_serializing_if = "Option::is_none")]
378    pub venue: Option<MarketdataVenue>,
379    #[serde(default, skip_serializing_if = "Option::is_none")]
380    pub symbols: Option<Vec<String>>,
381    pub candle_width: CandleWidth,
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct HistoricalCandlesRequest {
386    #[serde(default, skip_serializing_if = "Option::is_none")]
387    pub venue: Option<MarketdataVenue>,
388    pub symbol: String,
389    pub candle_width: CandleWidth,
390    pub start_date: DateTime<Utc>,
391    pub end_date: DateTime<Utc>,
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct HistoricalCandlesResponse {
396    pub candles: Vec<Candle>,
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct MarketStatusRequest {
401    #[serde(default, skip_serializing_if = "Option::is_none")]
402    pub venue: Option<MarketdataVenue>,
403    pub symbol: String,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct MarketStatus {
408    #[serde(rename = "s")]
409    pub symbol: String,
410    #[serde(default, skip_serializing_if = "Option::is_none")]
411    pub is_trading: Option<bool>,
412    #[serde(default, skip_serializing_if = "Option::is_none")]
413    pub is_quoting: Option<bool>,
414}
415
416#[derive(Default, Debug, Clone, Serialize, Deserialize)]
417pub struct TickerValues {
418    #[serde(default, rename = "bp", skip_serializing_if = "Option::is_none")]
419    pub bid_price: Option<Decimal>,
420    #[serde(default, rename = "bs", skip_serializing_if = "Option::is_none")]
421    pub bid_size: Option<Decimal>,
422    #[serde(default, rename = "ap", skip_serializing_if = "Option::is_none")]
423    pub ask_price: Option<Decimal>,
424    #[serde(default, rename = "as", skip_serializing_if = "Option::is_none")]
425    pub ask_size: Option<Decimal>,
426    #[serde(default, rename = "p", skip_serializing_if = "Option::is_none")]
427    pub last_price: Option<Decimal>,
428    #[serde(default, rename = "q", skip_serializing_if = "Option::is_none")]
429    pub last_size: Option<Decimal>,
430    #[serde(default, rename = "xo", skip_serializing_if = "Option::is_none")]
431    pub session_open: Option<Decimal>,
432    #[serde(default, rename = "xl", skip_serializing_if = "Option::is_none")]
433    pub session_low: Option<Decimal>,
434    #[serde(default, rename = "xh", skip_serializing_if = "Option::is_none")]
435    pub session_high: Option<Decimal>,
436    #[serde(default, rename = "xv", skip_serializing_if = "Option::is_none")]
437    pub session_volume: Option<Decimal>,
438    #[serde(default, rename = "o", skip_serializing_if = "Option::is_none")]
439    pub open_24h: Option<Decimal>,
440    #[serde(default, rename = "l", skip_serializing_if = "Option::is_none")]
441    pub low_24h: Option<Decimal>,
442    #[serde(default, rename = "h", skip_serializing_if = "Option::is_none")]
443    pub high_24h: Option<Decimal>,
444    #[serde(default, rename = "v", skip_serializing_if = "Option::is_none")]
445    pub volume_24h: Option<Decimal>,
446    #[serde(default, rename = "vm", skip_serializing_if = "Option::is_none")]
447    pub volume_30d: Option<Decimal>,
448    #[serde(default, rename = "oi", skip_serializing_if = "Option::is_none")]
449    pub open_interest: Option<Decimal>,
450    #[serde(default, rename = "sp", skip_serializing_if = "Option::is_none")]
451    pub last_settlement_price: Option<Decimal>,
452    #[serde(default, rename = "sd", skip_serializing_if = "Option::is_none")]
453    pub last_settlement_date: Option<chrono::NaiveDate>,
454    #[serde(default, rename = "isp", skip_serializing_if = "Option::is_none")]
455    pub indicative_settlement_price: Option<Decimal>,
456    #[serde(default, rename = "mp", skip_serializing_if = "Option::is_none")]
457    pub mark_price: Option<Decimal>,
458    #[serde(default, rename = "ip", skip_serializing_if = "Option::is_none")]
459    pub index_price: Option<Decimal>,
460    #[serde(default, rename = "fr", skip_serializing_if = "Option::is_none")]
461    pub funding_rate: Option<Decimal>,
462    #[serde(default, rename = "ft", skip_serializing_if = "Option::is_none")]
463    pub next_funding_time: Option<DateTime<Utc>>,
464    #[serde(default, skip_serializing_if = "Option::is_none")]
465    pub market_cap: Option<Decimal>,
466    #[serde(default, skip_serializing_if = "Option::is_none")]
467    pub price_to_earnings: Option<Decimal>,
468    #[serde(default, skip_serializing_if = "Option::is_none")]
469    pub eps_adj: Option<Decimal>,
470    #[serde(default, skip_serializing_if = "Option::is_none")]
471    pub shares_outstanding_weighted_adj: Option<Decimal>,
472    #[serde(default, skip_serializing_if = "Option::is_none")]
473    pub dividend: Option<Decimal>,
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub dividend_yield: Option<Decimal>,
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize)]
479pub struct Ticker {
480    #[serde(rename = "s")]
481    pub symbol: String,
482    #[serde(rename = "ve")]
483    pub venue: MarketdataVenue,
484    #[serde(rename = "ts")]
485    pub timestamp: i64,
486    #[serde(rename = "tn")]
487    pub timestamp_ns: u32,
488    #[serde(flatten)]
489    pub values: TickerValues,
490}
491
492#[derive(Debug, Clone, Serialize, Deserialize)]
493pub struct TickerRequest {
494    #[serde(default, skip_serializing_if = "Option::is_none")]
495    pub venue: Option<MarketdataVenue>,
496    pub symbol: String,
497}
498
499#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct TickersRequest {
501    #[serde(default, skip_serializing_if = "Option::is_none")]
502    pub venue: Option<MarketdataVenue>,
503    #[serde(default, skip_serializing_if = "Option::is_none")]
504    pub symbols: Option<Vec<String>>,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct TickersResponse {
509    pub tickers: Vec<Ticker>,
510}