Skip to main content

rust_okx/ws/model/
market.rs

1//! Market data channel models (`tickers`, `candles`, `trades`, `books`, etc.).
2//!
3//! Public channels; no authentication required.
4
5use serde::Deserialize;
6
7use super::ExtraFields;
8use crate::model::NumberString;
9
10/// Access element `index` from a JSON array, falling back to `NumberString::default()`.
11pub(super) fn array_value(values: &[NumberString], index: usize) -> NumberString {
12    values.get(index).cloned().unwrap_or_default()
13}
14
15/// `tickers` channel row.
16///
17/// OKX docs: <https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-tickers-channel>
18#[derive(Debug, Clone, Default, Deserialize)]
19#[serde(rename_all = "camelCase")]
20#[non_exhaustive]
21pub struct TickerUpdate {
22    /// Instrument type, e.g., `SPOT`, `SWAP`, `FUTURES`, `OPTION`.
23    #[serde(default)]
24    pub inst_type: String,
25    /// Instrument ID, e.g., `BTC-USDT`.
26    #[serde(default)]
27    pub inst_id: String,
28    /// Last traded price.
29    #[serde(default)]
30    pub last: NumberString,
31    /// Last traded size.
32    #[serde(default)]
33    pub last_sz: NumberString,
34    /// Best ask price.
35    #[serde(default)]
36    pub ask_px: NumberString,
37    /// Best ask size.
38    #[serde(default)]
39    pub ask_sz: NumberString,
40    /// Best bid price.
41    #[serde(default)]
42    pub bid_px: NumberString,
43    /// Best bid size.
44    #[serde(default)]
45    pub bid_sz: NumberString,
46    /// Open price over the last 24 hours.
47    #[serde(default)]
48    pub open24h: NumberString,
49    /// Highest price over the last 24 hours.
50    #[serde(default)]
51    pub high24h: NumberString,
52    /// Lowest price over the last 24 hours.
53    #[serde(default)]
54    pub low24h: NumberString,
55    /// Trading volume in quote currency over the last 24 hours.
56    #[serde(default)]
57    pub vol_ccy24h: NumberString,
58    /// Trading volume in base currency (or contracts for derivatives) over the last 24 hours.
59    #[serde(default)]
60    pub vol24h: NumberString,
61    /// Open price at the start of day (00:00 UTC).
62    #[serde(default)]
63    pub sod_utc0: NumberString,
64    /// Open price at the start of day (08:00 UTC+8).
65    #[serde(default)]
66    pub sod_utc8: NumberString,
67    /// Ticker push time (Unix milliseconds).
68    #[serde(default)]
69    pub ts: NumberString,
70    /// Unrecognized fields retained for forward compatibility.
71    #[serde(flatten, default)]
72    pub extra: ExtraFields,
73}
74
75/// `candle*` channel row represented by OKX's nine-element array.
76///
77/// OKX docs: <https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-candlesticks-channel>
78#[derive(Debug, Clone, Default, Deserialize)]
79#[serde(from = "Vec<NumberString>", rename_all = "camelCase")]
80#[non_exhaustive]
81pub struct CandleUpdate {
82    /// Opening time of the candlestick (Unix milliseconds).
83    pub ts: NumberString,
84    /// Open price.
85    pub o: NumberString,
86    /// Highest price.
87    pub h: NumberString,
88    /// Lowest price.
89    pub l: NumberString,
90    /// Close price.
91    pub c: NumberString,
92    /// Trading volume in contracts (derivatives) or base currency (SPOT/MARGIN).
93    pub vol: NumberString,
94    /// Trading volume in base currency (derivatives) or quote currency (SPOT/MARGIN).
95    pub vol_ccy: NumberString,
96    /// Trading volume in quote currency, e.g., `USDT` for `BTC-USDT` and `BTC-USDT-SWAP`,
97    /// `USD` for `BTC-USD-SWAP`.
98    pub vol_ccy_quote: NumberString,
99    /// Candlestick state: `0` incomplete (still forming), `1` completed.
100    pub confirm: NumberString,
101}
102
103impl From<Vec<NumberString>> for CandleUpdate {
104    fn from(values: Vec<NumberString>) -> Self {
105        Self {
106            ts: array_value(&values, 0),
107            o: array_value(&values, 1),
108            h: array_value(&values, 2),
109            l: array_value(&values, 3),
110            c: array_value(&values, 4),
111            vol: array_value(&values, 5),
112            vol_ccy: array_value(&values, 6),
113            vol_ccy_quote: array_value(&values, 7),
114            confirm: array_value(&values, 8),
115        }
116    }
117}
118
119/// `trades` channel row.
120///
121/// OKX docs: <https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-trades-channel>
122#[derive(Debug, Clone, Default, Deserialize)]
123#[serde(rename_all = "camelCase")]
124#[non_exhaustive]
125pub struct TradeUpdate {
126    /// Instrument ID, e.g., `BTC-USDT`.
127    #[serde(default)]
128    pub inst_id: String,
129    /// Trade ID assigned by OKX.
130    #[serde(default)]
131    pub trade_id: String,
132    /// Trade price.
133    #[serde(default)]
134    pub px: NumberString,
135    /// Trade size.
136    #[serde(default)]
137    pub sz: NumberString,
138    /// Trade side: `buy` or `sell`.
139    #[serde(default)]
140    pub side: String,
141    /// Trade source.
142    ///
143    /// Empty string for regular trades. `"1"` indicates a block trade.
144    #[serde(default)]
145    pub source: String,
146    /// Number of trades aggregated into this push (only applicable to the `trades` channel).
147    #[serde(default)]
148    pub count: NumberString,
149    /// Trade timestamp (Unix milliseconds).
150    #[serde(default)]
151    pub ts: NumberString,
152    /// Unrecognized fields retained for forward compatibility.
153    #[serde(flatten, default)]
154    pub extra: ExtraFields,
155}
156
157/// `trades-all` channel row.
158///
159/// OKX docs: <https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-all-trades-channel>
160#[derive(Debug, Clone, Default, Deserialize)]
161#[serde(rename_all = "camelCase")]
162#[non_exhaustive]
163pub struct AllTradeUpdate {
164    /// Instrument ID, e.g., `BTC-USDT`.
165    #[serde(default)]
166    pub inst_id: String,
167    /// Trade ID assigned by OKX.
168    #[serde(default)]
169    pub trade_id: String,
170    /// Trade price.
171    #[serde(default)]
172    pub px: NumberString,
173    /// Trade size.
174    ///
175    /// For spot, the unit is base currency; for FUTURES/SWAP/OPTION, the unit is contract.
176    #[serde(default)]
177    pub sz: NumberString,
178    /// Trade side: `buy` or `sell`.
179    #[serde(default)]
180    pub side: String,
181    /// Order source: `"0"` = normal, `"1"` = Enhanced Liquidity Program order.
182    #[serde(default)]
183    pub source: String,
184    /// Trade timestamp (Unix milliseconds).
185    #[serde(default)]
186    pub ts: NumberString,
187    /// Unrecognized fields retained for forward compatibility.
188    #[serde(flatten, default)]
189    pub extra: ExtraFields,
190}
191
192/// `option-trades` channel row.
193///
194/// OKX docs: <https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-option-trades-channel>
195#[derive(Debug, Clone, Default, Deserialize)]
196#[serde(rename_all = "camelCase")]
197#[non_exhaustive]
198pub struct OptionTradeUpdate {
199    /// Instrument ID, e.g., `BTC-USD-240329-40000-C`.
200    #[serde(default)]
201    pub inst_id: String,
202    /// Instrument family, e.g., `BTC-USD`.
203    #[serde(default)]
204    pub inst_family: String,
205    /// Trade ID assigned by OKX.
206    #[serde(default)]
207    pub trade_id: String,
208    /// Trade price.
209    #[serde(default)]
210    pub px: NumberString,
211    /// Trade size (number of contracts).
212    #[serde(default)]
213    pub sz: NumberString,
214    /// Trade side: `buy` or `sell`.
215    #[serde(default)]
216    pub side: String,
217    /// Option type: `C` (call) or `P` (put).
218    #[serde(default)]
219    pub opt_type: String,
220    /// Implied volatility at the fill price.
221    #[serde(default)]
222    pub fill_vol: NumberString,
223    /// Forward price at the time of the trade.
224    #[serde(default)]
225    pub fwd_px: NumberString,
226    /// Index price at the time of the trade.
227    #[serde(default)]
228    pub idx_px: NumberString,
229    /// Mark price at the time of the trade.
230    #[serde(default)]
231    pub mark_px: NumberString,
232    /// Trade timestamp (Unix milliseconds).
233    #[serde(default)]
234    pub ts: NumberString,
235    /// Unrecognized fields retained for forward compatibility.
236    #[serde(flatten, default)]
237    pub extra: ExtraFields,
238}
239
240/// `call-auction-details` channel row.
241///
242/// OKX docs: <https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-call-auction-details-channel>
243#[derive(Debug, Clone, Default, Deserialize)]
244#[serde(rename_all = "camelCase")]
245#[non_exhaustive]
246pub struct CallAuctionDetailsUpdate {
247    /// Instrument ID, e.g., `BTC-USDT`.
248    #[serde(default)]
249    pub inst_id: String,
250    /// Equilibrium price — the price at which the maximum volume can be matched.
251    #[serde(default)]
252    pub eq_px: NumberString,
253    /// Total matched volume at the equilibrium price.
254    #[serde(default)]
255    pub matched_sz: NumberString,
256    /// Unmatched volume remaining at the equilibrium price.
257    #[serde(default)]
258    pub unmatched_sz: NumberString,
259    /// Auction end time (Unix milliseconds).
260    #[serde(default)]
261    pub auction_end_time: NumberString,
262    /// Auction state.
263    ///
264    /// Documented values: `prepareStart`, `parallelTrading`, `callAuction`,
265    /// `cancelOrder`, `matchWaiting`, `matched`, `normal`.
266    #[serde(default)]
267    pub state: String,
268    /// Push time (Unix milliseconds).
269    #[serde(default)]
270    pub ts: NumberString,
271    /// Unrecognized fields retained for forward compatibility.
272    #[serde(flatten, default)]
273    pub extra: ExtraFields,
274}
275
276/// An order book push from `books`, `books5`, or tick-by-tick book channels.
277///
278/// OKX docs: <https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-order-book-channel>
279#[derive(Debug, Clone, Default, Deserialize)]
280#[serde(rename_all = "camelCase")]
281#[non_exhaustive]
282pub struct OrderBookUpdate {
283    /// Ask levels sorted from best (lowest) price to worst.
284    #[serde(default)]
285    pub asks: Vec<BookLevel>,
286    /// Bid levels sorted from best (highest) price to worst.
287    #[serde(default)]
288    pub bids: Vec<BookLevel>,
289    /// CRC32 checksum of the top-25 bid/ask levels for integrity verification.
290    #[serde(default)]
291    pub checksum: i64,
292    /// Sequence ID of the previous message; used to detect gaps.
293    ///
294    /// Only applicable to `books`, `books-l2-tbt`, and `books50-l2-tbt`.
295    #[serde(default)]
296    pub prev_seq_id: i64,
297    /// Sequence ID of the current message; monotonically increasing.
298    #[serde(default)]
299    pub seq_id: i64,
300    /// Order book generation time (Unix milliseconds).
301    #[serde(default)]
302    pub ts: NumberString,
303    /// Unrecognized fields retained for forward compatibility.
304    #[serde(flatten, default)]
305    pub extra: ExtraFields,
306}
307
308/// A single four-value WebSocket order-book level.
309#[derive(Debug, Clone, Default, Deserialize)]
310#[serde(from = "Vec<NumberString>", rename_all = "camelCase")]
311#[non_exhaustive]
312pub struct BookLevel {
313    /// The limit price of this order book level.
314    pub price: NumberString,
315
316    /// The total depth or size available at this price level (in coins or contracts).
317    pub size: NumberString,
318
319    /// The number of liquidation orders currently resting at this price level.
320    pub liquidated_order_count: NumberString,
321
322    /// The total number of individual orders making up the total size at this level.
323    pub order_count: NumberString,
324}
325
326impl From<Vec<NumberString>> for BookLevel {
327    fn from(values: Vec<NumberString>) -> Self {
328        Self {
329            price: array_value(&values, 0),
330            size: array_value(&values, 1),
331            liquidated_order_count: array_value(&values, 2),
332            order_count: array_value(&values, 3),
333        }
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn parses_ticker_and_retains_new_fields() {
343        let row: TickerUpdate = serde_json::from_str(
344            r#"{"instType":"SPOT","instId":"BTC-USDT","last":"1","ts":"2","futureField":"ok"}"#,
345        )
346        .unwrap();
347        assert_eq!(row.inst_id, "BTC-USDT");
348        assert_eq!(row.extra["futureField"], "ok");
349    }
350
351    #[test]
352    fn parses_market_candle_array() {
353        let row: CandleUpdate =
354            serde_json::from_str(r#"["1","2","3","4","5","6","7","8","1"]"#).unwrap();
355        assert_eq!(row.ts.as_str(), "1");
356        assert_eq!(row.vol.as_str(), "6");
357        assert_eq!(row.confirm.as_str(), "1");
358    }
359
360    #[test]
361    fn parses_all_trade_update() {
362        let row: AllTradeUpdate = serde_json::from_str(
363            r#"{"instId":"BTC-USDT","tradeId":"130639474","px":"42219.9","sz":"0.12060306","side":"buy","source":"0","ts":"1630048897897"}"#,
364        )
365        .unwrap();
366        assert_eq!(row.inst_id, "BTC-USDT");
367        assert_eq!(row.trade_id, "130639474");
368        assert_eq!(row.source, "0");
369        assert_eq!(row.ts.as_str(), "1630048897897");
370    }
371
372    #[test]
373    fn parses_option_trade_update() {
374        let row: OptionTradeUpdate = serde_json::from_str(
375            r#"{"fillVol":"0.5066007836914062","fwdPx":"16469.69928595038","idxPx":"16537.2","instFamily":"BTC-USD","instId":"BTC-USD-230224-18000-C","markPx":"0.04690107010619562","optType":"C","px":"0.045","side":"sell","sz":"2","tradeId":"38","ts":"1672286551080"}"#,
376        )
377        .unwrap();
378        assert_eq!(row.inst_id, "BTC-USD-230224-18000-C");
379        assert_eq!(row.inst_family, "BTC-USD");
380        assert_eq!(row.opt_type, "C");
381        assert_eq!(row.fill_vol.as_str(), "0.5066007836914062");
382    }
383}