Skip to main content

rust_okx/ws/model/
spread.rs

1//! Spread trading channel models (`sprd-orders`, `sprd-trades`) and spread-operation result rows.
2//!
3//! Mixed public and private channels.
4
5use serde::Deserialize;
6
7use super::ExtraFields;
8use crate::model::NumberString;
9
10/// Private `sprd-orders` channel row.
11///
12/// OKX docs: <https://www.okx.com/docs-v5/en/#spread-trading-websocket-sprd-orders-channel>
13#[derive(Debug, Clone, Default, Deserialize)]
14#[serde(rename_all = "camelCase")]
15#[non_exhaustive]
16pub struct SpreadOrderUpdate {
17    /// Spread ID, e.g., `BTC-USDT_BTC-USDT-SWAP`.
18    #[serde(default)]
19    pub sprd_id: String,
20    /// OKX-assigned order ID.
21    #[serde(default)]
22    pub ord_id: String,
23    /// Client-supplied order ID, if any.
24    #[serde(default)]
25    pub cl_ord_id: String,
26    /// Order tag.
27    #[serde(default)]
28    pub tag: String,
29    /// Order price.
30    #[serde(default)]
31    pub px: NumberString,
32    /// Order size (number of contracts).
33    #[serde(default)]
34    pub sz: NumberString,
35    /// Order type, e.g., `limit`, `post_only`, `ioc`, `fok`.
36    #[serde(default)]
37    pub ord_type: String,
38    /// Order side: `buy` or `sell`.
39    #[serde(default)]
40    pub side: String,
41    /// Fill size for the most recent fill of this push.
42    #[serde(default)]
43    pub fill_sz: NumberString,
44    /// Fill price for the most recent fill.
45    #[serde(default)]
46    pub fill_px: NumberString,
47    /// Trade ID of the most recent fill.
48    #[serde(default)]
49    pub trade_id: String,
50    /// Accumulated filled size.
51    #[serde(default)]
52    pub acc_fill_sz: NumberString,
53    /// Size pending to be filled.
54    #[serde(default)]
55    pub pending_fill_sz: NumberString,
56    /// Size pending to be settled.
57    #[serde(default)]
58    pub pending_settle_sz: NumberString,
59    /// Canceled size.
60    #[serde(default)]
61    pub canceled_sz: NumberString,
62    /// Average fill price.
63    #[serde(default)]
64    pub avg_px: NumberString,
65    /// Order state.
66    ///
67    /// Documented values: `live`, `partially_filled`, `filled`, `canceled`.
68    #[serde(default)]
69    pub state: String,
70    /// Source that triggered the cancellation.
71    #[serde(default)]
72    pub cancel_source: String,
73    /// Client-supplied request ID, echoed from the original operation request.
74    #[serde(default)]
75    pub req_id: String,
76    /// Result of the last amendment: `-1` failure, `0` success, `""` no amendment.
77    #[serde(default)]
78    pub amend_result: String,
79    /// Error code; `"0"` on success.
80    #[serde(default)]
81    pub code: String,
82    /// Error message; empty on success.
83    #[serde(default)]
84    pub msg: String,
85    /// Order creation time (Unix milliseconds).
86    #[serde(default)]
87    pub c_time: NumberString,
88    /// Last update time (Unix milliseconds).
89    #[serde(default)]
90    pub u_time: NumberString,
91    /// Unrecognized fields retained for forward compatibility.
92    #[serde(flatten, default)]
93    pub extra: ExtraFields,
94}
95
96/// Leg execution nested in a spread trade.
97#[derive(Debug, Clone, Default, Deserialize)]
98#[serde(rename_all = "camelCase")]
99#[non_exhaustive]
100pub struct SpreadTradeLeg {
101    /// Instrument ID of this leg, e.g., `BTC-USDT`.
102    #[serde(default)]
103    pub inst_id: String,
104    /// Leg fill price.
105    #[serde(default)]
106    pub px: NumberString,
107    /// Leg fill size in base currency.
108    #[serde(default)]
109    pub sz: NumberString,
110    /// Leg side: `buy` or `sell`.
111    #[serde(default)]
112    pub side: String,
113    /// Fill PnL for this leg (for closing fills; `""` otherwise).
114    #[serde(default)]
115    pub fill_pnl: NumberString,
116    /// Fee charged for this leg.
117    #[serde(default)]
118    pub fee: NumberString,
119    /// Leg fill size in contracts.
120    #[serde(default)]
121    pub sz_cont: NumberString,
122    /// Fee currency for this leg.
123    #[serde(default)]
124    pub fee_ccy: String,
125    /// Trade ID of this leg fill.
126    #[serde(default)]
127    pub trade_id: String,
128    /// Unrecognized fields retained for forward compatibility.
129    #[serde(flatten, default)]
130    pub extra: ExtraFields,
131}
132
133/// Private/public spread-trade channel row.
134///
135/// OKX docs: <https://www.okx.com/docs-v5/en/#spread-trading-websocket-sprd-trades-channel>
136#[derive(Debug, Clone, Default, Deserialize)]
137#[serde(rename_all = "camelCase")]
138#[non_exhaustive]
139pub struct SpreadTradeUpdate {
140    /// Spread ID, e.g., `BTC-USDT_BTC-USDT-SWAP`.
141    #[serde(default)]
142    pub sprd_id: String,
143    /// Trade ID assigned by OKX.
144    #[serde(default)]
145    pub trade_id: String,
146    /// OKX-assigned order ID.
147    #[serde(default)]
148    pub ord_id: String,
149    /// Client-supplied order ID.
150    #[serde(default)]
151    pub cl_ord_id: String,
152    /// Order tag.
153    #[serde(default)]
154    pub tag: String,
155    /// Fill price of this trade.
156    #[serde(default)]
157    pub fill_px: NumberString,
158    /// Fill size of this trade.
159    #[serde(default)]
160    pub fill_sz: NumberString,
161    /// Trade side: `buy` or `sell`.
162    #[serde(default)]
163    pub side: String,
164    /// Trade state: `filled` or `rejected`.
165    #[serde(default)]
166    pub state: String,
167    /// Liquidity role of this fill: `T` (taker) or `M` (maker).
168    #[serde(default)]
169    pub exec_type: String,
170    /// Trade timestamp (Unix milliseconds).
171    #[serde(default)]
172    pub ts: NumberString,
173    /// Per-leg execution details for this trade.
174    #[serde(default)]
175    pub legs: Vec<SpreadTradeLeg>,
176    /// Error code; `"0"` on success.
177    #[serde(default)]
178    pub code: String,
179    /// Error message; empty on success.
180    #[serde(default)]
181    pub msg: String,
182    /// Unrecognized fields retained for forward compatibility.
183    #[serde(flatten, default)]
184    pub extra: ExtraFields,
185}
186
187/// Public `sprd-public-trades` channel row.
188///
189/// OKX docs: <https://www.okx.com/docs-v5/en/#spread-trading-websocket-sprd-public-trades-channel>
190#[derive(Debug, Clone, Default, Deserialize)]
191#[serde(rename_all = "camelCase")]
192#[non_exhaustive]
193pub struct SpreadPublicTradeUpdate {
194    /// Spread ID, e.g., `BTC-USDT_BTC-USDT-SWAP`.
195    #[serde(default)]
196    pub sprd_id: String,
197    /// Trade ID assigned by OKX.
198    #[serde(default)]
199    pub trade_id: String,
200    /// Trade price.
201    #[serde(default)]
202    pub px: NumberString,
203    /// Trade size.
204    #[serde(default)]
205    pub sz: NumberString,
206    /// Trade direction: `buy` or `sell`.
207    #[serde(default)]
208    pub side: String,
209    /// Filled time (Unix milliseconds).
210    #[serde(default)]
211    pub ts: NumberString,
212    /// Unrecognized fields retained for forward compatibility.
213    #[serde(flatten, default)]
214    pub extra: ExtraFields,
215}
216
217/// `sprd-tickers` channel row.
218///
219/// OKX docs: <https://www.okx.com/docs-v5/en/#spread-trading-websocket-sprd-tickers-channel>
220#[derive(Debug, Clone, Default, Deserialize)]
221#[serde(rename_all = "camelCase")]
222#[non_exhaustive]
223pub struct SpreadTickerUpdate {
224    /// Spread ID, e.g., `BTC-USDT_BTC-USDT-SWAP`.
225    #[serde(default)]
226    pub sprd_id: String,
227    /// Last traded price.
228    #[serde(default)]
229    pub last: NumberString,
230    /// Last traded size.
231    #[serde(default)]
232    pub last_sz: NumberString,
233    /// Best ask price.
234    #[serde(default)]
235    pub ask_px: NumberString,
236    /// Best ask size.
237    #[serde(default)]
238    pub ask_sz: NumberString,
239    /// Best bid price.
240    #[serde(default)]
241    pub bid_px: NumberString,
242    /// Best bid size.
243    #[serde(default)]
244    pub bid_sz: NumberString,
245    /// Open price over the past 24 hours.
246    #[serde(default)]
247    pub open24h: NumberString,
248    /// Highest price over the past 24 hours.
249    #[serde(default)]
250    pub high24h: NumberString,
251    /// Lowest price over the past 24 hours.
252    #[serde(default)]
253    pub low24h: NumberString,
254    /// 24-hour trading volume in base currency (spot-USDT spreads) or USD (coin-margined spreads).
255    #[serde(default)]
256    pub vol24h: NumberString,
257    /// Ticker data generation time (Unix milliseconds).
258    #[serde(default)]
259    pub ts: NumberString,
260    /// Unrecognized fields retained for forward compatibility.
261    #[serde(flatten, default)]
262    pub extra: ExtraFields,
263}
264
265/// Result row returned by `sprd-order`.
266///
267/// OKX docs: <https://www.okx.com/docs-v5/en/#spread-trading-websocket-trade-api-ws-place-order>
268#[derive(Debug, Clone, Default, Deserialize)]
269#[serde(rename_all = "camelCase")]
270#[non_exhaustive]
271pub struct SpreadPlaceOrderResult {
272    /// Client-supplied order ID.
273    #[serde(default)]
274    pub cl_ord_id: String,
275    /// OKX-assigned order ID; empty on failure.
276    #[serde(default)]
277    pub ord_id: String,
278    /// Order tag.
279    #[serde(default)]
280    pub tag: String,
281    /// Per-order status code; `"0"` on success.
282    #[serde(default)]
283    pub s_code: String,
284    /// Per-order status message; empty on success.
285    #[serde(default)]
286    pub s_msg: String,
287    /// Unrecognized fields retained for forward compatibility.
288    #[serde(flatten, default)]
289    pub extra: ExtraFields,
290}
291
292/// Result row returned by `sprd-amend-order`.
293///
294/// OKX docs: <https://www.okx.com/docs-v5/en/#spread-trading-websocket-trade-api-ws-amend-order>
295#[derive(Debug, Clone, Default, Deserialize)]
296#[serde(rename_all = "camelCase")]
297#[non_exhaustive]
298pub struct SpreadAmendOrderResult {
299    /// Client-supplied order ID.
300    #[serde(default)]
301    pub cl_ord_id: String,
302    /// OKX-assigned order ID.
303    #[serde(default)]
304    pub ord_id: String,
305    /// Client-supplied request ID, echoed from the amend request.
306    #[serde(default)]
307    pub req_id: String,
308    /// Per-order status code; `"0"` on success.
309    #[serde(default)]
310    pub s_code: String,
311    /// Per-order status message; empty on success.
312    #[serde(default)]
313    pub s_msg: String,
314    /// Unrecognized fields retained for forward compatibility.
315    #[serde(flatten, default)]
316    pub extra: ExtraFields,
317}
318
319/// Result row returned by `sprd-cancel-order`.
320///
321/// OKX docs: <https://www.okx.com/docs-v5/en/#spread-trading-websocket-trade-api-ws-cancel-order>
322#[derive(Debug, Clone, Default, Deserialize)]
323#[serde(rename_all = "camelCase")]
324#[non_exhaustive]
325pub struct SpreadCancelOrderResult {
326    /// Client-supplied order ID.
327    #[serde(default)]
328    pub cl_ord_id: String,
329    /// OKX-assigned order ID.
330    #[serde(default)]
331    pub ord_id: String,
332    /// Per-order status code; `"0"` on success.
333    #[serde(default)]
334    pub s_code: String,
335    /// Per-order status message; empty on success.
336    #[serde(default)]
337    pub s_msg: String,
338    /// Unrecognized fields retained for forward compatibility.
339    #[serde(flatten, default)]
340    pub extra: ExtraFields,
341}
342
343/// Result row returned by `sprd-mass-cancel`.
344///
345/// OKX docs: <https://www.okx.com/docs-v5/en/#spread-trading-websocket-trade-api-ws-cancel-all-orders>
346#[derive(Debug, Clone, Default, Deserialize)]
347#[serde(rename_all = "camelCase")]
348#[non_exhaustive]
349pub struct SpreadMassCancelResult {
350    /// `true` if the mass-cancel was accepted.
351    #[serde(default)]
352    pub result: bool,
353    /// Unrecognized fields retained for forward compatibility.
354    #[serde(flatten, default)]
355    pub extra: ExtraFields,
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361
362    #[test]
363    fn parses_operation_result() {
364        let row: SpreadAmendOrderResult = serde_json::from_str(
365            r#"{"ordId":"1","clOrdId":"c","reqId":"r","sCode":"0","sMsg":""}"#,
366        )
367        .unwrap();
368        assert_eq!(row.s_code, "0");
369    }
370
371    #[test]
372    fn parses_spread_order_update() {
373        let row: SpreadOrderUpdate = serde_json::from_str(
374            r#"{
375            "sprdId":"BTC-USDT_BTC-USDT-SWAP","ordId":"312269865356374016","clOrdId":"b1",
376            "tag":"","px":"999","sz":"3","ordType":"limit","side":"buy","fillSz":"0",
377            "fillPx":"","tradeId":"","accFillSz":"0","pendingFillSz":"2","pendingSettleSz":"1",
378            "canceledSz":"1","state":"live","avgPx":"0","cancelSource":"","uTime":"1597026383085",
379            "cTime":"1597026383085","code":"0","msg":"","reqId":"","amendResult":"",
380            "instId":"ignored","pTime":"ignored"
381        }"#,
382        )
383        .unwrap();
384        assert_eq!(row.sprd_id, "BTC-USDT_BTC-USDT-SWAP");
385        assert_eq!(row.amend_result, "");
386        assert!(row.extra.contains_key("instId"));
387        assert!(row.extra.contains_key("pTime"));
388    }
389
390    #[test]
391    fn parses_spread_trade_update() {
392        let row: SpreadTradeUpdate = serde_json::from_str(
393            r#"{
394            "sprdId":"BTC-USDT-SWAP_BTC-USDT-200329","tradeId":"123","ordId":"123445",
395            "clOrdId":"b16","tag":"","fillPx":"999","fillSz":"3","state":"filled","side":"buy",
396            "execType":"M","ts":"1597026383085","legs":[],"code":"","msg":""
397        }"#,
398        )
399        .unwrap();
400        assert_eq!(row.sprd_id, "BTC-USDT-SWAP_BTC-USDT-200329");
401        assert_eq!(row.exec_type, "M");
402        assert_eq!(row.code, "");
403        assert_eq!(row.msg, "");
404    }
405
406    #[test]
407    fn parses_spread_public_trade_update() {
408        let row: SpreadPublicTradeUpdate = serde_json::from_str(
409            r#"{"sprdId":"BTC-USDT_BTC-USDT-SWAP","tradeId":"2499206329160695808","px":"-10","sz":"0.001","side":"sell","ts":"1726801105519"}"#,
410        ).unwrap();
411        assert_eq!(row.sprd_id, "BTC-USDT_BTC-USDT-SWAP");
412        assert_eq!(row.px.as_str(), "-10");
413        assert_eq!(row.sz.as_str(), "0.001");
414    }
415
416    #[test]
417    fn parses_spread_ticker_update() {
418        let row: SpreadTickerUpdate = serde_json::from_str(
419            r#"{
420            "sprdId":"BTC-USDT_BTC-USDT-SWAP","last":"4","lastSz":"0.01","askPx":"19.7",
421            "askSz":"5.79","bidPx":"5.9","bidSz":"5.79","open24h":"-7","high24h":"19.6",
422            "low24h":"-7","vol24h":"9.87","ts":"1715247061026"
423        }"#,
424        )
425        .unwrap();
426        assert_eq!(row.sprd_id, "BTC-USDT_BTC-USDT-SWAP");
427        assert_eq!(row.last.as_str(), "4");
428        assert_eq!(row.ts.as_str(), "1715247061026");
429    }
430}