1use ccxt_core::{
6 Result,
7 error::{Error, ParseError},
8 types::{
9 Balance, BalanceEntry, Market, MarketLimits, MarketPrecision, MarketType, MinMax, OHLCV,
10 Order, OrderBook, OrderBookEntry, OrderSide, OrderStatus, OrderType, Ticker, Trade,
11 financial::{Amount, Cost, Price},
12 },
13};
14use rust_decimal::Decimal;
15use rust_decimal::prelude::{FromPrimitive, FromStr};
16use serde_json::Value;
17use std::collections::HashMap;
18
19fn parse_decimal(data: &Value, key: &str) -> Option<Decimal> {
25 data.get(key).and_then(|v| {
26 if let Some(num) = v.as_f64() {
27 Decimal::from_f64(num)
28 } else if let Some(s) = v.as_str() {
29 if s.is_empty() {
30 None
31 } else {
32 Decimal::from_str(s).ok()
33 }
34 } else {
35 None
36 }
37 })
38}
39
40fn parse_timestamp(data: &Value, key: &str) -> Option<i64> {
42 data.get(key).and_then(|v| {
43 v.as_i64()
44 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
45 })
46}
47
48fn value_to_hashmap(data: &Value) -> HashMap<String, Value> {
50 data.as_object()
51 .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
52 .unwrap_or_default()
53}
54
55pub fn timestamp_to_datetime(timestamp: i64) -> Option<String> {
57 chrono::DateTime::from_timestamp_millis(timestamp)
58 .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string())
59}
60
61pub fn datetime_to_timestamp(datetime: &str) -> Option<i64> {
63 chrono::DateTime::parse_from_rfc3339(datetime)
64 .ok()
65 .map(|dt| dt.timestamp_millis())
66}
67
68pub fn parse_market(data: &Value) -> Result<Market> {
84 let id = data["symbol"]
86 .as_str()
87 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
88 .to_string();
89
90 let base = data["baseCoin"]
92 .as_str()
93 .ok_or_else(|| Error::from(ParseError::missing_field("baseCoin")))?
94 .to_string();
95
96 let quote = data["quoteCoin"]
97 .as_str()
98 .ok_or_else(|| Error::from(ParseError::missing_field("quoteCoin")))?
99 .to_string();
100
101 let contract_type = data["contractType"].as_str();
103 let market_type = match contract_type {
104 Some("LinearPerpetual") => MarketType::Swap,
105 Some("InversePerpetual") => MarketType::Swap,
106 Some("LinearFutures") => MarketType::Futures,
107 Some("InverseFutures") => MarketType::Futures,
108 _ => MarketType::Spot,
109 };
110
111 let status = data["status"].as_str().unwrap_or("Trading");
113 let active = status == "Trading";
114
115 let price_precision = parse_decimal(data, "priceFilter").or_else(|| {
117 data.get("priceFilter")
118 .and_then(|pf| parse_decimal(pf, "tickSize"))
119 });
120 let amount_precision = parse_decimal(data, "lotSizeFilter").or_else(|| {
121 data.get("lotSizeFilter")
122 .and_then(|lf| parse_decimal(lf, "basePrecision"))
123 });
124
125 let (min_amount, max_amount) = if let Some(lot_filter) = data.get("lotSizeFilter") {
127 (
128 parse_decimal(lot_filter, "minOrderQty"),
129 parse_decimal(lot_filter, "maxOrderQty"),
130 )
131 } else {
132 (None, None)
133 };
134
135 let contract = market_type != MarketType::Spot;
137 let linear = if contract {
138 Some(contract_type == Some("LinearPerpetual") || contract_type == Some("LinearFutures"))
139 } else {
140 None
141 };
142 let inverse = if contract {
143 Some(contract_type == Some("InversePerpetual") || contract_type == Some("InverseFutures"))
144 } else {
145 None
146 };
147 let contract_size = parse_decimal(data, "contractSize");
148
149 let settle = data["settleCoin"].as_str().map(|s| s.to_string());
151 let settle_id = settle.clone();
152
153 let expiry = parse_timestamp(data, "deliveryTime");
155 let expiry_datetime = expiry.and_then(timestamp_to_datetime);
156
157 let symbol = match market_type {
162 MarketType::Spot => format!("{}/{}", base, quote),
163 MarketType::Swap => {
164 if let Some(ref s) = settle {
165 format!("{}/{}:{}", base, quote, s)
166 } else if linear == Some(true) {
167 format!("{}/{}:{}", base, quote, quote)
169 } else {
170 format!("{}/{}:{}", base, quote, base)
172 }
173 }
174 MarketType::Futures => {
175 let settle_ccy = settle.clone().unwrap_or_else(|| {
176 if linear == Some(true) {
177 quote.clone()
178 } else {
179 base.clone()
180 }
181 });
182 if let Some(exp_ts) = expiry {
183 if let Some(dt) = chrono::DateTime::from_timestamp_millis(exp_ts) {
185 let year = (dt.format("%y").to_string().parse::<u8>()).unwrap_or(0);
186 let month = (dt.format("%m").to_string().parse::<u8>()).unwrap_or(1);
187 let day = (dt.format("%d").to_string().parse::<u8>()).unwrap_or(1);
188 format!(
189 "{}/{}:{}-{:02}{:02}{:02}",
190 base, quote, settle_ccy, year, month, day
191 )
192 } else {
193 format!("{}/{}:{}", base, quote, settle_ccy)
194 }
195 } else {
196 format!("{}/{}:{}", base, quote, settle_ccy)
197 }
198 }
199 MarketType::Option => format!("{}/{}", base, quote),
200 };
201
202 let parsed_symbol = ccxt_core::symbol::SymbolParser::parse(&symbol).ok();
204
205 Ok(Market {
206 id,
207 symbol,
208 parsed_symbol,
209 base: base.clone(),
210 quote: quote.clone(),
211 settle,
212 base_id: Some(base),
213 quote_id: Some(quote),
214 settle_id,
215 market_type,
216 active,
217 margin: contract,
218 contract: Some(contract),
219 linear,
220 inverse,
221 contract_size,
222 expiry,
223 expiry_datetime,
224 strike: None,
225 option_type: None,
226 precision: MarketPrecision {
227 price: price_precision,
228 amount: amount_precision,
229 base: None,
230 quote: None,
231 },
232 limits: MarketLimits {
233 amount: Some(MinMax {
234 min: min_amount,
235 max: max_amount,
236 }),
237 price: None,
238 cost: None,
239 leverage: None,
240 },
241 maker: parse_decimal(data, "makerFeeRate"),
242 taker: parse_decimal(data, "takerFeeRate"),
243 percentage: Some(true),
244 tier_based: Some(false),
245 fee_side: Some("quote".to_string()),
246 info: value_to_hashmap(data),
247 })
248}
249
250pub fn parse_ticker(data: &Value, market: Option<&Market>) -> Result<Ticker> {
261 let symbol = if let Some(m) = market {
262 m.symbol.clone()
263 } else {
264 data["symbol"]
267 .as_str()
268 .map(|s| s.to_string())
269 .unwrap_or_default()
270 };
271
272 let timestamp = parse_timestamp(data, "time")
274 .or_else(|| parse_timestamp(data, "timestamp"))
275 .unwrap_or(0);
276
277 Ok(Ticker {
278 symbol,
279 timestamp,
280 datetime: timestamp_to_datetime(timestamp),
281 high: parse_decimal(data, "highPrice24h").map(Price::new),
282 low: parse_decimal(data, "lowPrice24h").map(Price::new),
283 bid: parse_decimal(data, "bid1Price").map(Price::new),
284 bid_volume: parse_decimal(data, "bid1Size").map(Amount::new),
285 ask: parse_decimal(data, "ask1Price").map(Price::new),
286 ask_volume: parse_decimal(data, "ask1Size").map(Amount::new),
287 vwap: None,
288 open: parse_decimal(data, "prevPrice24h").map(Price::new),
289 close: parse_decimal(data, "lastPrice").map(Price::new),
290 last: parse_decimal(data, "lastPrice").map(Price::new),
291 previous_close: parse_decimal(data, "prevPrice24h").map(Price::new),
292 change: parse_decimal(data, "price24hPcnt")
293 .and_then(|pct| parse_decimal(data, "prevPrice24h").map(|prev| Price::new(prev * pct))),
294 percentage: parse_decimal(data, "price24hPcnt").map(|p| p * Decimal::from(100)),
295 average: None,
296 base_volume: parse_decimal(data, "volume24h").map(Amount::new),
297 quote_volume: parse_decimal(data, "turnover24h").map(Amount::new),
298 info: value_to_hashmap(data),
299 })
300}
301
302pub fn parse_orderbook(data: &Value, symbol: String) -> Result<OrderBook> {
314 let timestamp = parse_timestamp(data, "ts")
315 .or_else(|| parse_timestamp(data, "time"))
316 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
317
318 let mut bids = parse_orderbook_side(&data["b"])?;
319 let mut asks = parse_orderbook_side(&data["a"])?;
320
321 bids.sort_by(|a, b| b.price.cmp(&a.price));
323
324 asks.sort_by(|a, b| a.price.cmp(&b.price));
326
327 Ok(OrderBook {
328 symbol,
329 timestamp,
330 datetime: timestamp_to_datetime(timestamp),
331 nonce: None,
332 bids,
333 asks,
334 buffered_deltas: std::collections::VecDeque::new(),
335 bids_map: std::collections::BTreeMap::new(),
336 asks_map: std::collections::BTreeMap::new(),
337 is_synced: false,
338 needs_resync: false,
339 last_resync_time: 0,
340 info: value_to_hashmap(data),
341 })
342}
343
344fn parse_orderbook_side(data: &Value) -> Result<Vec<OrderBookEntry>> {
346 let Some(array) = data.as_array() else {
347 return Ok(Vec::new());
348 };
349
350 let mut result = Vec::new();
351
352 for item in array {
353 if let Some(arr) = item.as_array() {
354 if arr.len() >= 2 {
356 let price = arr[0]
357 .as_str()
358 .and_then(|s| Decimal::from_str(s).ok())
359 .or_else(|| arr[0].as_f64().and_then(Decimal::from_f64))
360 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "price")))?;
361
362 let amount = arr[1]
363 .as_str()
364 .and_then(|s| Decimal::from_str(s).ok())
365 .or_else(|| arr[1].as_f64().and_then(Decimal::from_f64))
366 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "amount")))?;
367
368 result.push(OrderBookEntry {
369 price: Price::new(price),
370 amount: Amount::new(amount),
371 });
372 }
373 }
374 }
375
376 Ok(result)
377}
378
379pub fn parse_trade(data: &Value, market: Option<&Market>) -> Result<Trade> {
390 let symbol = if let Some(m) = market {
391 m.symbol.clone()
392 } else {
393 data["symbol"]
394 .as_str()
395 .map(|s| s.to_string())
396 .unwrap_or_default()
397 };
398
399 let id = data["execId"]
400 .as_str()
401 .or_else(|| data["id"].as_str())
402 .map(|s| s.to_string());
403
404 let timestamp = parse_timestamp(data, "time")
405 .or_else(|| parse_timestamp(data, "T"))
406 .unwrap_or(0);
407
408 let side = match data["side"].as_str() {
410 Some("Buy") | Some("buy") | Some("BUY") => OrderSide::Buy,
411 Some("Sell") | Some("sell") | Some("SELL") => OrderSide::Sell,
412 _ => OrderSide::Buy, };
414
415 let price = parse_decimal(data, "price").or_else(|| parse_decimal(data, "execPrice"));
416 let amount = parse_decimal(data, "size")
417 .or_else(|| parse_decimal(data, "execQty"))
418 .or_else(|| parse_decimal(data, "qty"));
419
420 let cost = match (price, amount) {
421 (Some(p), Some(a)) => Some(p * a),
422 _ => None,
423 };
424
425 Ok(Trade {
426 id,
427 order: data["orderId"].as_str().map(|s| s.to_string()),
428 timestamp,
429 datetime: timestamp_to_datetime(timestamp),
430 symbol,
431 trade_type: None,
432 side,
433 taker_or_maker: None,
434 price: Price::new(price.unwrap_or(Decimal::ZERO)),
435 amount: Amount::new(amount.unwrap_or(Decimal::ZERO)),
436 cost: cost.map(Cost::new),
437 fee: None,
438 info: value_to_hashmap(data),
439 })
440}
441
442pub fn parse_ohlcv(data: &Value) -> Result<OHLCV> {
452 if let Some(arr) = data.as_array() {
455 if arr.len() < 6 {
456 return Err(Error::from(ParseError::invalid_format(
457 "data",
458 "OHLCV array with at least 6 elements",
459 )));
460 }
461
462 let timestamp = arr[0]
463 .as_str()
464 .and_then(|s| s.parse::<i64>().ok())
465 .or_else(|| arr[0].as_i64())
466 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "timestamp")))?;
467
468 let open = arr[1]
469 .as_str()
470 .and_then(|s| s.parse::<f64>().ok())
471 .or_else(|| arr[1].as_f64())
472 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "open")))?;
473
474 let high = arr[2]
475 .as_str()
476 .and_then(|s| s.parse::<f64>().ok())
477 .or_else(|| arr[2].as_f64())
478 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "high")))?;
479
480 let low = arr[3]
481 .as_str()
482 .and_then(|s| s.parse::<f64>().ok())
483 .or_else(|| arr[3].as_f64())
484 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "low")))?;
485
486 let close = arr[4]
487 .as_str()
488 .and_then(|s| s.parse::<f64>().ok())
489 .or_else(|| arr[4].as_f64())
490 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "close")))?;
491
492 let volume = arr[5]
493 .as_str()
494 .and_then(|s| s.parse::<f64>().ok())
495 .or_else(|| arr[5].as_f64())
496 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "volume")))?;
497
498 Ok(OHLCV {
499 timestamp,
500 open,
501 high,
502 low,
503 close,
504 volume,
505 })
506 } else {
507 let timestamp = parse_timestamp(data, "startTime")
509 .or_else(|| parse_timestamp(data, "openTime"))
510 .ok_or_else(|| Error::from(ParseError::missing_field("startTime")))?;
511
512 let open = data["openPrice"]
513 .as_str()
514 .and_then(|s| s.parse::<f64>().ok())
515 .or_else(|| data["openPrice"].as_f64())
516 .or_else(|| data["open"].as_str().and_then(|s| s.parse::<f64>().ok()))
517 .or_else(|| data["open"].as_f64())
518 .ok_or_else(|| Error::from(ParseError::missing_field("openPrice")))?;
519
520 let high = data["highPrice"]
521 .as_str()
522 .and_then(|s| s.parse::<f64>().ok())
523 .or_else(|| data["highPrice"].as_f64())
524 .or_else(|| data["high"].as_str().and_then(|s| s.parse::<f64>().ok()))
525 .or_else(|| data["high"].as_f64())
526 .ok_or_else(|| Error::from(ParseError::missing_field("highPrice")))?;
527
528 let low = data["lowPrice"]
529 .as_str()
530 .and_then(|s| s.parse::<f64>().ok())
531 .or_else(|| data["lowPrice"].as_f64())
532 .or_else(|| data["low"].as_str().and_then(|s| s.parse::<f64>().ok()))
533 .or_else(|| data["low"].as_f64())
534 .ok_or_else(|| Error::from(ParseError::missing_field("lowPrice")))?;
535
536 let close = data["closePrice"]
537 .as_str()
538 .and_then(|s| s.parse::<f64>().ok())
539 .or_else(|| data["closePrice"].as_f64())
540 .or_else(|| data["close"].as_str().and_then(|s| s.parse::<f64>().ok()))
541 .or_else(|| data["close"].as_f64())
542 .ok_or_else(|| Error::from(ParseError::missing_field("closePrice")))?;
543
544 let volume = data["volume"]
545 .as_str()
546 .and_then(|s| s.parse::<f64>().ok())
547 .or_else(|| data["volume"].as_f64())
548 .ok_or_else(|| Error::from(ParseError::missing_field("volume")))?;
549
550 Ok(OHLCV {
551 timestamp,
552 open,
553 high,
554 low,
555 close,
556 volume,
557 })
558 }
559}
560
561pub fn parse_order_status(status: &str) -> OrderStatus {
583 match status {
584 "New" | "Created" | "Untriggered" => OrderStatus::Open,
585 "PartiallyFilled" => OrderStatus::Open,
586 "Filled" => OrderStatus::Closed,
587 "Cancelled" | "Canceled" | "PartiallyFilledCanceled" | "Deactivated" => {
588 OrderStatus::Canceled
589 }
590 "Rejected" => OrderStatus::Rejected,
591 "Expired" => OrderStatus::Expired,
592 "Triggered" => OrderStatus::Open,
593 _ => OrderStatus::Open, }
595}
596
597pub fn parse_order(data: &Value, market: Option<&Market>) -> Result<Order> {
608 let symbol = if let Some(m) = market {
609 m.symbol.clone()
610 } else {
611 data["symbol"]
612 .as_str()
613 .map(|s| s.to_string())
614 .unwrap_or_default()
615 };
616
617 let id = data["orderId"]
618 .as_str()
619 .ok_or_else(|| Error::from(ParseError::missing_field("orderId")))?
620 .to_string();
621
622 let timestamp =
623 parse_timestamp(data, "createdTime").or_else(|| parse_timestamp(data, "createTime"));
624
625 let status_str = data["orderStatus"].as_str().unwrap_or("New");
626 let status = parse_order_status(status_str);
627
628 let side = match data["side"].as_str() {
630 Some("Buy") | Some("buy") | Some("BUY") => OrderSide::Buy,
631 Some("Sell") | Some("sell") | Some("SELL") => OrderSide::Sell,
632 _ => return Err(Error::from(ParseError::invalid_format("data", "side"))),
633 };
634
635 let order_type = match data["orderType"].as_str() {
637 Some("Market") | Some("MARKET") => OrderType::Market,
638 Some("Limit") | Some("LIMIT") => OrderType::Limit,
639 _ => OrderType::Limit, };
641
642 let price = parse_decimal(data, "price");
643 let amount =
644 parse_decimal(data, "qty").ok_or_else(|| Error::from(ParseError::missing_field("qty")))?;
645 let filled = parse_decimal(data, "cumExecQty");
646 let remaining = match filled {
647 Some(f) => Some(amount - f),
648 None => Some(amount),
649 };
650
651 let average = parse_decimal(data, "avgPrice");
652
653 let cost = parse_decimal(data, "cumExecValue").or_else(|| match (filled, average) {
655 (Some(f), Some(avg)) => Some(f * avg),
656 _ => None,
657 });
658
659 Ok(Order {
660 id,
661 client_order_id: data["orderLinkId"].as_str().map(|s| s.to_string()),
662 timestamp,
663 datetime: timestamp.and_then(timestamp_to_datetime),
664 last_trade_timestamp: parse_timestamp(data, "updatedTime"),
665 status,
666 symbol,
667 order_type,
668 time_in_force: data["timeInForce"].as_str().map(|s| s.to_string()),
669 side,
670 price,
671 average,
672 amount,
673 filled,
674 remaining,
675 cost,
676 trades: None,
677 fee: None,
678 post_only: data["timeInForce"].as_str().map(|s| s == "PostOnly"),
679 reduce_only: data["reduceOnly"].as_bool(),
680 trigger_price: parse_decimal(data, "triggerPrice"),
681 stop_price: parse_decimal(data, "stopLoss"),
682 take_profit_price: parse_decimal(data, "takeProfit"),
683 stop_loss_price: parse_decimal(data, "stopLoss"),
684 trailing_delta: None,
685 trailing_percent: None,
686 activation_price: None,
687 callback_rate: None,
688 working_type: None,
689 fees: Some(Vec::new()),
690 info: value_to_hashmap(data),
691 })
692}
693
694pub fn parse_balance(data: &Value) -> Result<Balance> {
704 let mut balances = HashMap::new();
705
706 if let Some(coins) = data["coin"].as_array() {
708 for coin in coins {
709 parse_balance_entry(coin, &mut balances)?;
710 }
711 } else if let Some(list) = data["list"].as_array() {
712 for item in list {
714 if let Some(coins) = item["coin"].as_array() {
715 for coin in coins {
716 parse_balance_entry(coin, &mut balances)?;
717 }
718 }
719 }
720 } else {
721 parse_balance_entry(data, &mut balances)?;
723 }
724
725 Ok(Balance {
726 balances,
727 info: value_to_hashmap(data),
728 })
729}
730
731fn parse_balance_entry(data: &Value, balances: &mut HashMap<String, BalanceEntry>) -> Result<()> {
733 let currency = data["coin"]
734 .as_str()
735 .or_else(|| data["currency"].as_str())
736 .map(|s| s.to_string());
737
738 if let Some(currency) = currency {
739 let available = parse_decimal(data, "availableToWithdraw")
741 .or_else(|| parse_decimal(data, "free"))
742 .or_else(|| parse_decimal(data, "walletBalance"))
743 .unwrap_or(Decimal::ZERO);
744
745 let frozen = parse_decimal(data, "locked")
746 .or_else(|| parse_decimal(data, "frozen"))
747 .unwrap_or(Decimal::ZERO);
748
749 let total = parse_decimal(data, "walletBalance")
750 .or_else(|| parse_decimal(data, "equity"))
751 .unwrap_or(available + frozen);
752
753 if total > Decimal::ZERO {
755 balances.insert(
756 currency,
757 BalanceEntry {
758 free: available,
759 used: frozen,
760 total,
761 },
762 );
763 }
764 }
765
766 Ok(())
767}
768
769#[cfg(test)]
774mod tests {
775 use super::*;
776 use rust_decimal_macros::dec;
777 use serde_json::json;
778
779 #[test]
780 fn test_parse_market() {
781 let data = json!({
782 "symbol": "BTCUSDT",
783 "baseCoin": "BTC",
784 "quoteCoin": "USDT",
785 "status": "Trading",
786 "lotSizeFilter": {
787 "basePrecision": "0.0001",
788 "minOrderQty": "0.0001",
789 "maxOrderQty": "100"
790 },
791 "priceFilter": {
792 "tickSize": "0.01"
793 }
794 });
795
796 let market = parse_market(&data).unwrap();
797 assert_eq!(market.id, "BTCUSDT");
798 assert_eq!(market.symbol, "BTC/USDT");
799 assert_eq!(market.base, "BTC");
800 assert_eq!(market.quote, "USDT");
801 assert!(market.active);
802 assert_eq!(market.market_type, MarketType::Spot);
803 }
804
805 #[test]
806 fn test_parse_ticker() {
807 let data = json!({
808 "symbol": "BTCUSDT",
809 "lastPrice": "50000.00",
810 "highPrice24h": "51000.00",
811 "lowPrice24h": "49000.00",
812 "bid1Price": "49999.00",
813 "ask1Price": "50001.00",
814 "volume24h": "1000.5",
815 "time": "1700000000000"
816 });
817
818 let ticker = parse_ticker(&data, None).unwrap();
819 assert_eq!(ticker.symbol, "BTCUSDT");
820 assert_eq!(ticker.last, Some(Price::new(dec!(50000.00))));
821 assert_eq!(ticker.high, Some(Price::new(dec!(51000.00))));
822 assert_eq!(ticker.low, Some(Price::new(dec!(49000.00))));
823 assert_eq!(ticker.timestamp, 1700000000000);
824 }
825
826 #[test]
827 fn test_parse_orderbook() {
828 let data = json!({
829 "b": [
830 ["50000.00", "1.5"],
831 ["49999.00", "2.0"]
832 ],
833 "a": [
834 ["50001.00", "1.0"],
835 ["50002.00", "3.0"]
836 ],
837 "ts": "1700000000000"
838 });
839
840 let orderbook = parse_orderbook(&data, "BTC/USDT".to_string()).unwrap();
841 assert_eq!(orderbook.symbol, "BTC/USDT");
842 assert_eq!(orderbook.bids.len(), 2);
843 assert_eq!(orderbook.asks.len(), 2);
844 assert_eq!(orderbook.bids[0].price, Price::new(dec!(50000.00)));
845 assert_eq!(orderbook.asks[0].price, Price::new(dec!(50001.00)));
846 }
847
848 #[test]
849 fn test_parse_trade() {
850 let data = json!({
851 "execId": "123456",
852 "symbol": "BTCUSDT",
853 "side": "Buy",
854 "price": "50000.00",
855 "size": "0.5",
856 "time": "1700000000000"
857 });
858
859 let trade = parse_trade(&data, None).unwrap();
860 assert_eq!(trade.id, Some("123456".to_string()));
861 assert_eq!(trade.side, OrderSide::Buy);
862 assert_eq!(trade.price, Price::new(dec!(50000.00)));
863 assert_eq!(trade.amount, Amount::new(dec!(0.5)));
864 }
865
866 #[test]
867 fn test_parse_ohlcv_array() {
868 let data = json!([
869 "1700000000000",
870 "50000.00",
871 "51000.00",
872 "49000.00",
873 "50500.00",
874 "1000.5"
875 ]);
876
877 let ohlcv = parse_ohlcv(&data).unwrap();
878 assert_eq!(ohlcv.timestamp, 1700000000000);
879 assert_eq!(ohlcv.open, 50000.00);
880 assert_eq!(ohlcv.high, 51000.00);
881 assert_eq!(ohlcv.low, 49000.00);
882 assert_eq!(ohlcv.close, 50500.00);
883 assert_eq!(ohlcv.volume, 1000.5);
884 }
885
886 #[test]
887 fn test_parse_ohlcv_object() {
888 let data = json!({
889 "startTime": "1700000000000",
890 "openPrice": "50000.00",
891 "highPrice": "51000.00",
892 "lowPrice": "49000.00",
893 "closePrice": "50500.00",
894 "volume": "1000.5"
895 });
896
897 let ohlcv = parse_ohlcv(&data).unwrap();
898 assert_eq!(ohlcv.timestamp, 1700000000000);
899 assert_eq!(ohlcv.open, 50000.00);
900 assert_eq!(ohlcv.high, 51000.00);
901 assert_eq!(ohlcv.low, 49000.00);
902 assert_eq!(ohlcv.close, 50500.00);
903 assert_eq!(ohlcv.volume, 1000.5);
904 }
905
906 #[test]
907 fn test_parse_order_status() {
908 assert_eq!(parse_order_status("New"), OrderStatus::Open);
909 assert_eq!(parse_order_status("PartiallyFilled"), OrderStatus::Open);
910 assert_eq!(parse_order_status("Filled"), OrderStatus::Closed);
911 assert_eq!(parse_order_status("Cancelled"), OrderStatus::Canceled);
912 assert_eq!(parse_order_status("Rejected"), OrderStatus::Rejected);
913 assert_eq!(parse_order_status("Expired"), OrderStatus::Expired);
914 }
915
916 #[test]
917 fn test_parse_order() {
918 let data = json!({
919 "orderId": "123456789",
920 "symbol": "BTCUSDT",
921 "side": "Buy",
922 "orderType": "Limit",
923 "price": "50000.00",
924 "qty": "0.5",
925 "orderStatus": "New",
926 "createdTime": "1700000000000"
927 });
928
929 let order = parse_order(&data, None).unwrap();
930 assert_eq!(order.id, "123456789");
931 assert_eq!(order.side, OrderSide::Buy);
932 assert_eq!(order.order_type, OrderType::Limit);
933 assert_eq!(order.price, Some(dec!(50000.00)));
934 assert_eq!(order.amount, dec!(0.5));
935 assert_eq!(order.status, OrderStatus::Open);
936 }
937
938 #[test]
939 fn test_parse_balance() {
940 let data = json!({
941 "coin": [
942 {
943 "coin": "BTC",
944 "walletBalance": "2.0",
945 "availableToWithdraw": "1.5",
946 "locked": "0.5"
947 },
948 {
949 "coin": "USDT",
950 "walletBalance": "10000.00",
951 "availableToWithdraw": "10000.00",
952 "locked": "0"
953 }
954 ]
955 });
956
957 let balance = parse_balance(&data).unwrap();
958 let btc = balance.get("BTC").unwrap();
959 assert_eq!(btc.free, dec!(1.5));
960 assert_eq!(btc.used, dec!(0.5));
961 assert_eq!(btc.total, dec!(2.0));
962
963 let usdt = balance.get("USDT").unwrap();
964 assert_eq!(usdt.free, dec!(10000.00));
965 assert_eq!(usdt.total, dec!(10000.00));
966 }
967
968 #[test]
969 fn test_timestamp_to_datetime() {
970 let ts = 1700000000000i64;
971 let dt = timestamp_to_datetime(ts).unwrap();
972 assert!(dt.contains("2023-11-14"));
973 }
974}