1#[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 assert_eq!(parsed.code, "0");
43 assert_eq!(parsed.msg, "");
44 assert_eq!(parsed.data.len(), 2);
45
46 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 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 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 assert_eq!(parsed.code, "0");
99 assert_eq!(parsed.msg, "");
100 assert_eq!(parsed.data.len(), 2);
101
102 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 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 assert_eq!(parsed.code, "0");
130 assert_eq!(parsed.msg, "");
131 assert_eq!(parsed.data.len(), 1);
132
133 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 assert_eq!(parsed.code, "0");
148 assert_eq!(parsed.msg, "");
149 assert_eq!(parsed.data.len(), 1);
150
151 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 assert_eq!(parsed.code, "0");
166 assert_eq!(parsed.msg, "");
167 assert_eq!(parsed.data.len(), 1);
168
169 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.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 assert_eq!(parsed.code, "0");
222 assert_eq!(parsed.msg, "");
223 assert_eq!(parsed.data.len(), 1);
224
225 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 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 assert_eq!(parsed.code, "0");
242 assert_eq!(parsed.msg, "");
243 assert_eq!(parsed.data.len(), 1);
244
245 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 assert_eq!(parsed.code, "0");
262 assert_eq!(parsed.msg, "");
263 assert_eq!(parsed.data.len(), 1);
264
265 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 assert_eq!(parsed.code, "0");
286 assert_eq!(parsed.msg, "");
287 assert_eq!(parsed.data.len(), 1);
288
289 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 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 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 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 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 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 let invalid_price = "invalid-price";
509 let result = crate::common::parse::parse_price(invalid_price, 2);
510 assert!(result.is_err());
511
512 let invalid_quantity = "invalid-quantity";
514 let result = crate::common::parse::parse_quantity(invalid_quantity, 8);
515 assert!(result.is_err());
516 }
517}