nautilus_okx/http/
parse.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16#[cfg(test)]
17mod tests {
18    use rstest::rstest;
19    use ustr::Ustr;
20
21    use crate::{
22        common::{
23            enums::{OKXExecType, OKXInstrumentType, OKXMarginMode, OKXPositionSide, OKXSide},
24            testing::load_test_json,
25        },
26        http::{
27            client::OKXResponse,
28            models::{
29                OKXAccount, OKXBalanceDetail, OKXCandlestick, OKXIndexTicker, OKXMarkPrice,
30                OKXOrderHistory, OKXPlaceOrderResponse, OKXPosition, OKXPositionHistory,
31                OKXPositionTier, OKXTrade, OKXTransactionDetail,
32            },
33        },
34    };
35
36    #[rstest]
37    fn test_parse_trades() {
38        let json_data = load_test_json("http_get_trades.json");
39        let parsed: OKXResponse<OKXTrade> = serde_json::from_str(&json_data).unwrap();
40
41        // Basic response envelope
42        assert_eq!(parsed.code, "0");
43        assert_eq!(parsed.msg, "");
44        assert_eq!(parsed.data.len(), 2);
45
46        // Inspect first record
47        let trade0 = &parsed.data[0];
48        assert_eq!(trade0.inst_id, "BTC-USDT");
49        assert_eq!(trade0.px, "102537.9");
50        assert_eq!(trade0.sz, "0.00013669");
51        assert_eq!(trade0.side, OKXSide::Sell);
52        assert_eq!(trade0.trade_id, "734864333");
53        assert_eq!(trade0.ts, 1747087163557);
54
55        // Inspect second record
56        let trade1 = &parsed.data[1];
57        assert_eq!(trade1.inst_id, "BTC-USDT");
58        assert_eq!(trade1.px, "102537.9");
59        assert_eq!(trade1.sz, "0.0000125");
60        assert_eq!(trade1.side, OKXSide::Buy);
61        assert_eq!(trade1.trade_id, "734864332");
62        assert_eq!(trade1.ts, 1747087161666);
63    }
64
65    #[rstest]
66    fn test_parse_candlesticks() {
67        let json_data = load_test_json("http_get_candlesticks.json");
68        let parsed: OKXResponse<OKXCandlestick> = serde_json::from_str(&json_data).unwrap();
69
70        // Basic response envelope
71        assert_eq!(parsed.code, "0");
72        assert_eq!(parsed.msg, "");
73        assert_eq!(parsed.data.len(), 2);
74
75        let bar0 = &parsed.data[0];
76        assert_eq!(bar0.0, "1625097600000");
77        assert_eq!(bar0.1, "33528.6");
78        assert_eq!(bar0.2, "33870.0");
79        assert_eq!(bar0.3, "33528.6");
80        assert_eq!(bar0.4, "33783.9");
81        assert_eq!(bar0.5, "778.838");
82
83        let bar1 = &parsed.data[1];
84        assert_eq!(bar1.0, "1625097660000");
85        assert_eq!(bar1.1, "33783.9");
86        assert_eq!(bar1.2, "33783.9");
87        assert_eq!(bar1.3, "33782.1");
88        assert_eq!(bar1.4, "33782.1");
89        assert_eq!(bar1.5, "0.123");
90    }
91
92    #[rstest]
93    fn test_parse_candlesticks_full() {
94        let json_data = load_test_json("http_get_candlesticks_full.json");
95        let parsed: OKXResponse<OKXCandlestick> = serde_json::from_str(&json_data).unwrap();
96
97        // Basic response envelope
98        assert_eq!(parsed.code, "0");
99        assert_eq!(parsed.msg, "");
100        assert_eq!(parsed.data.len(), 2);
101
102        // Inspect first record
103        let bar0 = &parsed.data[0];
104        assert_eq!(bar0.0, "1747094040000");
105        assert_eq!(bar0.1, "102806.1");
106        assert_eq!(bar0.2, "102820.4");
107        assert_eq!(bar0.3, "102806.1");
108        assert_eq!(bar0.4, "102820.4");
109        assert_eq!(bar0.5, "1040.37");
110        assert_eq!(bar0.6, "10.4037");
111        assert_eq!(bar0.7, "1069603.34883");
112        assert_eq!(bar0.8, "1");
113
114        // Inspect second record
115        let bar1 = &parsed.data[1];
116        assert_eq!(bar1.0, "1747093980000");
117        assert_eq!(bar1.5, "7164.04");
118        assert_eq!(bar1.6, "71.6404");
119        assert_eq!(bar1.7, "7364701.57952");
120        assert_eq!(bar1.8, "1");
121    }
122
123    #[rstest]
124    fn test_parse_mark_price() {
125        let json_data = load_test_json("http_get_mark_price.json");
126        let parsed: OKXResponse<OKXMarkPrice> = serde_json::from_str(&json_data).unwrap();
127
128        // Basic response envelope
129        assert_eq!(parsed.code, "0");
130        assert_eq!(parsed.msg, "");
131        assert_eq!(parsed.data.len(), 1);
132
133        // Inspect first record
134        let mark_price = &parsed.data[0];
135
136        assert_eq!(mark_price.inst_id, "BTC-USDT-SWAP");
137        assert_eq!(mark_price.mark_px, "84660.1");
138        assert_eq!(mark_price.ts, 1744590349506);
139    }
140
141    #[rstest]
142    fn test_parse_index_price() {
143        let json_data = load_test_json("http_get_index_price.json");
144        let parsed: OKXResponse<OKXIndexTicker> = serde_json::from_str(&json_data).unwrap();
145
146        // Basic response envelope
147        assert_eq!(parsed.code, "0");
148        assert_eq!(parsed.msg, "");
149        assert_eq!(parsed.data.len(), 1);
150
151        // Inspect first record
152        let index_price = &parsed.data[0];
153
154        assert_eq!(index_price.inst_id, "BTC-USDT");
155        assert_eq!(index_price.idx_px, "103895");
156        assert_eq!(index_price.ts, 1746942707815);
157    }
158
159    #[rstest]
160    fn test_parse_account() {
161        let json_data = load_test_json("http_get_account_balance.json");
162        let parsed: OKXResponse<OKXAccount> = serde_json::from_str(&json_data).unwrap();
163
164        // Basic response envelope
165        assert_eq!(parsed.code, "0");
166        assert_eq!(parsed.msg, "");
167        assert_eq!(parsed.data.len(), 1);
168
169        // Inspect first record
170        let account = &parsed.data[0];
171        assert_eq!(account.adj_eq, "");
172        assert_eq!(account.borrow_froz, "");
173        assert_eq!(account.imr, "");
174        assert_eq!(account.iso_eq, "5.4682385526666675");
175        assert_eq!(account.mgn_ratio, "");
176        assert_eq!(account.mmr, "");
177        assert_eq!(account.notional_usd, "");
178        assert_eq!(account.notional_usd_for_borrow, "");
179        assert_eq!(account.notional_usd_for_futures, "");
180        assert_eq!(account.notional_usd_for_option, "");
181        assert_eq!(account.notional_usd_for_swap, "");
182        assert_eq!(account.ord_froz, "");
183        assert_eq!(account.total_eq, "99.88870288820581");
184        assert_eq!(account.upl, "");
185        assert_eq!(account.u_time, 1744499648556);
186        assert_eq!(account.details.len(), 1);
187
188        let detail = &account.details[0];
189        assert_eq!(detail.ccy, "USDT");
190        assert_eq!(detail.avail_bal, "94.42612990333333");
191        assert_eq!(detail.avail_eq, "94.42612990333333");
192        assert_eq!(detail.cash_bal, "94.42612990333333");
193        // assert_eq!(detail.collateral_enabled, false);  // TODO: Determine field
194        assert_eq!(detail.dis_eq, "5.4682385526666675");
195        assert_eq!(detail.eq, "99.89469657000001");
196        assert_eq!(detail.eq_usd, "99.88870288820581");
197        assert_eq!(detail.fixed_bal, "0");
198        assert_eq!(detail.frozen_bal, "5.468566666666667");
199        assert_eq!(detail.imr, "0");
200        assert_eq!(detail.iso_eq, "5.468566666666667");
201        assert_eq!(detail.iso_upl, "-0.0273000000000002");
202        assert_eq!(detail.mmr, "0");
203        assert_eq!(detail.notional_lever, "0");
204        assert_eq!(detail.ord_frozen, "0");
205        assert_eq!(detail.reward_bal, "0");
206        assert_eq!(detail.smt_sync_eq, "0");
207        assert_eq!(detail.spot_copy_trading_eq, "0");
208        assert_eq!(detail.spot_iso_bal, "0");
209        assert_eq!(detail.stgy_eq, "0");
210        assert_eq!(detail.twap, "0");
211        assert_eq!(detail.upl, "-0.0273000000000002");
212        assert_eq!(detail.u_time, 1744498994783);
213    }
214
215    #[rstest]
216    fn test_parse_order_history() {
217        let json_data = load_test_json("http_get_orders_history.json");
218        let parsed: OKXResponse<OKXOrderHistory> = serde_json::from_str(&json_data).unwrap();
219
220        // Basic response envelope
221        assert_eq!(parsed.code, "0");
222        assert_eq!(parsed.msg, "");
223        assert_eq!(parsed.data.len(), 1);
224
225        // Inspect first record
226        let order = &parsed.data[0];
227        assert_eq!(order.ord_id, "2497956918703120384");
228        assert_eq!(order.fill_sz, "0.03");
229        assert_eq!(order.acc_fill_sz, "0.03");
230        assert_eq!(order.state, "filled");
231        // fill_fee was omitted in response
232        assert!(order.fill_fee.is_none());
233    }
234
235    #[rstest]
236    fn test_parse_position() {
237        let json_data = load_test_json("http_get_positions.json");
238        let parsed: OKXResponse<OKXPosition> = serde_json::from_str(&json_data).unwrap();
239
240        // Basic response envelope
241        assert_eq!(parsed.code, "0");
242        assert_eq!(parsed.msg, "");
243        assert_eq!(parsed.data.len(), 1);
244
245        // Inspect first record
246        let pos = &parsed.data[0];
247        assert_eq!(pos.inst_id, "BTC-USDT-SWAP");
248        assert_eq!(pos.pos_side, OKXPositionSide::Long);
249        assert_eq!(pos.pos, "0.5");
250        assert_eq!(pos.base_bal, "0.5");
251        assert_eq!(pos.quote_bal, "5000");
252        assert_eq!(pos.u_time, 1622559930237);
253    }
254
255    #[rstest]
256    fn test_parse_position_history() {
257        let json_data = load_test_json("http_get_account_positions-history.json");
258        let parsed: OKXResponse<OKXPositionHistory> = serde_json::from_str(&json_data).unwrap();
259
260        // Basic response envelope
261        assert_eq!(parsed.code, "0");
262        assert_eq!(parsed.msg, "");
263        assert_eq!(parsed.data.len(), 1);
264
265        // Inspect first record
266        let hist = &parsed.data[0];
267        assert_eq!(hist.inst_id, "ETH-USDT-SWAP");
268        assert_eq!(hist.inst_type, OKXInstrumentType::Swap);
269        assert_eq!(hist.mgn_mode, OKXMarginMode::Isolated);
270        assert_eq!(hist.pos_side, OKXPositionSide::Long);
271        assert_eq!(hist.lever, "3.0");
272        assert_eq!(hist.open_avg_px, "3226.93");
273        assert_eq!(hist.close_avg_px.as_deref(), Some("3224.8"));
274        assert_eq!(hist.pnl.as_deref(), Some("-0.0213"));
275        assert!(!hist.c_time.is_empty());
276        assert!(hist.u_time > 0);
277    }
278
279    #[rstest]
280    fn test_parse_position_tiers() {
281        let json_data = load_test_json("http_get_position_tiers.json");
282        let parsed: OKXResponse<OKXPositionTier> = serde_json::from_str(&json_data).unwrap();
283
284        // Basic response envelope
285        assert_eq!(parsed.code, "0");
286        assert_eq!(parsed.msg, "");
287        assert_eq!(parsed.data.len(), 1);
288
289        // Inspect first tier record
290        let tier = &parsed.data[0];
291        assert_eq!(tier.inst_id, "BTC-USDT");
292        assert_eq!(tier.tier, "1");
293        assert_eq!(tier.min_sz, "0");
294        assert_eq!(tier.max_sz, "50");
295        assert_eq!(tier.imr, "0.1");
296        assert_eq!(tier.mmr, "0.03");
297    }
298
299    #[rstest]
300    fn test_parse_account_field_name_compatibility() {
301        // Test with new field names (with Amt suffix)
302        let json_new = r#"{
303            "accAvgPx": "",
304            "availBal": "100.0",
305            "availEq": "100.0",
306            "borrowFroz": "",
307            "cashBal": "100.0",
308            "ccy": "USDT",
309            "clSpotInUseAmt": "25.0",
310            "crossLiab": "",
311            "disEq": "0",
312            "eq": "100.0",
313            "eqUsd": "100.0",
314            "fixedBal": "0",
315            "frozenBal": "0",
316            "imr": "0",
317            "interest": "",
318            "isoEq": "0",
319            "isoLiab": "",
320            "isoUpl": "0",
321            "liab": "",
322            "maxLoan": "",
323            "maxSpotInUseAmt": "50.0",
324            "mgnRatio": "",
325            "mmr": "0",
326            "notionalLever": "0",
327            "openAvgPx": "",
328            "ordFrozen": "0",
329            "rewardBal": "0",
330            "smtSyncEq": "0",
331            "spotBal": "",
332            "spotCopyTradingEq": "0",
333            "spotInUseAmt": "30.0",
334            "spotIsoBal": "0",
335            "spotUpl": "",
336            "spotUplRatio": "",
337            "stgyEq": "0",
338            "totalPnl": "",
339            "totalPnlRatio": "",
340            "twap": "0",
341            "uTime": "1234567890",
342            "upl": "0",
343            "uplLiab": ""
344        }"#;
345
346        let detail_new: OKXBalanceDetail = serde_json::from_str(json_new).unwrap();
347        assert_eq!(detail_new.max_spot_in_use_amt, "50.0");
348        assert_eq!(detail_new.spot_in_use_amt, "30.0");
349        assert_eq!(detail_new.cl_spot_in_use_amt, "25.0");
350
351        // Test with old field names (without Amt suffix) - for backward compatibility
352        let json_old = r#"{
353            "accAvgPx": "",
354            "availBal": "100.0",
355            "availEq": "100.0",
356            "borrowFroz": "",
357            "cashBal": "100.0",
358            "ccy": "USDT",
359            "clSpotInUse": "35.0",
360            "crossLiab": "",
361            "disEq": "0",
362            "eq": "100.0",
363            "eqUsd": "100.0",
364            "fixedBal": "0",
365            "frozenBal": "0",
366            "imr": "0",
367            "interest": "",
368            "isoEq": "0",
369            "isoLiab": "",
370            "isoUpl": "0",
371            "liab": "",
372            "maxLoan": "",
373            "maxSpotInUse": "75.0",
374            "mgnRatio": "",
375            "mmr": "0",
376            "notionalLever": "0",
377            "openAvgPx": "",
378            "ordFrozen": "0",
379            "rewardBal": "0",
380            "smtSyncEq": "0",
381            "spotBal": "",
382            "spotCopyTradingEq": "0",
383            "spotInUse": "40.0",
384            "spotIsoBal": "0",
385            "spotUpl": "",
386            "spotUplRatio": "",
387            "stgyEq": "0",
388            "totalPnl": "",
389            "totalPnlRatio": "",
390            "twap": "0",
391            "uTime": "1234567890",
392            "upl": "0",
393            "uplLiab": ""
394        }"#;
395
396        let detail_old: OKXBalanceDetail = serde_json::from_str(json_old).unwrap();
397        assert_eq!(detail_old.max_spot_in_use_amt, "75.0");
398        assert_eq!(detail_old.spot_in_use_amt, "40.0");
399        assert_eq!(detail_old.cl_spot_in_use_amt, "35.0");
400    }
401
402    #[rstest]
403    fn test_parse_place_order_response() {
404        let json_data = r#"{
405            "ordId": "12345678901234567890",
406            "clOrdId": "client_order_123",
407            "tag": "",
408            "sCode": "0",
409            "sMsg": ""
410        }"#;
411
412        let parsed: OKXPlaceOrderResponse = serde_json::from_str(json_data).unwrap();
413        assert_eq!(
414            parsed.ord_id,
415            Some(ustr::Ustr::from("12345678901234567890"))
416        );
417        assert_eq!(parsed.cl_ord_id, Some(ustr::Ustr::from("client_order_123")));
418        assert_eq!(parsed.tag, Some("".to_string()));
419    }
420
421    #[rstest]
422    fn test_parse_transaction_details() {
423        let json_data = r#"{
424            "instType": "SPOT",
425            "instId": "BTC-USDT",
426            "tradeId": "123456789",
427            "ordId": "987654321",
428            "clOrdId": "client_123",
429            "billId": "bill_456",
430            "fillPx": "42000.5",
431            "fillSz": "0.001",
432            "side": "buy",
433            "execType": "T",
434            "feeCcy": "USDT",
435            "fee": "0.042",
436            "ts": "1625097600000"
437        }"#;
438
439        let parsed: OKXTransactionDetail = serde_json::from_str(json_data).unwrap();
440        assert_eq!(parsed.inst_type, OKXInstrumentType::Spot);
441        assert_eq!(parsed.inst_id, Ustr::from("BTC-USDT"));
442        assert_eq!(parsed.trade_id, Ustr::from("123456789"));
443        assert_eq!(parsed.ord_id, Ustr::from("987654321"));
444        assert_eq!(parsed.cl_ord_id, Ustr::from("client_123"));
445        assert_eq!(parsed.bill_id, Ustr::from("bill_456"));
446        assert_eq!(parsed.fill_px, "42000.5");
447        assert_eq!(parsed.fill_sz, "0.001");
448        assert_eq!(parsed.side, OKXSide::Buy);
449        assert_eq!(parsed.exec_type, OKXExecType::Taker);
450        assert_eq!(parsed.fee_ccy, "USDT");
451        assert_eq!(parsed.fee, Some("0.042".to_string()));
452        assert_eq!(parsed.ts, 1625097600000);
453    }
454
455    #[rstest]
456    fn test_parse_empty_fee_field() {
457        use crate::http::models::OKXTransactionDetail;
458
459        let json_data = r#"{
460            "instType": "SPOT",
461            "instId": "BTC-USDT",
462            "tradeId": "123456789",
463            "ordId": "987654321",
464            "clOrdId": "client_123",
465            "billId": "bill_456",
466            "fillPx": "42000.5",
467            "fillSz": "0.001",
468            "side": "buy",
469            "execType": "T",
470            "feeCcy": "USDT",
471            "fee": "",
472            "ts": "1625097600000"
473        }"#;
474
475        let parsed: OKXTransactionDetail = serde_json::from_str(json_data).unwrap();
476        assert_eq!(parsed.fee, None);
477    }
478
479    #[rstest]
480    fn test_parse_optional_string_to_u64() {
481        use serde::Deserialize;
482
483        #[derive(Deserialize)]
484        struct TestStruct {
485            #[serde(deserialize_with = "crate::common::parse::deserialize_optional_string_to_u64")]
486            value: Option<u64>,
487        }
488
489        // Test with valid string
490        let json_value = r#"{"value": "12345"}"#;
491        let result: TestStruct = serde_json::from_str(json_value).unwrap();
492        assert_eq!(result.value, Some(12345));
493
494        // Test with empty string
495        let json_empty = r#"{"value": ""}"#;
496        let result_empty: TestStruct = serde_json::from_str(json_empty).unwrap();
497        assert_eq!(result_empty.value, None);
498
499        // Test with null
500        let json_null = r#"{"value": null}"#;
501        let result_null: TestStruct = serde_json::from_str(json_null).unwrap();
502        assert_eq!(result_null.value, None);
503    }
504
505    #[rstest]
506    fn test_parse_error_handling() {
507        // Test error handling with invalid price string
508        let invalid_price = "invalid-price";
509        let result = crate::common::parse::parse_price(invalid_price, 2);
510        assert!(result.is_err());
511
512        // Test error handling with invalid quantity string
513        let invalid_quantity = "invalid-quantity";
514        let result = crate::common::parse::parse_quantity(invalid_quantity, 8);
515        assert!(result.is_err());
516    }
517}