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