1use ccxt_core::{
6 Result,
7 error::{Error, ParseError},
8 parser_utils::{parse_decimal, parse_timestamp, value_to_hashmap},
9 types::{
10 Balance, BalanceEntry, Market, MarketLimits, MarketPrecision, MarketType, MinMax, OHLCV,
11 Order, OrderBook, OrderBookEntry, OrderSide, OrderStatus, OrderType, Ticker, Trade,
12 financial::{Amount, Cost, Price},
13 },
14};
15use rust_decimal::Decimal;
16use rust_decimal::prelude::{FromPrimitive, FromStr};
17use serde_json::Value;
18use std::collections::HashMap;
19
20pub use ccxt_core::parser_utils::{datetime_to_timestamp, timestamp_to_datetime};
22
23pub fn parse_market(data: &Value) -> Result<Market> {
39 let id = data["symbol"]
41 .as_str()
42 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
43 .to_string();
44
45 let base = data["baseCoin"]
47 .as_str()
48 .ok_or_else(|| Error::from(ParseError::missing_field("baseCoin")))?
49 .to_string();
50
51 let quote = data["quoteCoin"]
52 .as_str()
53 .ok_or_else(|| Error::from(ParseError::missing_field("quoteCoin")))?
54 .to_string();
55
56 let contract_type = data["contractType"].as_str();
58 let market_type = match contract_type {
59 Some("LinearPerpetual" | "InversePerpetual") => MarketType::Swap,
60 Some("LinearFutures" | "InverseFutures") => MarketType::Futures,
61 _ => MarketType::Spot,
62 };
63
64 let status = data["status"].as_str().unwrap_or("Trading");
66 let active = status == "Trading";
67
68 let price_precision = parse_decimal(data, "priceFilter").or_else(|| {
70 data.get("priceFilter")
71 .and_then(|pf| parse_decimal(pf, "tickSize"))
72 });
73 let amount_precision = parse_decimal(data, "lotSizeFilter").or_else(|| {
74 data.get("lotSizeFilter")
75 .and_then(|lf| parse_decimal(lf, "basePrecision"))
76 });
77
78 let (min_amount, max_amount) = if let Some(lot_filter) = data.get("lotSizeFilter") {
80 (
81 parse_decimal(lot_filter, "minOrderQty"),
82 parse_decimal(lot_filter, "maxOrderQty"),
83 )
84 } else {
85 (None, None)
86 };
87
88 let contract = market_type != MarketType::Spot;
90 let linear = if contract {
91 Some(contract_type == Some("LinearPerpetual") || contract_type == Some("LinearFutures"))
92 } else {
93 None
94 };
95 let inverse = if contract {
96 Some(contract_type == Some("InversePerpetual") || contract_type == Some("InverseFutures"))
97 } else {
98 None
99 };
100 let contract_size = parse_decimal(data, "contractSize");
101
102 let settle = data["settleCoin"].as_str().map(ToString::to_string);
104 let settle_id = settle.clone();
105
106 let expiry = parse_timestamp(data, "deliveryTime");
108 let expiry_datetime = expiry.and_then(timestamp_to_datetime);
109
110 let symbol = match market_type {
115 MarketType::Swap => {
116 if let Some(ref s) = settle {
117 format!("{}/{}:{}", base, quote, s)
118 } else if linear == Some(true) {
119 format!("{}/{}:{}", base, quote, quote)
121 } else {
122 format!("{}/{}:{}", base, quote, base)
124 }
125 }
126 MarketType::Futures => {
127 let settle_ccy = settle.clone().unwrap_or_else(|| {
128 if linear == Some(true) {
129 quote.clone()
130 } else {
131 base.clone()
132 }
133 });
134 if let Some(exp_ts) = expiry {
135 if let Some(dt) = chrono::DateTime::from_timestamp_millis(exp_ts) {
137 let year = (dt.format("%y").to_string().parse::<u8>()).unwrap_or(0);
138 let month = (dt.format("%m").to_string().parse::<u8>()).unwrap_or(1);
139 let day = (dt.format("%d").to_string().parse::<u8>()).unwrap_or(1);
140 format!(
141 "{}/{}:{}-{:02}{:02}{:02}",
142 base, quote, settle_ccy, year, month, day
143 )
144 } else {
145 format!("{}/{}:{}", base, quote, settle_ccy)
146 }
147 } else {
148 format!("{}/{}:{}", base, quote, settle_ccy)
149 }
150 }
151 _ => format!("{}/{}", base, quote), };
153
154 let parsed_symbol = ccxt_core::symbol::SymbolParser::parse(&symbol).ok();
156
157 Ok(Market {
158 id,
159 symbol,
160 parsed_symbol,
161 base: base.clone(),
162 quote: quote.clone(),
163 settle,
164 base_id: Some(base),
165 quote_id: Some(quote),
166 settle_id,
167 market_type,
168 active,
169 margin: contract,
170 contract: Some(contract),
171 linear,
172 inverse,
173 contract_size,
174 expiry,
175 expiry_datetime,
176 strike: None,
177 option_type: None,
178 precision: MarketPrecision {
179 price: price_precision,
180 amount: amount_precision,
181 base: None,
182 quote: None,
183 },
184 limits: MarketLimits {
185 amount: Some(MinMax {
186 min: min_amount,
187 max: max_amount,
188 }),
189 price: None,
190 cost: None,
191 leverage: None,
192 },
193 maker: parse_decimal(data, "makerFeeRate"),
194 taker: parse_decimal(data, "takerFeeRate"),
195 percentage: Some(true),
196 tier_based: Some(false),
197 fee_side: Some("quote".to_string()),
198 info: value_to_hashmap(data),
199 })
200}
201
202pub fn parse_ticker(data: &Value, market: Option<&Market>) -> Result<Ticker> {
213 let symbol = if let Some(m) = market {
214 m.symbol.clone()
215 } else {
216 data["symbol"]
219 .as_str()
220 .map(ToString::to_string)
221 .ok_or_else(|| {
222 ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol"))
223 .context("Failed to parse ticker: missing symbol identifier")
224 })?
225 };
226
227 let timestamp = parse_timestamp(data, "time")
229 .or_else(|| parse_timestamp(data, "timestamp"))
230 .unwrap_or(0);
231
232 Ok(Ticker {
233 symbol,
234 timestamp,
235 datetime: timestamp_to_datetime(timestamp),
236 high: parse_decimal(data, "highPrice24h").map(Price::new),
237 low: parse_decimal(data, "lowPrice24h").map(Price::new),
238 bid: parse_decimal(data, "bid1Price").map(Price::new),
239 bid_volume: parse_decimal(data, "bid1Size").map(Amount::new),
240 ask: parse_decimal(data, "ask1Price").map(Price::new),
241 ask_volume: parse_decimal(data, "ask1Size").map(Amount::new),
242 vwap: None,
243 open: parse_decimal(data, "prevPrice24h").map(Price::new),
244 close: parse_decimal(data, "lastPrice").map(Price::new),
245 last: parse_decimal(data, "lastPrice").map(Price::new),
246 previous_close: parse_decimal(data, "prevPrice24h").map(Price::new),
247 change: parse_decimal(data, "price24hPcnt")
248 .and_then(|pct| parse_decimal(data, "prevPrice24h").map(|prev| Price::new(prev * pct))),
249 percentage: parse_decimal(data, "price24hPcnt").map(|p| p * Decimal::from(100)),
250 average: None,
251 base_volume: parse_decimal(data, "volume24h").map(Amount::new),
252 quote_volume: parse_decimal(data, "turnover24h").map(Amount::new),
253 funding_rate: parse_decimal(data, "fundingRate"),
254 open_interest: parse_decimal(data, "openInterest"),
255 index_price: parse_decimal(data, "indexPrice").map(Price::new),
256 mark_price: parse_decimal(data, "markPrice").map(Price::new),
257 info: value_to_hashmap(data),
258 })
259}
260
261pub fn parse_orderbook(data: &Value, symbol: String) -> Result<OrderBook> {
273 let timestamp = parse_timestamp(data, "ts")
274 .or_else(|| parse_timestamp(data, "time"))
275 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
276
277 let mut bids = parse_orderbook_side(&data["b"])?;
278 let mut asks = parse_orderbook_side(&data["a"])?;
279
280 bids.sort_by(|a, b| b.price.cmp(&a.price));
282
283 asks.sort_by(|a, b| a.price.cmp(&b.price));
285
286 Ok(OrderBook {
287 symbol,
288 timestamp,
289 datetime: timestamp_to_datetime(timestamp),
290 nonce: None,
291 bids,
292 asks,
293 buffered_deltas: std::collections::VecDeque::new(),
294 bids_map: std::collections::BTreeMap::new(),
295 asks_map: std::collections::BTreeMap::new(),
296 is_synced: false,
297 needs_resync: false,
298 last_resync_time: 0,
299 info: value_to_hashmap(data),
300 })
301}
302
303fn parse_orderbook_side(data: &Value) -> Result<Vec<OrderBookEntry>> {
305 let Some(array) = data.as_array() else {
306 return Ok(Vec::new());
307 };
308
309 let mut result = Vec::new();
310
311 for item in array {
312 if let Some(arr) = item.as_array() {
313 if arr.len() >= 2 {
315 let price = arr[0]
316 .as_str()
317 .and_then(|s| Decimal::from_str(s).ok())
318 .or_else(|| arr[0].as_f64().and_then(Decimal::from_f64))
319 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "price")))?;
320
321 let amount = arr[1]
322 .as_str()
323 .and_then(|s| Decimal::from_str(s).ok())
324 .or_else(|| arr[1].as_f64().and_then(Decimal::from_f64))
325 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "amount")))?;
326
327 result.push(OrderBookEntry {
328 price: Price::new(price),
329 amount: Amount::new(amount),
330 });
331 }
332 }
333 }
334
335 Ok(result)
336}
337
338pub fn parse_trade(data: &Value, market: Option<&Market>) -> Result<Trade> {
349 let symbol = if let Some(m) = market {
350 m.symbol.clone()
351 } else {
352 data["symbol"]
354 .as_str()
355 .or_else(|| data["s"].as_str())
356 .map(ToString::to_string)
357 .ok_or_else(|| {
358 ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol/s"))
359 .context("Failed to parse: missing symbol identifier")
360 })?
361 };
362
363 let id = data["execId"]
364 .as_str()
365 .or_else(|| data["id"].as_str())
366 .map(ToString::to_string);
367
368 let timestamp = parse_timestamp(data, "time")
369 .or_else(|| parse_timestamp(data, "T"))
370 .unwrap_or(0);
371
372 let side = match data["side"].as_str() {
374 Some("Sell" | "sell" | "SELL") => OrderSide::Sell,
375 _ => OrderSide::Buy, };
377
378 let price = parse_decimal(data, "price").or_else(|| parse_decimal(data, "execPrice"));
379 let amount = parse_decimal(data, "size")
380 .or_else(|| parse_decimal(data, "execQty"))
381 .or_else(|| parse_decimal(data, "qty"));
382
383 let cost = match (price, amount) {
384 (Some(p), Some(a)) => Some(p * a),
385 _ => None,
386 };
387
388 Ok(Trade {
389 id,
390 order: data["orderId"].as_str().map(ToString::to_string),
391 timestamp,
392 datetime: timestamp_to_datetime(timestamp),
393 symbol,
394 trade_type: None,
395 side,
396 taker_or_maker: None,
397 price: Price::new(price.unwrap_or(Decimal::ZERO)),
398 amount: Amount::new(amount.unwrap_or(Decimal::ZERO)),
399 cost: cost.map(Cost::new),
400 fee: None,
401 info: value_to_hashmap(data),
402 })
403}
404
405pub fn parse_ohlcv(data: &Value) -> Result<OHLCV> {
415 if let Some(arr) = data.as_array() {
418 if arr.len() < 6 {
419 return Err(Error::from(ParseError::invalid_format(
420 "data",
421 "OHLCV array with at least 6 elements",
422 )));
423 }
424
425 let timestamp = arr[0]
426 .as_str()
427 .and_then(|s| s.parse::<i64>().ok())
428 .or_else(|| arr[0].as_i64())
429 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "timestamp")))?;
430
431 let open = arr[1]
432 .as_str()
433 .and_then(|s| s.parse::<f64>().ok())
434 .or_else(|| arr[1].as_f64())
435 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "open")))?;
436
437 let high = arr[2]
438 .as_str()
439 .and_then(|s| s.parse::<f64>().ok())
440 .or_else(|| arr[2].as_f64())
441 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "high")))?;
442
443 let low = arr[3]
444 .as_str()
445 .and_then(|s| s.parse::<f64>().ok())
446 .or_else(|| arr[3].as_f64())
447 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "low")))?;
448
449 let close = arr[4]
450 .as_str()
451 .and_then(|s| s.parse::<f64>().ok())
452 .or_else(|| arr[4].as_f64())
453 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "close")))?;
454
455 let volume = arr[5]
456 .as_str()
457 .and_then(|s| s.parse::<f64>().ok())
458 .or_else(|| arr[5].as_f64())
459 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "volume")))?;
460
461 Ok(OHLCV {
462 timestamp,
463 open,
464 high,
465 low,
466 close,
467 volume,
468 })
469 } else {
470 let timestamp = parse_timestamp(data, "startTime")
472 .or_else(|| parse_timestamp(data, "openTime"))
473 .ok_or_else(|| Error::from(ParseError::missing_field("startTime")))?;
474
475 let open = data["openPrice"]
476 .as_str()
477 .and_then(|s| s.parse::<f64>().ok())
478 .or_else(|| data["openPrice"].as_f64())
479 .or_else(|| data["open"].as_str().and_then(|s| s.parse::<f64>().ok()))
480 .or_else(|| data["open"].as_f64())
481 .ok_or_else(|| Error::from(ParseError::missing_field("openPrice")))?;
482
483 let high = data["highPrice"]
484 .as_str()
485 .and_then(|s| s.parse::<f64>().ok())
486 .or_else(|| data["highPrice"].as_f64())
487 .or_else(|| data["high"].as_str().and_then(|s| s.parse::<f64>().ok()))
488 .or_else(|| data["high"].as_f64())
489 .ok_or_else(|| Error::from(ParseError::missing_field("highPrice")))?;
490
491 let low = data["lowPrice"]
492 .as_str()
493 .and_then(|s| s.parse::<f64>().ok())
494 .or_else(|| data["lowPrice"].as_f64())
495 .or_else(|| data["low"].as_str().and_then(|s| s.parse::<f64>().ok()))
496 .or_else(|| data["low"].as_f64())
497 .ok_or_else(|| Error::from(ParseError::missing_field("lowPrice")))?;
498
499 let close = data["closePrice"]
500 .as_str()
501 .and_then(|s| s.parse::<f64>().ok())
502 .or_else(|| data["closePrice"].as_f64())
503 .or_else(|| data["close"].as_str().and_then(|s| s.parse::<f64>().ok()))
504 .or_else(|| data["close"].as_f64())
505 .ok_or_else(|| Error::from(ParseError::missing_field("closePrice")))?;
506
507 let volume = data["volume"]
508 .as_str()
509 .and_then(|s| s.parse::<f64>().ok())
510 .or_else(|| data["volume"].as_f64())
511 .ok_or_else(|| Error::from(ParseError::missing_field("volume")))?;
512
513 Ok(OHLCV {
514 timestamp,
515 open,
516 high,
517 low,
518 close,
519 volume,
520 })
521 }
522}
523
524pub fn parse_order_status(status: &str) -> OrderStatus {
546 match status {
547 "Filled" => OrderStatus::Closed,
548 "Cancelled" | "Canceled" | "PartiallyFilledCanceled" | "Deactivated" => {
549 OrderStatus::Cancelled
550 }
551 "Rejected" => OrderStatus::Rejected,
552 "Expired" => OrderStatus::Expired,
553 _ => OrderStatus::Open, }
555}
556
557pub fn parse_order(data: &Value, market: Option<&Market>) -> Result<Order> {
568 let symbol = if let Some(m) = market {
569 m.symbol.clone()
570 } else {
571 data["symbol"]
573 .as_str()
574 .or_else(|| data["s"].as_str())
575 .map(ToString::to_string)
576 .ok_or_else(|| {
577 ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol/s"))
578 .context("Failed to parse: missing symbol identifier")
579 })?
580 };
581
582 let id = data["orderId"]
583 .as_str()
584 .ok_or_else(|| Error::from(ParseError::missing_field("orderId")))?
585 .to_string();
586
587 let timestamp =
588 parse_timestamp(data, "createdTime").or_else(|| parse_timestamp(data, "createTime"));
589
590 let status_str = data["orderStatus"].as_str().unwrap_or("New");
591 let status = parse_order_status(status_str);
592
593 let side = match data["side"].as_str() {
595 Some("Buy" | "buy" | "BUY") => OrderSide::Buy,
596 Some("Sell" | "sell" | "SELL") => OrderSide::Sell,
597 _ => return Err(Error::from(ParseError::invalid_format("data", "side"))),
598 };
599
600 let order_type = match data["orderType"].as_str() {
602 Some("Market" | "MARKET") => OrderType::Market,
603 _ => OrderType::Limit, };
605
606 let price = parse_decimal(data, "price");
607 let amount =
608 parse_decimal(data, "qty").ok_or_else(|| Error::from(ParseError::missing_field("qty")))?;
609 let filled = parse_decimal(data, "cumExecQty");
610 let remaining = match filled {
611 Some(f) => Some(amount - f),
612 None => Some(amount),
613 };
614
615 let average = parse_decimal(data, "avgPrice");
616
617 let cost = parse_decimal(data, "cumExecValue").or_else(|| match (filled, average) {
619 (Some(f), Some(avg)) => Some(f * avg),
620 _ => None,
621 });
622
623 Ok(Order {
624 id,
625 client_order_id: data["orderLinkId"].as_str().map(ToString::to_string),
626 timestamp,
627 datetime: timestamp.and_then(timestamp_to_datetime),
628 last_trade_timestamp: parse_timestamp(data, "updatedTime"),
629 status,
630 symbol,
631 order_type,
632 time_in_force: data["timeInForce"].as_str().map(ToString::to_string),
633 side,
634 price,
635 average,
636 amount,
637 filled,
638 remaining,
639 cost,
640 trades: None,
641 fee: None,
642 post_only: data["timeInForce"].as_str().map(|s| s == "PostOnly"),
643 reduce_only: data["reduceOnly"].as_bool(),
644 trigger_price: parse_decimal(data, "triggerPrice"),
645 stop_price: parse_decimal(data, "stopLoss"),
646 take_profit_price: parse_decimal(data, "takeProfit"),
647 stop_loss_price: parse_decimal(data, "stopLoss"),
648 trailing_delta: None,
649 trailing_percent: None,
650 activation_price: None,
651 callback_rate: None,
652 working_type: None,
653 fees: Some(Vec::new()),
654 info: value_to_hashmap(data),
655 })
656}
657
658pub fn parse_balance(data: &Value) -> Result<Balance> {
668 let mut balances = HashMap::new();
669
670 if let Some(coins) = data["coin"].as_array() {
672 for coin in coins {
673 parse_balance_entry(coin, &mut balances);
674 }
675 } else if let Some(list) = data["list"].as_array() {
676 for item in list {
678 if let Some(coins) = item["coin"].as_array() {
679 for coin in coins {
680 parse_balance_entry(coin, &mut balances);
681 }
682 }
683 }
684 } else {
685 parse_balance_entry(data, &mut balances);
687 }
688
689 Ok(Balance {
690 balances,
691 info: value_to_hashmap(data),
692 })
693}
694
695fn parse_balance_entry(data: &Value, balances: &mut HashMap<String, BalanceEntry>) {
697 let currency = data["coin"]
698 .as_str()
699 .or_else(|| data["currency"].as_str())
700 .map(ToString::to_string);
701
702 if let Some(currency) = currency {
703 let available = parse_decimal(data, "availableToWithdraw")
705 .or_else(|| parse_decimal(data, "free"))
706 .or_else(|| parse_decimal(data, "walletBalance"))
707 .unwrap_or(Decimal::ZERO);
708
709 let frozen = parse_decimal(data, "locked")
710 .or_else(|| parse_decimal(data, "frozen"))
711 .unwrap_or(Decimal::ZERO);
712
713 let total = parse_decimal(data, "walletBalance")
714 .or_else(|| parse_decimal(data, "equity"))
715 .unwrap_or(available + frozen);
716
717 if total > Decimal::ZERO {
719 balances.insert(
720 currency,
721 BalanceEntry {
722 free: available,
723 used: frozen,
724 total,
725 },
726 );
727 }
728 }
729}
730
731#[cfg(test)]
736mod tests {
737 use super::*;
738 use rust_decimal_macros::dec;
739 use serde_json::json;
740
741 #[test]
742 fn test_parse_market() {
743 let data = json!({
744 "symbol": "BTCUSDT",
745 "baseCoin": "BTC",
746 "quoteCoin": "USDT",
747 "status": "Trading",
748 "lotSizeFilter": {
749 "basePrecision": "0.0001",
750 "minOrderQty": "0.0001",
751 "maxOrderQty": "100"
752 },
753 "priceFilter": {
754 "tickSize": "0.01"
755 }
756 });
757
758 let market = parse_market(&data).unwrap();
759 assert_eq!(market.id, "BTCUSDT");
760 assert_eq!(market.symbol, "BTC/USDT");
761 assert_eq!(market.base, "BTC");
762 assert_eq!(market.quote, "USDT");
763 assert!(market.active);
764 assert_eq!(market.market_type, MarketType::Spot);
765 }
766
767 #[test]
768 fn test_parse_ticker() {
769 let data = json!({
770 "symbol": "BTCUSDT",
771 "lastPrice": "50000.00",
772 "highPrice24h": "51000.00",
773 "lowPrice24h": "49000.00",
774 "bid1Price": "49999.00",
775 "ask1Price": "50001.00",
776 "volume24h": "1000.5",
777 "time": "1700000000000"
778 });
779
780 let ticker = parse_ticker(&data, None).unwrap();
781 assert_eq!(ticker.symbol, "BTCUSDT");
782 assert_eq!(ticker.last, Some(Price::new(dec!(50000.00))));
783 assert_eq!(ticker.high, Some(Price::new(dec!(51000.00))));
784 assert_eq!(ticker.low, Some(Price::new(dec!(49000.00))));
785 assert_eq!(ticker.timestamp, 1700000000000);
786 }
787
788 #[test]
789 fn test_parse_orderbook() {
790 let data = json!({
791 "b": [
792 ["50000.00", "1.5"],
793 ["49999.00", "2.0"]
794 ],
795 "a": [
796 ["50001.00", "1.0"],
797 ["50002.00", "3.0"]
798 ],
799 "ts": "1700000000000"
800 });
801
802 let orderbook = parse_orderbook(&data, "BTC/USDT".to_string()).unwrap();
803 assert_eq!(orderbook.symbol, "BTC/USDT");
804 assert_eq!(orderbook.bids.len(), 2);
805 assert_eq!(orderbook.asks.len(), 2);
806 assert_eq!(orderbook.bids[0].price, Price::new(dec!(50000.00)));
807 assert_eq!(orderbook.asks[0].price, Price::new(dec!(50001.00)));
808 }
809
810 #[test]
811 fn test_parse_trade() {
812 let data = json!({
813 "execId": "123456",
814 "symbol": "BTCUSDT",
815 "side": "Buy",
816 "price": "50000.00",
817 "size": "0.5",
818 "time": "1700000000000"
819 });
820
821 let trade = parse_trade(&data, None).unwrap();
822 assert_eq!(trade.id, Some("123456".to_string()));
823 assert_eq!(trade.side, OrderSide::Buy);
824 assert_eq!(trade.price, Price::new(dec!(50000.00)));
825 assert_eq!(trade.amount, Amount::new(dec!(0.5)));
826 }
827
828 #[test]
829 fn test_parse_ohlcv_array() {
830 let data = json!([
831 "1700000000000",
832 "50000.00",
833 "51000.00",
834 "49000.00",
835 "50500.00",
836 "1000.5"
837 ]);
838
839 let ohlcv = parse_ohlcv(&data).unwrap();
840 assert_eq!(ohlcv.timestamp, 1700000000000);
841 assert_eq!(ohlcv.open, 50000.00);
842 assert_eq!(ohlcv.high, 51000.00);
843 assert_eq!(ohlcv.low, 49000.00);
844 assert_eq!(ohlcv.close, 50500.00);
845 assert_eq!(ohlcv.volume, 1000.5);
846 }
847
848 #[test]
849 fn test_parse_ohlcv_object() {
850 let data = json!({
851 "startTime": "1700000000000",
852 "openPrice": "50000.00",
853 "highPrice": "51000.00",
854 "lowPrice": "49000.00",
855 "closePrice": "50500.00",
856 "volume": "1000.5"
857 });
858
859 let ohlcv = parse_ohlcv(&data).unwrap();
860 assert_eq!(ohlcv.timestamp, 1700000000000);
861 assert_eq!(ohlcv.open, 50000.00);
862 assert_eq!(ohlcv.high, 51000.00);
863 assert_eq!(ohlcv.low, 49000.00);
864 assert_eq!(ohlcv.close, 50500.00);
865 assert_eq!(ohlcv.volume, 1000.5);
866 }
867
868 #[test]
869 fn test_parse_order_status() {
870 assert_eq!(parse_order_status("New"), OrderStatus::Open);
871 assert_eq!(parse_order_status("PartiallyFilled"), OrderStatus::Open);
872 assert_eq!(parse_order_status("Filled"), OrderStatus::Closed);
873 assert_eq!(parse_order_status("Cancelled"), OrderStatus::Cancelled);
874 assert_eq!(parse_order_status("Rejected"), OrderStatus::Rejected);
875 assert_eq!(parse_order_status("Expired"), OrderStatus::Expired);
876 }
877
878 #[test]
879 fn test_parse_order() {
880 let data = json!({
881 "orderId": "123456789",
882 "symbol": "BTCUSDT",
883 "side": "Buy",
884 "orderType": "Limit",
885 "price": "50000.00",
886 "qty": "0.5",
887 "orderStatus": "New",
888 "createdTime": "1700000000000"
889 });
890
891 let order = parse_order(&data, None).unwrap();
892 assert_eq!(order.id, "123456789");
893 assert_eq!(order.side, OrderSide::Buy);
894 assert_eq!(order.order_type, OrderType::Limit);
895 assert_eq!(order.price, Some(dec!(50000.00)));
896 assert_eq!(order.amount, dec!(0.5));
897 assert_eq!(order.status, OrderStatus::Open);
898 }
899
900 #[test]
901 fn test_parse_balance() {
902 let data = json!({
903 "coin": [
904 {
905 "coin": "BTC",
906 "walletBalance": "2.0",
907 "availableToWithdraw": "1.5",
908 "locked": "0.5"
909 },
910 {
911 "coin": "USDT",
912 "walletBalance": "10000.00",
913 "availableToWithdraw": "10000.00",
914 "locked": "0"
915 }
916 ]
917 });
918
919 let balance = parse_balance(&data).unwrap();
920 let btc = balance.get("BTC").unwrap();
921 assert_eq!(btc.free, dec!(1.5));
922 assert_eq!(btc.used, dec!(0.5));
923 assert_eq!(btc.total, dec!(2.0));
924
925 let usdt = balance.get("USDT").unwrap();
926 assert_eq!(usdt.free, dec!(10000.00));
927 assert_eq!(usdt.total, dec!(10000.00));
928 }
929
930 #[test]
931 fn test_timestamp_to_datetime() {
932 let ts = 1700000000000i64;
933 let dt = timestamp_to_datetime(ts).unwrap();
934 assert!(dt.contains("2023-11-14"));
935 }
936}