Skip to main content

rust_okx/ws/model/
trade.rs

1//! Trade channel models (`orders`, `fills`) and order-operation result rows.
2//!
3//! Private channels; login required.
4
5use serde::Deserialize;
6use serde_json::Value;
7
8use super::ExtraFields;
9use crate::model::NumberString;
10
11/// Private `orders` channel row.
12///
13/// OKX docs: <https://www.okx.com/docs-v5/en/#order-book-trading-trade-ws-order-channel>
14#[derive(Debug, Clone, Default, Deserialize)]
15#[serde(rename_all = "camelCase")]
16#[non_exhaustive]
17pub struct OrderUpdate {
18    /// Instrument type, e.g., `SPOT`, `MARGIN`, `SWAP`, `FUTURES`, `OPTION`.
19    #[serde(default)]
20    pub inst_type: String,
21    /// Instrument ID, e.g., `BTC-USDT`.
22    #[serde(default)]
23    pub inst_id: String,
24    /// Target currency for quantity (spot currency-trade only).
25    ///
26    /// `base_ccy` means the order size is in base currency;
27    /// `quote_ccy` means the order size is in quote currency.
28    #[serde(default)]
29    pub tgt_ccy: String,
30    /// Margin currency (for cross-margin orders; empty otherwise).
31    #[serde(default)]
32    pub ccy: String,
33    /// OKX-assigned order ID.
34    #[serde(default)]
35    pub ord_id: String,
36    /// Client-supplied order ID, if any.
37    #[serde(default)]
38    pub cl_ord_id: String,
39    /// Order tag.
40    #[serde(default)]
41    pub tag: String,
42    /// Order price; empty for market orders.
43    #[serde(default)]
44    pub px: NumberString,
45    /// Order size.
46    #[serde(default)]
47    pub sz: NumberString,
48    /// Estimated notional value of the order in USD.
49    #[serde(default)]
50    pub notional_usd: NumberString,
51    /// Order type.
52    ///
53    /// Documented values: `market`, `limit`, `post_only`, `fok`, `ioc`,
54    /// `optimal_limit_ioc`, `mmp`, `mmp_and_post_only`, `op_fok`, `elp`.
55    #[serde(default)]
56    pub ord_type: String,
57    /// Order side: `buy` or `sell`.
58    #[serde(default)]
59    pub side: String,
60    /// Position side: `long`, `short`, or `net`.
61    #[serde(default)]
62    pub pos_side: String,
63    /// Trade mode: `cross`, `isolated`, or `cash`.
64    #[serde(default)]
65    pub td_mode: String,
66    /// Accumulated filled size.
67    #[serde(default)]
68    pub acc_fill_sz: NumberString,
69    /// Fill price of the most recent fill in this push.
70    #[serde(default)]
71    pub fill_px: NumberString,
72    /// Trade ID of the most recent fill.
73    #[serde(default)]
74    pub trade_id: String,
75    /// Fill size of the most recent fill.
76    #[serde(default)]
77    pub fill_sz: NumberString,
78    /// Fill timestamp of the most recent fill (Unix milliseconds).
79    #[serde(default)]
80    pub fill_time: NumberString,
81    /// Fill PnL of the most recent fill.
82    #[serde(default)]
83    pub fill_pnl: NumberString,
84    /// Fee charged for the most recent fill.
85    #[serde(default)]
86    pub fill_fee: NumberString,
87    /// Fee currency for the most recent fill.
88    #[serde(default)]
89    pub fill_fee_ccy: String,
90    /// Filled notional value in USD for the most recent fill.
91    #[serde(default)]
92    pub fill_notional_usd: NumberString,
93    /// Implied volatility at fill (options only).
94    #[serde(default)]
95    pub fill_px_vol: NumberString,
96    /// USD-denominated option price at fill (options only).
97    #[serde(default)]
98    pub fill_px_usd: NumberString,
99    /// Mark implied volatility at fill (options only).
100    #[serde(default)]
101    pub fill_mark_vol: NumberString,
102    /// Forward price at fill (options only).
103    #[serde(default)]
104    pub fill_fwd_px: NumberString,
105    /// Mark price at fill (FUTURES, SWAP, OPTION).
106    #[serde(default)]
107    pub fill_mark_px: NumberString,
108    /// Index price at fill.
109    #[serde(default)]
110    pub fill_idx_px: NumberString,
111    /// Liquidity role for the most recent fill: `T` (taker) or `M` (maker).
112    #[serde(default)]
113    pub exec_type: String,
114    /// Average fill price.
115    #[serde(default)]
116    pub avg_px: NumberString,
117    /// Order state.
118    ///
119    /// Documented values: `live`, `partially_filled`, `filled`, `canceled`, `mmp_canceled`.
120    #[serde(default)]
121    pub state: String,
122    /// Leverage.
123    #[serde(default)]
124    pub lever: NumberString,
125    /// Client-supplied attached algo order ID.
126    #[serde(default)]
127    pub attach_algo_cl_ord_id: String,
128    /// Take-profit trigger price.
129    #[serde(default)]
130    pub tp_trigger_px: NumberString,
131    /// Take-profit trigger price type: `last`, `index`, or `mark`.
132    #[serde(default)]
133    pub tp_trigger_px_type: String,
134    /// Take-profit order price; `-1` means market order.
135    #[serde(default)]
136    pub tp_ord_px: NumberString,
137    /// Stop-loss trigger price.
138    #[serde(default)]
139    pub sl_trigger_px: NumberString,
140    /// Stop-loss trigger price type: `last`, `index`, or `mark`.
141    #[serde(default)]
142    pub sl_trigger_px_type: String,
143    /// Stop-loss order price; `-1` means market order.
144    #[serde(default)]
145    pub sl_ord_px: NumberString,
146    /// Fee currency.
147    #[serde(default)]
148    pub fee_ccy: String,
149    /// Fee amount; negative means deducted, positive means maker rebate.
150    #[serde(default)]
151    pub fee: NumberString,
152    /// Rebate currency.
153    #[serde(default)]
154    pub rebate_ccy: String,
155    /// Rebate amount (maker rebate).
156    #[serde(default)]
157    pub rebate: NumberString,
158    /// Profit and loss for closing orders.
159    #[serde(default)]
160    pub pnl: NumberString,
161    /// Order source (internal OKX field).
162    #[serde(default)]
163    pub source: String,
164    /// Order category.
165    ///
166    /// Documented values: `normal`, `twap`, `adl`, `full_liquidation`,
167    /// `partial_liquidation`, `delivery`, `ddh`, `auto_conversion`.
168    #[serde(default)]
169    pub category: String,
170    /// Whether this is a reduce-only order: `"true"` or `"false"`.
171    #[serde(default)]
172    pub reduce_only: String,
173    /// Whether this is a TP limit order: `"true"` or `"false"`.
174    #[serde(default)]
175    pub is_tp_limit: String,
176    /// Source that triggered the cancellation.
177    #[serde(default)]
178    pub cancel_source: String,
179    /// Client-supplied algo order ID that triggered this order.
180    #[serde(default)]
181    pub algo_cl_ord_id: String,
182    /// OKX-assigned algo order ID that triggered this order.
183    #[serde(default)]
184    pub algo_id: String,
185    /// Source of the last amendment.
186    #[serde(default)]
187    pub amend_source: String,
188    /// Result of the last amendment.
189    ///
190    /// `-1`: failure, `0`: success, `1`: auto-canceled, `2`: auto-amended (options only).
191    #[serde(default)]
192    pub amend_result: String,
193    /// Client-supplied request ID, echoed from the operation that caused this push.
194    #[serde(default)]
195    pub req_id: String,
196    /// Error code; `"0"` on success.
197    #[serde(default)]
198    pub code: String,
199    /// Error message; empty on success.
200    #[serde(default)]
201    pub msg: String,
202    /// Price type for options: `px` (price), `pxVol` (IV), or `pxUsd` (USD price).
203    #[serde(default)]
204    pub px_type: String,
205    /// USD-denominated option order price.
206    #[serde(default)]
207    pub px_usd: NumberString,
208    /// Implied-volatility option order price.
209    #[serde(default)]
210    pub px_vol: NumberString,
211    /// Linked algo order details (JSON object; empty when not linked).
212    #[serde(default)]
213    pub linked_algo_ord: Value,
214    /// Attached algo orders (TP/SL orders attached to this order).
215    #[serde(default)]
216    pub attach_algo_ords: Vec<Value>,
217    /// Self-trade prevention mode: `cancel_maker`, `cancel_taker`, or `cancel_both`.
218    #[serde(default)]
219    pub stp_mode: String,
220    /// Quote currency used for the trade (event contracts only).
221    #[serde(default)]
222    pub trade_quote_ccy: String,
223    /// Last price at the time of the push.
224    #[serde(default)]
225    pub last_px: NumberString,
226    /// Settlement outcome (event contracts only): `yes` or `no`.
227    #[serde(default)]
228    pub outcome: String,
229    /// Order creation time (Unix milliseconds).
230    #[serde(default)]
231    pub c_time: NumberString,
232    /// Last update time (Unix milliseconds).
233    #[serde(default)]
234    pub u_time: NumberString,
235    /// Unrecognized fields retained for forward compatibility.
236    #[serde(flatten, default)]
237    pub extra: ExtraFields,
238}
239
240/// Private `fills` channel row.
241///
242/// OKX docs: <https://www.okx.com/docs-v5/en/#order-book-trading-trade-ws-fills-channel>
243#[derive(Debug, Clone, Default, Deserialize)]
244#[serde(rename_all = "camelCase")]
245#[non_exhaustive]
246pub struct FillUpdate {
247    /// Instrument ID, e.g., `BTC-USDT-SWAP`.
248    #[serde(default)]
249    pub inst_id: String,
250    /// Filled quantity; aggregated when `count > 1`.
251    #[serde(default)]
252    pub fill_sz: NumberString,
253    /// Last filled price.
254    #[serde(default)]
255    pub fill_px: NumberString,
256    /// Trade direction: `buy` or `sell`.
257    #[serde(default)]
258    pub side: String,
259    /// Filled time (Unix milliseconds).
260    #[serde(default)]
261    pub ts: NumberString,
262    /// OKX-assigned order ID.
263    #[serde(default)]
264    pub ord_id: String,
265    /// Client-supplied order ID.
266    #[serde(default)]
267    pub cl_ord_id: String,
268    /// The last trade ID in the trades aggregation.
269    #[serde(default)]
270    pub trade_id: String,
271    /// Liquidity role: `T` (taker) or `M` (maker).
272    #[serde(default)]
273    pub exec_type: String,
274    /// Number of trades aggregated into this push.
275    #[serde(default)]
276    pub count: NumberString,
277    /// Unrecognized fields retained for forward compatibility.
278    #[serde(flatten, default)]
279    pub extra: ExtraFields,
280}
281
282/// Result row returned by `order` and `batch-orders`.
283///
284/// OKX docs: <https://www.okx.com/docs-v5/en/#order-book-trading-trade-ws-place-order>
285#[derive(Debug, Clone, Default, Deserialize)]
286#[serde(rename_all = "camelCase")]
287#[non_exhaustive]
288pub struct PlaceOrderResult {
289    /// OKX-assigned order ID; empty on failure.
290    #[serde(default)]
291    pub ord_id: String,
292    /// Client-supplied order ID.
293    #[serde(default)]
294    pub cl_ord_id: String,
295    /// Order tag.
296    #[serde(default)]
297    pub tag: String,
298    /// Timestamp when request processing finished (Unix milliseconds).
299    #[serde(default)]
300    pub ts: NumberString,
301    /// Per-order status code; `"0"` on success.
302    #[serde(default)]
303    pub s_code: String,
304    /// Per-order status message; empty on success.
305    #[serde(default)]
306    pub s_msg: String,
307    /// Sub-code when `s_code` is non-zero; `""` on success.
308    #[serde(default)]
309    pub sub_code: String,
310    /// Unrecognized fields retained for forward compatibility.
311    #[serde(flatten, default)]
312    pub extra: ExtraFields,
313}
314
315/// Result row returned by `cancel-order` and `batch-cancel-orders`.
316///
317/// OKX docs: <https://www.okx.com/docs-v5/en/#order-book-trading-trade-ws-cancel-order>
318#[derive(Debug, Clone, Default, Deserialize)]
319#[serde(rename_all = "camelCase")]
320#[non_exhaustive]
321pub struct CancelOrderResult {
322    /// OKX-assigned order ID.
323    #[serde(default)]
324    pub ord_id: String,
325    /// Client-supplied order ID.
326    #[serde(default)]
327    pub cl_ord_id: String,
328    /// Timestamp when request processing finished (Unix milliseconds).
329    #[serde(default)]
330    pub ts: NumberString,
331    /// Per-order status code; `"0"` on success.
332    #[serde(default)]
333    pub s_code: String,
334    /// Per-order status message; empty on success.
335    #[serde(default)]
336    pub s_msg: String,
337    /// Unrecognized fields retained for forward compatibility.
338    #[serde(flatten, default)]
339    pub extra: ExtraFields,
340}
341
342/// Result row returned by `amend-order` and `batch-amend-orders`.
343///
344/// OKX docs: <https://www.okx.com/docs-v5/en/#order-book-trading-trade-ws-amend-order>
345#[derive(Debug, Clone, Default, Deserialize)]
346#[serde(rename_all = "camelCase")]
347#[non_exhaustive]
348pub struct AmendOrderResult {
349    /// OKX-assigned order ID.
350    #[serde(default)]
351    pub ord_id: String,
352    /// Client-supplied order ID.
353    #[serde(default)]
354    pub cl_ord_id: String,
355    /// Timestamp when request processing finished (Unix milliseconds).
356    #[serde(default)]
357    pub ts: NumberString,
358    /// Client-supplied request ID, echoed from the amend request.
359    #[serde(default)]
360    pub req_id: String,
361    /// Per-order status code; `"0"` on success.
362    #[serde(default)]
363    pub s_code: String,
364    /// Per-order status message; empty on success.
365    #[serde(default)]
366    pub s_msg: String,
367    /// Sub-code when `s_code` is non-zero; `""` on success.
368    #[serde(default)]
369    pub sub_code: String,
370    /// Unrecognized fields retained for forward compatibility.
371    #[serde(flatten, default)]
372    pub extra: ExtraFields,
373}
374
375/// Result row returned by `mass-cancel`.
376///
377/// OKX docs: <https://www.okx.com/docs-v5/en/#order-book-trading-trade-ws-mass-cancel-order>
378#[derive(Debug, Clone, Default, Deserialize)]
379#[serde(rename_all = "camelCase")]
380#[non_exhaustive]
381pub struct MassCancelOperationResult {
382    /// `true` if the mass-cancel was accepted by OKX.
383    #[serde(default)]
384    pub result: bool,
385    /// Unrecognized fields retained for forward compatibility.
386    #[serde(flatten, default)]
387    pub extra: ExtraFields,
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn parses_order_update() {
396        let row: OrderUpdate = serde_json::from_str(
397            r#"{
398            "accFillSz":"0.001","algoClOrdId":"","algoId":"","amendResult":"","amendSource":"",
399            "avgPx":"31527.1","cancelSource":"","category":"normal","ccy":"","clOrdId":"",
400            "code":"0","cTime":"1654084334977","execType":"M","fee":"-0.02522168","feeCcy":"USDT",
401            "fillFee":"-0.02522168","fillFeeCcy":"USDT","fillNotionalUsd":"31.50818374",
402            "fillPx":"31527.1","fillSz":"0.001","fillPnl":"0.01","fillTime":"1654084353263",
403            "fillPxVol":"","fillPxUsd":"","fillMarkVol":"","fillFwdPx":"","fillMarkPx":"",
404            "fillIdxPx":"","instId":"BTC-USDT","instType":"SPOT","lever":"0","msg":"",
405            "notionalUsd":"31.50818374","ordId":"452197707845865472","ordType":"limit","pnl":"0",
406            "posSide":"","px":"31527.1","pxUsd":"","pxVol":"","pxType":"","quickMgnType":"",
407            "rebate":"0","rebateCcy":"BTC","reduceOnly":"false","reqId":"","side":"sell",
408            "attachAlgoClOrdId":"","slOrdPx":"","slTriggerPx":"","slTriggerPxType":"last",
409            "source":"","state":"filled","stpId":"","stpMode":"","sz":"0.001","tag":"",
410            "tdMode":"cash","tgtCcy":"","tpOrdPx":"","tpTriggerPx":"","tpTriggerPxType":"last",
411            "attachAlgoOrds":[],"tradeId":"242589207","tradeQuoteCcy":"USDT","lastPx":"38892.2",
412            "uTime":"1654084353264","isTpLimit":"false","linkedAlgoOrd":{"algoId":""}
413        }"#,
414        )
415        .unwrap();
416        assert_eq!(row.ord_id, "452197707845865472");
417        assert_eq!(row.state, "filled");
418        assert_eq!(row.fill_notional_usd.as_str(), "31.50818374");
419        assert_eq!(row.last_px.as_str(), "38892.2");
420        assert_eq!(row.is_tp_limit, "false");
421        // deprecated fields fall through to extra
422        assert!(row.extra.contains_key("stpId"));
423        assert!(row.extra.contains_key("quickMgnType"));
424    }
425
426    #[test]
427    fn parses_fill_update() {
428        let row: FillUpdate = serde_json::from_str(
429            r#"{
430            "instId":"BTC-USDT-SWAP","fillSz":"100","fillPx":"70000","side":"buy",
431            "ts":"1705449605015","ordId":"680800019749904384","clOrdId":"1234567890",
432            "tradeId":"12345","execType":"T","count":"10"
433        }"#,
434        )
435        .unwrap();
436        assert_eq!(row.inst_id, "BTC-USDT-SWAP");
437        assert_eq!(row.exec_type, "T");
438        assert_eq!(row.count.as_str(), "10");
439    }
440
441    #[test]
442    fn parses_place_order_result() {
443        let row: PlaceOrderResult = serde_json::from_str(
444            r#"{"clOrdId":"","ordId":"12345689","tag":"","ts":"1695190491421","sCode":"0","sMsg":"","subCode":""}"#,
445        )
446        .unwrap();
447        assert_eq!(row.ord_id, "12345689");
448        assert_eq!(row.s_code, "0");
449        assert_eq!(row.ts.as_str(), "1695190491421");
450    }
451
452    #[test]
453    fn parses_cancel_order_result() {
454        let row: CancelOrderResult = serde_json::from_str(
455            r#"{"clOrdId":"","ordId":"2510789768709120","ts":"1695190491421","sCode":"0","sMsg":""}"#,
456        )
457        .unwrap();
458        assert_eq!(row.ord_id, "2510789768709120");
459        assert_eq!(row.s_code, "0");
460    }
461
462    #[test]
463    fn parses_amend_order_result() {
464        let row: AmendOrderResult = serde_json::from_str(
465            r#"{"clOrdId":"","ordId":"2510789768709120","ts":"1695190491421","reqId":"b12344","sCode":"0","sMsg":"","subCode":""}"#,
466        )
467        .unwrap();
468        assert_eq!(row.ord_id, "2510789768709120");
469        assert_eq!(row.req_id, "b12344");
470        assert_eq!(row.s_code, "0");
471    }
472}