1use ccxt_core::{
6 Result,
7 error::{Error, ParseError},
8 types::{
9 AccountConfig, Balance, BalanceEntry, BidAsk, BorrowInterest, BorrowRate,
10 BorrowRateHistory, CommissionRate, DepositWithdrawFee, FeeFundingRate,
11 FeeFundingRateHistory, FeeTradingFee, FundingFee, FundingHistory, IndexPrice,
12 LedgerDirection, LedgerEntry, LedgerEntryType, Leverage, LeverageTier, Liquidation,
13 MarginAdjustment, MarginLoan, MarginType, MarkPrice, Market, MaxBorrowable, MaxLeverage,
14 MaxTransferable, NextFundingRate, OHLCV, OcoOrder, OcoOrderInfo, OpenInterest,
15 OpenInterestHistory, Order, OrderBook, OrderBookDelta, OrderBookEntry, OrderReport,
16 OrderSide, OrderStatus, OrderType, Position, PremiumIndex, TakerOrMaker, Ticker,
17 TimeInForce, Trade, Transfer,
18 financial::{Amount, Cost, Price},
19 },
20};
21use rust_decimal::Decimal;
22use rust_decimal::prelude::{FromPrimitive, FromStr, ToPrimitive};
23use serde_json::Value;
24use std::collections::HashMap;
25
26fn parse_f64(data: &Value, key: &str) -> Option<f64> {
32 data.get(key).and_then(|v| {
33 v.as_f64()
34 .or_else(|| v.as_str().and_then(|s| s.parse::<f64>().ok()))
35 })
36}
37
38fn parse_decimal(data: &Value, key: &str) -> Option<Decimal> {
41 data.get(key).and_then(|v| {
42 if let Some(s) = v.as_str() {
44 Decimal::from_str(s).ok()
45 } else if let Some(num) = v.as_f64() {
46 Decimal::from_f64(num)
48 } else {
49 None
50 }
51 })
52}
53
54fn parse_decimal_multi(data: &Value, keys: &[&str]) -> Option<Decimal> {
57 for key in keys {
58 if let Some(decimal) = parse_decimal(data, key) {
59 return Some(decimal);
60 }
61 }
62 None
63}
64
65fn value_to_hashmap(data: &Value) -> HashMap<String, Value> {
67 data.as_object()
68 .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
69 .unwrap_or_default()
70}
71
72#[allow(dead_code)]
74fn parse_order_book_entries(data: &Value) -> Vec<OrderBookEntry> {
75 data.as_array()
76 .map(|arr| {
77 arr.iter()
78 .filter_map(|item| {
79 let price = if let Some(arr) = item.as_array() {
80 arr.get(0)
81 .and_then(|v| v.as_str())
82 .and_then(|s| Decimal::from_str(s).ok())
83 } else {
84 None
85 }?;
86
87 let amount = if let Some(arr) = item.as_array() {
88 arr.get(1)
89 .and_then(|v| v.as_str())
90 .and_then(|s| Decimal::from_str(s).ok())
91 } else {
92 None
93 }?;
94
95 Some(OrderBookEntry {
96 price: Price::new(price),
97 amount: Amount::new(amount),
98 })
99 })
100 .collect()
101 })
102 .unwrap_or_default()
103}
104
105pub fn parse_market(data: &Value) -> Result<Market> {
123 use ccxt_core::types::{MarketLimits, MarketPrecision, MarketType, MinMax};
124
125 let symbol = data["symbol"]
126 .as_str()
127 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
128 .to_string();
129
130 let base_asset = data["baseAsset"]
131 .as_str()
132 .ok_or_else(|| Error::from(ParseError::missing_field("baseAsset")))?
133 .to_string();
134
135 let quote_asset = data["quoteAsset"]
136 .as_str()
137 .ok_or_else(|| Error::from(ParseError::missing_field("quoteAsset")))?
138 .to_string();
139
140 let status = data["status"]
141 .as_str()
142 .ok_or_else(|| Error::from(ParseError::missing_field("status")))?;
143
144 let active = Some(status == "TRADING");
145
146 let margin = data["isMarginTradingAllowed"].as_bool().unwrap_or(false);
148
149 let mut price_precision: Option<Decimal> = None;
151 let mut amount_precision: Option<Decimal> = None;
152 let mut min_amount: Option<Decimal> = None;
153 let mut max_amount: Option<Decimal> = None;
154 let mut min_cost: Option<Decimal> = None;
155 let mut min_price: Option<Decimal> = None;
156 let mut max_price: Option<Decimal> = None;
157
158 if let Some(filters) = data["filters"].as_array() {
159 for filter in filters {
160 let filter_type = filter["filterType"].as_str().unwrap_or("");
161
162 match filter_type {
163 "PRICE_FILTER" => {
164 if let Some(tick_size) = filter["tickSize"].as_str() {
165 if let Ok(dec) = Decimal::from_str(tick_size) {
166 price_precision = Some(dec);
167 }
168 }
169 if let Some(min) = filter["minPrice"].as_str() {
170 min_price = Decimal::from_str(min).ok();
171 }
172 if let Some(max) = filter["maxPrice"].as_str() {
173 max_price = Decimal::from_str(max).ok();
174 }
175 }
176 "LOT_SIZE" => {
177 if let Some(step_size) = filter["stepSize"].as_str() {
178 if let Ok(dec) = Decimal::from_str(step_size) {
179 amount_precision = Some(dec);
180 }
181 }
182 if let Some(min) = filter["minQty"].as_str() {
183 min_amount = Decimal::from_str(min).ok();
184 }
185 if let Some(max) = filter["maxQty"].as_str() {
186 max_amount = Decimal::from_str(max).ok();
187 }
188 }
189 "MIN_NOTIONAL" | "NOTIONAL" => {
190 if let Some(min) = filter["minNotional"].as_str() {
191 min_cost = Decimal::from_str(min).ok();
192 }
193 }
194 _ => {}
195 }
196 }
197 }
198
199 let unified_symbol = format!("{}/{}", base_asset, quote_asset);
201 let parsed_symbol = ccxt_core::symbol::SymbolParser::parse(&unified_symbol).ok();
203
204 Ok(Market {
205 id: symbol.clone(),
206 symbol: unified_symbol,
207 parsed_symbol,
208 base: base_asset.clone(),
209 quote: quote_asset.clone(),
210 settle: None,
211 base_id: Some(base_asset),
212 quote_id: Some(quote_asset),
213 settle_id: None,
214 market_type: MarketType::Spot,
215 active: active.unwrap_or(true),
216 margin,
217 contract: Some(false),
218 linear: None,
219 inverse: None,
220 taker: Decimal::from_str("0.001").ok(),
222 maker: Decimal::from_str("0.001").ok(),
223 contract_size: None,
224 expiry: None,
225 expiry_datetime: None,
226 strike: None,
227 option_type: None,
228 percentage: Some(true),
229 tier_based: Some(false),
230 fee_side: Some("quote".to_string()),
231 precision: MarketPrecision {
232 price: price_precision,
233 amount: amount_precision,
234 base: None,
235 quote: None,
236 },
237 limits: MarketLimits {
238 amount: Some(MinMax {
239 min: min_amount,
240 max: max_amount,
241 }),
242 price: Some(MinMax {
243 min: min_price,
244 max: max_price,
245 }),
246 cost: Some(MinMax {
247 min: min_cost,
248 max: None,
249 }),
250 leverage: None,
251 },
252 info: value_to_hashmap(data),
253 })
254}
255
256pub fn parse_ticker(data: &Value, market: Option<&Market>) -> Result<Ticker> {
271 let symbol = if let Some(m) = market {
272 m.symbol.clone()
273 } else {
274 data["symbol"]
275 .as_str()
276 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
277 .to_string()
278 };
279
280 let timestamp = data["closeTime"].as_i64();
281
282 Ok(Ticker {
283 symbol,
284 timestamp: timestamp.unwrap_or(0),
285 datetime: timestamp.map(|t| {
286 chrono::DateTime::from_timestamp(t / 1000, 0)
287 .map(|dt| dt.to_rfc3339())
288 .unwrap_or_default()
289 }),
290 high: parse_decimal(data, "highPrice").map(Price::new),
291 low: parse_decimal(data, "lowPrice").map(Price::new),
292 bid: parse_decimal(data, "bidPrice").map(Price::new),
293 bid_volume: parse_decimal(data, "bidQty").map(Amount::new),
294 ask: parse_decimal(data, "askPrice").map(Price::new),
295 ask_volume: parse_decimal(data, "askQty").map(Amount::new),
296 vwap: parse_decimal(data, "weightedAvgPrice").map(Price::new),
297 open: parse_decimal(data, "openPrice").map(Price::new),
298 close: parse_decimal(data, "lastPrice").map(Price::new),
299 last: parse_decimal(data, "lastPrice").map(Price::new),
300 previous_close: parse_decimal(data, "prevClosePrice").map(Price::new),
301 change: parse_decimal(data, "priceChange").map(Price::new),
302 percentage: parse_decimal(data, "priceChangePercent"),
303 average: None,
304 base_volume: parse_decimal(data, "volume").map(Amount::new),
305 quote_volume: parse_decimal(data, "quoteVolume").map(Amount::new),
306 info: value_to_hashmap(data),
307 })
308}
309
310pub fn parse_trade(data: &Value, market: Option<&Market>) -> Result<Trade> {
325 let symbol = if let Some(m) = market {
326 m.symbol.clone()
327 } else {
328 data["symbol"]
329 .as_str()
330 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
331 .to_string()
332 };
333
334 let id = data["id"]
335 .as_u64()
336 .or_else(|| data["a"].as_u64())
337 .map(|i| i.to_string());
338
339 let timestamp = data["time"].as_i64().or_else(|| data["T"].as_i64());
340
341 let side = if data["isBuyerMaker"].as_bool().unwrap_or(false) {
342 OrderSide::Sell
343 } else if data["m"].as_bool().unwrap_or(false) {
344 OrderSide::Sell
345 } else {
346 OrderSide::Buy
347 };
348
349 let price = parse_decimal_multi(data, &["price", "p"]);
350 let amount = parse_decimal_multi(data, &["qty", "q"]);
351
352 let cost = match (price, amount) {
353 (Some(p), Some(a)) => Some(p * a),
354 _ => None,
355 };
356
357 Ok(Trade {
358 id,
359 order: data["orderId"]
360 .as_u64()
361 .or_else(|| data["orderid"].as_u64())
362 .map(|i| i.to_string()),
363 timestamp: timestamp.unwrap_or(0),
364 datetime: timestamp.map(|t| {
365 chrono::DateTime::from_timestamp(t / 1000, 0)
366 .map(|dt| dt.to_rfc3339())
367 .unwrap_or_default()
368 }),
369 symbol,
370 trade_type: None,
371 side,
372 taker_or_maker: if data["isBuyerMaker"].as_bool().unwrap_or(false) {
373 Some(TakerOrMaker::Maker)
374 } else {
375 Some(TakerOrMaker::Taker)
376 },
377 price: Price::new(price.unwrap_or(Decimal::ZERO)),
378 amount: Amount::new(amount.unwrap_or(Decimal::ZERO)),
379 cost: cost.map(Cost::new),
380 fee: None,
381 info: value_to_hashmap(data),
382 })
383}
384
385pub fn parse_order(data: &Value, market: Option<&Market>) -> Result<Order> {
400 let symbol = if let Some(m) = market {
401 m.symbol.clone()
402 } else {
403 data["symbol"]
404 .as_str()
405 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
406 .to_string()
407 };
408
409 let id = data["orderId"]
410 .as_u64()
411 .ok_or_else(|| Error::from(ParseError::missing_field("orderId")))?
412 .to_string();
413
414 let timestamp = data["time"]
415 .as_i64()
416 .or_else(|| data["transactTime"].as_i64());
417
418 let status_str = data["status"]
419 .as_str()
420 .ok_or_else(|| Error::from(ParseError::missing_field("status")))?;
421
422 let status = match status_str {
423 "NEW" | "PARTIALLY_FILLED" => OrderStatus::Open,
424 "FILLED" => OrderStatus::Closed,
425 "CANCELED" => OrderStatus::Cancelled,
426 "EXPIRED" => OrderStatus::Expired,
427 "REJECTED" => OrderStatus::Rejected,
428 _ => OrderStatus::Open,
429 };
430
431 let side = match data["side"].as_str() {
432 Some("BUY") => OrderSide::Buy,
433 Some("SELL") => OrderSide::Sell,
434 _ => return Err(Error::from(ParseError::invalid_format("data", "side"))),
435 };
436
437 let order_type = match data["type"].as_str() {
438 Some("MARKET") => OrderType::Market,
439 Some("LIMIT") => OrderType::Limit,
440 Some("STOP_LOSS") => OrderType::StopLoss,
441 Some("STOP_LOSS_LIMIT") => OrderType::StopLossLimit,
442 Some("TAKE_PROFIT") => OrderType::TakeProfit,
443 Some("TAKE_PROFIT_LIMIT") => OrderType::TakeProfitLimit,
444 Some("STOP_MARKET") | Some("STOP") => OrderType::StopMarket,
445 Some("TAKE_PROFIT_MARKET") => OrderType::TakeProfitLimit,
446 Some("TRAILING_STOP_MARKET") => OrderType::TrailingStop,
447 Some("LIMIT_MAKER") => OrderType::LimitMaker,
448 _ => OrderType::Limit,
449 };
450
451 let time_in_force = match data["timeInForce"].as_str() {
452 Some("GTC") => Some(TimeInForce::GTC),
453 Some("IOC") => Some(TimeInForce::IOC),
454 Some("FOK") => Some(TimeInForce::FOK),
455 Some("GTX") => Some(TimeInForce::PO),
456 _ => None,
457 };
458
459 let price = parse_decimal(data, "price");
460 let amount = parse_decimal(data, "origQty");
461 let filled = parse_decimal(data, "executedQty");
462 let remaining = match (&amount, &filled) {
463 (Some(a), Some(f)) => Some(*a - *f),
464 _ => None,
465 };
466
467 let cost = parse_decimal(data, "cummulativeQuoteQty");
468
469 let average = match (&cost, &filled) {
470 (Some(c), Some(f)) if !f.is_zero() => Some(*c / *f),
471 _ => None,
472 };
473
474 Ok(Order {
475 id,
476 client_order_id: data["clientOrderId"].as_str().map(|s| s.to_string()),
477 timestamp,
478 datetime: timestamp.map(|t| {
479 chrono::DateTime::from_timestamp(t / 1000, 0)
480 .map(|dt| dt.to_rfc3339())
481 .unwrap_or_default()
482 }),
483 last_trade_timestamp: data["updateTime"].as_i64(),
484 status,
485 symbol,
486 order_type,
487 time_in_force: time_in_force.map(|t| t.to_string()),
488 side,
489 price,
490 average,
491 amount: amount.ok_or_else(|| Error::from(ParseError::missing_field("amount")))?,
492 filled,
493 remaining,
494 cost,
495 trades: None,
496 fee: None,
497 post_only: None,
498 reduce_only: data["reduceOnly"].as_bool(),
499 trigger_price: parse_decimal(data, "triggerPrice"),
500 stop_price: parse_decimal(data, "stopPrice"),
501 take_profit_price: parse_decimal(data, "takeProfitPrice"),
502 stop_loss_price: parse_decimal(data, "stopLossPrice"),
503 trailing_delta: parse_decimal(data, "trailingDelta"),
504 trailing_percent: parse_decimal_multi(data, &["trailingPercent", "callbackRate"]),
505 activation_price: parse_decimal_multi(data, &["activationPrice", "activatePrice"]),
506 callback_rate: parse_decimal(data, "callbackRate"),
507 working_type: data["workingType"].as_str().map(|s| s.to_string()),
508 fees: Some(Vec::new()),
509 info: value_to_hashmap(data),
510 })
511}
512pub fn parse_oco_order(data: &Value) -> Result<OcoOrder> {
526 let order_list_id = data["orderListId"]
527 .as_i64()
528 .ok_or_else(|| Error::from(ParseError::missing_field("orderListId")))?;
529
530 let list_client_order_id = data["listClientOrderId"].as_str().map(|s| s.to_string());
531
532 let symbol = data["symbol"]
533 .as_str()
534 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
535 .to_string();
536
537 let list_status = data["listStatusType"]
538 .as_str()
539 .ok_or_else(|| Error::from(ParseError::missing_field("listStatusType")))?
540 .to_string();
541
542 let list_order_status = data["listOrderStatus"]
543 .as_str()
544 .ok_or_else(|| Error::from(ParseError::missing_field("listOrderStatus")))?
545 .to_string();
546
547 let transaction_time = data["transactionTime"]
548 .as_i64()
549 .ok_or_else(|| Error::from(ParseError::missing_field("transactionTime")))?;
550
551 let datetime = chrono::DateTime::from_timestamp((transaction_time / 1000) as i64, 0)
552 .map(|dt| dt.to_rfc3339())
553 .unwrap_or_default();
554
555 let mut orders = Vec::new();
556 if let Some(orders_array) = data["orders"].as_array() {
557 for order in orders_array {
558 let order_info = OcoOrderInfo {
559 symbol: order["symbol"].as_str().unwrap_or(&symbol).to_string(),
560 order_id: order["orderId"]
561 .as_i64()
562 .ok_or_else(|| Error::from(ParseError::missing_field("orderId")))?,
563 client_order_id: order["clientOrderId"].as_str().map(|s| s.to_string()),
564 };
565 orders.push(order_info);
566 }
567 }
568
569 let order_reports = if let Some(reports_array) = data["orderReports"].as_array() {
570 let mut reports = Vec::new();
571 for report in reports_array {
572 let order_report = OrderReport {
573 symbol: report["symbol"].as_str().unwrap_or(&symbol).to_string(),
574 order_id: report["orderId"]
575 .as_i64()
576 .ok_or_else(|| Error::from(ParseError::missing_field("orderId")))?,
577 order_list_id: report["orderListId"].as_i64().unwrap_or(order_list_id),
578 client_order_id: report["clientOrderId"].as_str().map(|s| s.to_string()),
579 transact_time: report["transactTime"].as_i64().unwrap_or(transaction_time),
580 price: report["price"].as_str().unwrap_or("0").to_string(),
581 orig_qty: report["origQty"].as_str().unwrap_or("0").to_string(),
582 executed_qty: report["executedQty"].as_str().unwrap_or("0").to_string(),
583 cummulative_quote_qty: report["cummulativeQuoteQty"]
584 .as_str()
585 .unwrap_or("0")
586 .to_string(),
587 status: report["status"].as_str().unwrap_or("NEW").to_string(),
588 time_in_force: report["timeInForce"].as_str().unwrap_or("GTC").to_string(),
589 type_: report["type"].as_str().unwrap_or("LIMIT").to_string(),
590 side: report["side"].as_str().unwrap_or("SELL").to_string(),
591 stop_price: report["stopPrice"].as_str().map(|s| s.to_string()),
592 };
593 reports.push(order_report);
594 }
595 Some(reports)
596 } else {
597 None
598 };
599
600 Ok(OcoOrder {
601 info: Some(data.clone()),
602 order_list_id,
603 list_client_order_id,
604 symbol,
605 list_status,
606 list_order_status,
607 transaction_time,
608 datetime,
609 orders,
610 order_reports,
611 })
612}
613
614pub fn parse_balance(data: &Value) -> Result<Balance> {
624 let mut balances = HashMap::new();
625
626 if let Some(balances_array) = data["balances"].as_array() {
627 for balance in balances_array {
628 let currency = balance["asset"]
629 .as_str()
630 .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
631 .to_string();
632
633 let free = parse_decimal(balance, "free").unwrap_or(Decimal::ZERO);
634 let locked = parse_decimal(balance, "locked").unwrap_or(Decimal::ZERO);
635 let total = free + locked;
636
637 balances.insert(
638 currency,
639 BalanceEntry {
640 free,
641 used: locked,
642 total,
643 },
644 );
645 }
646 }
647
648 Ok(Balance {
649 balances,
650 info: value_to_hashmap(data),
651 })
652}
653
654pub fn parse_orderbook(data: &Value, symbol: String) -> Result<OrderBook> {
665 let timestamp = data["T"]
667 .as_i64()
668 .or_else(|| data["E"].as_i64())
669 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
670
671 let bids = parse_orderbook_side(&data["bids"])?;
672 let asks = parse_orderbook_side(&data["asks"])?;
673
674 Ok(OrderBook {
675 symbol,
676 timestamp,
677 datetime: Some({
678 chrono::DateTime::from_timestamp_millis(timestamp)
679 .map(|dt| dt.to_rfc3339())
680 .unwrap_or_default()
681 }),
682 nonce: data["lastUpdateId"].as_i64(),
683 bids,
684 asks,
685 buffered_deltas: std::collections::VecDeque::new(),
686 bids_map: std::collections::BTreeMap::new(),
687 asks_map: std::collections::BTreeMap::new(),
688 is_synced: false,
689 needs_resync: false,
690 last_resync_time: 0,
691 info: value_to_hashmap(data),
692 })
693}
694
695fn parse_orderbook_side(data: &Value) -> Result<Vec<OrderBookEntry>> {
697 let array = data
698 .as_array()
699 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "orderbook side")))?;
700
701 let mut result = Vec::new();
702
703 for item in array {
704 if let Some(arr) = item.as_array() {
705 if arr.len() >= 2 {
706 let price = arr[0]
707 .as_str()
708 .and_then(|s| s.parse::<f64>().ok())
709 .and_then(Decimal::from_f64_retain)
710 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "price")))?;
711 let amount = arr[1]
712 .as_str()
713 .and_then(|s| s.parse::<f64>().ok())
714 .and_then(Decimal::from_f64_retain)
715 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "amount")))?;
716 result.push(OrderBookEntry {
717 price: Price::new(price),
718 amount: Amount::new(amount),
719 });
720 }
721 }
722 }
723
724 Ok(result)
725}
726
727pub fn parse_ws_orderbook_delta(data: &Value, symbol: String) -> Result<OrderBookDelta> {
772 let first_update_id = data["U"]
775 .as_i64()
776 .ok_or_else(|| Error::from(ParseError::missing_field("U (first_update_id)")))?;
777
778 let final_update_id = data["u"]
780 .as_i64()
781 .ok_or_else(|| Error::from(ParseError::missing_field("u (final_update_id)")))?;
782
783 let prev_final_update_id = data["pu"].as_i64();
785
786 let timestamp = data["E"]
788 .as_i64()
789 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
790
791 let bids = parse_orderbook_side_ws(&data["b"])?;
793 let asks = parse_orderbook_side_ws(&data["a"])?;
794
795 Ok(OrderBookDelta {
796 symbol,
797 first_update_id,
798 final_update_id,
799 prev_final_update_id,
800 timestamp,
801 bids,
802 asks,
803 })
804}
805
806fn parse_orderbook_side_ws(data: &Value) -> Result<Vec<OrderBookEntry>> {
810 let Some(array) = data.as_array() else {
812 return Ok(Vec::new());
813 };
814
815 let mut result = Vec::with_capacity(array.len());
816
817 for item in array {
818 if let Some(arr) = item.as_array() {
819 if arr.len() >= 2 {
820 let price = arr[0]
821 .as_str()
822 .and_then(|s| s.parse::<f64>().ok())
823 .and_then(Decimal::from_f64_retain)
824 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "price")))?;
825 let amount = arr[1]
826 .as_str()
827 .and_then(|s| s.parse::<f64>().ok())
828 .and_then(Decimal::from_f64_retain)
829 .ok_or_else(|| Error::from(ParseError::invalid_value("data", "amount")))?;
830 result.push(OrderBookEntry {
831 price: Price::new(price),
832 amount: Amount::new(amount),
833 });
834 }
835 }
836 }
837
838 Ok(result)
839}
840
841pub fn parse_funding_rate(data: &Value, market: Option<&Market>) -> Result<FeeFundingRate> {
856 let symbol = if let Some(m) = market {
857 m.symbol.clone()
858 } else {
859 data["symbol"]
860 .as_str()
861 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
862 .to_string()
863 };
864
865 let funding_rate =
866 parse_decimal(data, "lastFundingRate").or_else(|| parse_decimal(data, "fundingRate"));
867
868 let mark_price = parse_decimal(data, "markPrice");
869 let index_price = parse_decimal(data, "indexPrice");
870 let interest_rate = parse_decimal(data, "interestRate");
871
872 let next_funding_time = data["nextFundingTime"].as_i64();
873 let funding_timestamp = next_funding_time.or_else(|| data["fundingTime"].as_i64());
874
875 Ok(FeeFundingRate {
876 info: data.clone(),
877 symbol,
878 mark_price,
879 index_price,
880 interest_rate,
881 estimated_settle_price: None,
882 funding_rate,
883 funding_timestamp,
884 funding_datetime: funding_timestamp.map(|t| {
885 chrono::DateTime::from_timestamp(t / 1000, 0)
886 .map(|dt| dt.to_rfc3339())
887 .unwrap_or_default()
888 }),
889 next_funding_rate: None,
890 next_funding_timestamp: None,
891 next_funding_datetime: None,
892 previous_funding_rate: None,
893 previous_funding_timestamp: None,
894 previous_funding_datetime: None,
895 timestamp: None,
896 datetime: None,
897 interval: None,
898 })
899}
900
901pub fn parse_funding_rate_history(
912 data: &Value,
913 market: Option<&Market>,
914) -> Result<FeeFundingRateHistory> {
915 let symbol = if let Some(m) = market {
916 m.symbol.clone()
917 } else {
918 data["symbol"]
919 .as_str()
920 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
921 .to_string()
922 };
923
924 let funding_rate = parse_decimal(data, "fundingRate");
925 let funding_time = data["fundingTime"].as_i64();
926
927 Ok(FeeFundingRateHistory {
928 info: data.clone(),
929 symbol,
930 funding_rate,
931 funding_timestamp: funding_time,
932 funding_datetime: funding_time.map(|t| {
933 chrono::DateTime::from_timestamp(t / 1000, 0)
934 .map(|dt| dt.to_rfc3339())
935 .unwrap_or_default()
936 }),
937 timestamp: funding_time,
938 datetime: funding_time.map(|t| {
939 chrono::DateTime::from_timestamp(t / 1000, 0)
940 .map(|dt| dt.to_rfc3339())
941 .unwrap_or_default()
942 }),
943 })
944}
945
946pub fn parse_position(data: &Value, market: Option<&Market>) -> Result<Position> {
957 let symbol = if let Some(m) = market {
958 m.symbol.clone()
959 } else {
960 data["symbol"]
961 .as_str()
962 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
963 .to_string()
964 };
965
966 let position_side = data["positionSide"].as_str().unwrap_or("BOTH");
967
968 let side = match position_side {
969 "LONG" => Some("long".to_string()),
970 "SHORT" => Some("short".to_string()),
971 "BOTH" => {
972 let position_amt = parse_f64(data, "positionAmt").unwrap_or(0.0);
974 if position_amt > 0.0 {
975 Some("long".to_string())
976 } else if position_amt < 0.0 {
977 Some("short".to_string())
978 } else {
979 None
980 }
981 }
982 _ => None,
983 };
984
985 let contracts = parse_f64(data, "positionAmt").map(|v| v.abs());
986
987 let contract_size = Some(1.0); let entry_price = parse_f64(data, "entryPrice");
990 let mark_price = parse_f64(data, "markPrice");
991 let notional = parse_f64(data, "notional").map(|v| v.abs());
992
993 let leverage = parse_f64(data, "leverage");
994
995 let collateral =
996 parse_f64(data, "isolatedWallet").or_else(|| parse_f64(data, "positionInitialMargin"));
997
998 let initial_margin =
999 parse_f64(data, "initialMargin").or_else(|| parse_f64(data, "positionInitialMargin"));
1000
1001 let maintenance_margin =
1002 parse_f64(data, "maintMargin").or_else(|| parse_f64(data, "positionMaintMargin"));
1003
1004 let unrealized_pnl =
1005 parse_f64(data, "unrealizedProfit").or_else(|| parse_f64(data, "unRealizedProfit"));
1006
1007 let liquidation_price = parse_f64(data, "liquidationPrice");
1008
1009 let margin_ratio = parse_f64(data, "marginRatio");
1010
1011 let margin_mode = data["marginType"]
1012 .as_str()
1013 .or_else(|| data["marginMode"].as_str())
1014 .map(|s| s.to_lowercase());
1015
1016 let hedged = position_side != "BOTH";
1017
1018 let percentage = match (unrealized_pnl, collateral) {
1019 (Some(pnl), Some(col)) if col > 0.0 => Some((pnl / col) * 100.0),
1020 _ => None,
1021 };
1022
1023 let initial_margin_percentage = parse_f64(data, "initialMarginPercentage");
1024 let maintenance_margin_percentage = parse_f64(data, "maintMarginPercentage");
1025
1026 let update_time = data["updateTime"].as_i64();
1027
1028 Ok(Position {
1029 info: data.clone(),
1030 id: None,
1031 symbol,
1032 side,
1033 contracts,
1034 contract_size,
1035 entry_price,
1036 mark_price,
1037 notional,
1038 leverage,
1039 collateral,
1040 initial_margin,
1041 initial_margin_percentage,
1042 maintenance_margin,
1043 maintenance_margin_percentage,
1044 unrealized_pnl,
1045 realized_pnl: None, liquidation_price,
1047 margin_ratio,
1048 margin_mode,
1049 hedged: Some(hedged),
1050 percentage,
1051 position_side: None,
1052 dual_side_position: None,
1053 timestamp: update_time,
1054 datetime: update_time.map(|t| {
1055 chrono::DateTime::from_timestamp(t / 1000, 0)
1056 .map(|dt| dt.to_rfc3339())
1057 .unwrap_or_default()
1058 }),
1059 })
1060}
1061pub fn parse_leverage(data: &Value, _market: Option<&Market>) -> Result<Leverage> {
1072 let market_id = data.get("symbol").and_then(|v| v.as_str()).unwrap_or("");
1073
1074 let margin_mode = if let Some(isolated) = data.get("isolated").and_then(|v| v.as_bool()) {
1075 Some(if isolated {
1076 MarginType::Isolated
1077 } else {
1078 MarginType::Cross
1079 })
1080 } else if let Some(margin_type) = data.get("marginType").and_then(|v| v.as_str()) {
1081 Some(if margin_type == "crossed" {
1082 MarginType::Cross
1083 } else {
1084 MarginType::Isolated
1085 })
1086 } else {
1087 None
1088 };
1089
1090 let side = data
1091 .get("positionSide")
1092 .and_then(|v| v.as_str())
1093 .map(|s| s.to_lowercase());
1094
1095 let leverage_value = data.get("leverage").and_then(|v| v.as_i64());
1096
1097 let (long_leverage, short_leverage) = match side.as_deref() {
1099 None | Some("both") => (leverage_value, leverage_value),
1100 Some("long") => (leverage_value, None),
1101 Some("short") => (None, leverage_value),
1102 _ => (None, None),
1103 };
1104
1105 Ok(Leverage {
1106 info: data.clone(),
1107 symbol: market_id.to_string(),
1108 margin_mode,
1109 long_leverage,
1110 short_leverage,
1111 timestamp: None,
1112 datetime: None,
1113 })
1114}
1115
1116pub fn parse_funding_history(data: &Value, market: Option<&Market>) -> Result<FundingHistory> {
1127 let symbol = if let Some(m) = market {
1128 m.symbol.clone()
1129 } else {
1130 data["symbol"]
1131 .as_str()
1132 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
1133 .to_string()
1134 };
1135
1136 let id = data["tranId"]
1137 .as_u64()
1138 .or_else(|| data["id"].as_u64())
1139 .map(|i| i.to_string());
1140
1141 let amount = parse_f64(data, "income");
1142 let code = data["asset"].as_str().map(|s| s.to_string());
1143 let timestamp = data["time"].as_i64();
1144
1145 Ok(FundingHistory {
1146 info: data.clone(),
1147 id,
1148 symbol,
1149 code,
1150 amount,
1151 timestamp: timestamp,
1152 datetime: timestamp.map(|t| {
1153 chrono::DateTime::from_timestamp((t / 1000) as i64, 0)
1154 .map(|dt| dt.to_rfc3339())
1155 .unwrap_or_default()
1156 }),
1157 })
1158}
1159
1160pub fn parse_funding_fee(data: &Value, market: Option<&Market>) -> Result<FundingFee> {
1171 let symbol = if let Some(m) = market {
1172 m.symbol.clone()
1173 } else {
1174 data["symbol"]
1175 .as_str()
1176 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
1177 .to_string()
1178 };
1179
1180 let income = parse_f64(data, "income").unwrap_or(0.0);
1181 let asset = data["asset"]
1182 .as_str()
1183 .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
1184 .to_string();
1185
1186 let time = data["time"]
1187 .as_i64()
1188 .ok_or_else(|| Error::from(ParseError::missing_field("time")))?;
1189
1190 let funding_rate = parse_f64(data, "fundingRate");
1191 let mark_price = parse_f64(data, "markPrice");
1192
1193 let datetime = Some(
1194 chrono::DateTime::from_timestamp((time / 1000) as i64, 0)
1195 .map(|dt| dt.to_rfc3339())
1196 .unwrap_or_default(),
1197 );
1198
1199 Ok(FundingFee {
1200 info: data.clone(),
1201 symbol,
1202 income,
1203 asset,
1204 time,
1205 datetime,
1206 funding_rate,
1207 mark_price,
1208 })
1209}
1210
1211pub fn parse_next_funding_rate(data: &Value, market: &Market) -> Result<NextFundingRate> {
1222 let symbol = market.symbol.clone();
1223
1224 let mark_price = parse_f64(data, "markPrice")
1225 .ok_or_else(|| Error::from(ParseError::missing_field("markPrice")))?;
1226
1227 let index_price = parse_f64(data, "indexPrice");
1228
1229 let current_funding_rate = parse_f64(data, "lastFundingRate").unwrap_or(0.0);
1230
1231 let next_funding_rate = parse_f64(data, "interestRate")
1232 .or_else(|| parse_f64(data, "estimatedSettlePrice"))
1233 .unwrap_or(current_funding_rate);
1234
1235 let next_funding_time = data["nextFundingTime"]
1236 .as_i64()
1237 .ok_or_else(|| Error::from(ParseError::missing_field("nextFundingTime")))?;
1238
1239 let next_funding_datetime = Some(
1240 chrono::DateTime::from_timestamp((next_funding_time / 1000) as i64, 0)
1241 .map(|dt| dt.to_rfc3339())
1242 .unwrap_or_default(),
1243 );
1244
1245 Ok(NextFundingRate {
1246 info: data.clone(),
1247 symbol,
1248 mark_price,
1249 index_price,
1250 current_funding_rate,
1251 next_funding_rate,
1252 next_funding_time,
1253 next_funding_datetime,
1254 })
1255}
1256pub fn parse_account_config(data: &Value) -> Result<AccountConfig> {
1270 let multi_assets_margin = data["multiAssetsMargin"].as_bool().unwrap_or(false);
1271
1272 let fee_tier = data["feeTier"].as_i64().unwrap_or(0) as i32;
1273
1274 let can_trade = data["canTrade"].as_bool().unwrap_or(true);
1275
1276 let can_deposit = data["canDeposit"].as_bool().unwrap_or(true);
1277
1278 let can_withdraw = data["canWithdraw"].as_bool().unwrap_or(true);
1279
1280 let update_time = data["updateTime"].as_i64().unwrap_or(0);
1281
1282 Ok(AccountConfig {
1283 info: Some(data.clone()),
1284 multi_assets_margin,
1285 fee_tier,
1286 can_trade,
1287 can_deposit,
1288 can_withdraw,
1289 update_time,
1290 })
1291}
1292
1293pub fn parse_commission_rate(data: &Value, market: &Market) -> Result<CommissionRate> {
1304 let maker_commission_rate = data["makerCommissionRate"]
1305 .as_str()
1306 .and_then(|s| s.parse::<f64>().ok())
1307 .unwrap_or(0.0);
1308
1309 let taker_commission_rate = data["takerCommissionRate"]
1310 .as_str()
1311 .and_then(|s| s.parse::<f64>().ok())
1312 .unwrap_or(0.0);
1313
1314 Ok(CommissionRate {
1315 info: Some(data.clone()),
1316 symbol: market.symbol.clone(),
1317 maker_commission_rate,
1318 taker_commission_rate,
1319 })
1320}
1321
1322pub fn parse_open_interest(data: &Value, market: &Market) -> Result<OpenInterest> {
1347 let open_interest = data["openInterest"]
1348 .as_str()
1349 .and_then(|s| s.parse::<f64>().ok())
1350 .or_else(|| data["openInterest"].as_f64())
1351 .unwrap_or(0.0);
1352
1353 let timestamp = data["time"]
1354 .as_i64()
1355 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1356
1357 let contract_size = market
1358 .contract_size
1359 .unwrap_or_else(|| rust_decimal::Decimal::from(1))
1360 .to_f64()
1361 .unwrap_or(1.0);
1362 let open_interest_value = open_interest * contract_size;
1363
1364 Ok(OpenInterest {
1365 info: Some(data.clone()),
1366 symbol: market.symbol.clone(),
1367 open_interest,
1368 open_interest_value,
1369 timestamp,
1370 })
1371}
1372
1373pub fn parse_open_interest_history(
1397 data: &Value,
1398 market: &Market,
1399) -> Result<Vec<OpenInterestHistory>> {
1400 let array = data.as_array().ok_or_else(|| {
1401 Error::from(ParseError::invalid_value(
1402 "data",
1403 "Expected array for open interest history",
1404 ))
1405 })?;
1406
1407 let mut result = Vec::new();
1408
1409 for item in array {
1410 let sum_open_interest = item["sumOpenInterest"]
1411 .as_str()
1412 .and_then(|s| s.parse::<f64>().ok())
1413 .or_else(|| item["sumOpenInterest"].as_f64())
1414 .unwrap_or(0.0);
1415
1416 let sum_open_interest_value = item["sumOpenInterestValue"]
1417 .as_str()
1418 .and_then(|s| s.parse::<f64>().ok())
1419 .or_else(|| item["sumOpenInterestValue"].as_f64())
1420 .unwrap_or(0.0);
1421
1422 let timestamp = item["timestamp"]
1423 .as_i64()
1424 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1425
1426 result.push(OpenInterestHistory {
1427 info: Some(item.clone()),
1428 symbol: market.symbol.clone(),
1429 sum_open_interest,
1430 sum_open_interest_value,
1431 timestamp,
1432 });
1433 }
1434
1435 Ok(result)
1436}
1437
1438pub fn parse_max_leverage(data: &Value, market: &Market) -> Result<MaxLeverage> {
1468 let target_data = if let Some(array) = data.as_array() {
1469 array
1470 .iter()
1471 .find(|item| item["symbol"].as_str().unwrap_or("") == market.id)
1472 .ok_or_else(|| {
1473 Error::from(ParseError::invalid_value(
1474 "symbol",
1475 format!("Symbol {} not found in leverage brackets", market.id),
1476 ))
1477 })?
1478 } else {
1479 data
1480 };
1481
1482 let brackets = target_data["brackets"]
1483 .as_array()
1484 .ok_or_else(|| Error::from(ParseError::missing_field("brackets")))?;
1485
1486 if brackets.is_empty() {
1487 return Err(Error::from(ParseError::invalid_value(
1488 "data",
1489 "Empty brackets array",
1490 )));
1491 }
1492
1493 let first_bracket = &brackets[0];
1494 let max_leverage = first_bracket["initialLeverage"].as_i64().unwrap_or(1) as i32;
1495
1496 let notional = first_bracket["notionalCap"]
1497 .as_f64()
1498 .or_else(|| {
1499 first_bracket["notionalCap"]
1500 .as_str()
1501 .and_then(|s| s.parse::<f64>().ok())
1502 })
1503 .unwrap_or(0.0);
1504
1505 Ok(MaxLeverage {
1506 info: Some(data.clone()),
1507 symbol: market.symbol.clone(),
1508 max_leverage,
1509 notional,
1510 })
1511}
1512
1513pub fn parse_index_price(data: &Value, market: &Market) -> Result<IndexPrice> {
1528 let index_price = data["indexPrice"]
1529 .as_f64()
1530 .or_else(|| {
1531 data["indexPrice"]
1532 .as_str()
1533 .and_then(|s| s.parse::<f64>().ok())
1534 })
1535 .ok_or_else(|| Error::from(ParseError::missing_field("indexPrice")))?;
1536
1537 let timestamp = data["time"]
1538 .as_i64()
1539 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1540
1541 Ok(IndexPrice {
1542 info: Some(data.clone()),
1543 symbol: market.symbol.clone(),
1544 index_price,
1545 timestamp,
1546 })
1547}
1548
1549pub fn parse_premium_index(data: &Value, market: &Market) -> Result<PremiumIndex> {
1560 let mark_price = data["markPrice"]
1561 .as_f64()
1562 .or_else(|| {
1563 data["markPrice"]
1564 .as_str()
1565 .and_then(|s| s.parse::<f64>().ok())
1566 })
1567 .ok_or_else(|| Error::from(ParseError::missing_field("markPrice")))?;
1568
1569 let index_price = data["indexPrice"]
1570 .as_f64()
1571 .or_else(|| {
1572 data["indexPrice"]
1573 .as_str()
1574 .and_then(|s| s.parse::<f64>().ok())
1575 })
1576 .ok_or_else(|| Error::from(ParseError::missing_field("indexPrice")))?;
1577
1578 let estimated_settle_price = data["estimatedSettlePrice"]
1579 .as_f64()
1580 .or_else(|| {
1581 data["estimatedSettlePrice"]
1582 .as_str()
1583 .and_then(|s| s.parse::<f64>().ok())
1584 })
1585 .unwrap_or(0.0);
1586
1587 let last_funding_rate = data["lastFundingRate"]
1588 .as_f64()
1589 .or_else(|| {
1590 data["lastFundingRate"]
1591 .as_str()
1592 .and_then(|s| s.parse::<f64>().ok())
1593 })
1594 .unwrap_or(0.0);
1595
1596 let next_funding_time = data["nextFundingTime"].as_i64().unwrap_or(0);
1598
1599 let time = data["time"]
1601 .as_i64()
1602 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1603
1604 Ok(PremiumIndex {
1605 info: Some(data.clone()),
1606 symbol: market.symbol.clone(),
1607 mark_price,
1608 index_price,
1609 estimated_settle_price,
1610 last_funding_rate,
1611 next_funding_time,
1612 time,
1613 })
1614}
1615
1616pub fn parse_liquidation(data: &Value, market: &Market) -> Result<Liquidation> {
1627 let side = data["side"]
1628 .as_str()
1629 .ok_or_else(|| Error::from(ParseError::missing_field("side")))?
1630 .to_string();
1631
1632 let order_type = data["type"].as_str().unwrap_or("LIMIT").to_string();
1633
1634 let time = data["time"]
1635 .as_i64()
1636 .ok_or_else(|| Error::from(ParseError::missing_field("time")))?;
1637
1638 let price = data["price"]
1639 .as_f64()
1640 .or_else(|| data["price"].as_str().and_then(|s| s.parse::<f64>().ok()))
1641 .ok_or_else(|| Error::from(ParseError::missing_field("price")))?;
1642
1643 let quantity = data["origQty"]
1644 .as_f64()
1645 .or_else(|| data["origQty"].as_str().and_then(|s| s.parse::<f64>().ok()))
1646 .ok_or_else(|| Error::from(ParseError::missing_field("origQty")))?;
1647
1648 let average_price = data["averagePrice"]
1649 .as_f64()
1650 .or_else(|| {
1651 data["averagePrice"]
1652 .as_str()
1653 .and_then(|s| s.parse::<f64>().ok())
1654 })
1655 .unwrap_or(price);
1656
1657 Ok(Liquidation {
1658 info: Some(data.clone()),
1659 symbol: market.symbol.clone(),
1660 side,
1661 order_type,
1662 time,
1663 price,
1664 quantity,
1665 average_price,
1666 })
1667}
1668
1669pub fn parse_borrow_rate(data: &Value, currency: &str, symbol: Option<&str>) -> Result<BorrowRate> {
1685 let rate = if let Some(rate_str) = data["dailyInterestRate"].as_str() {
1686 rate_str.parse::<f64>().unwrap_or(0.0)
1687 } else {
1688 data["dailyInterestRate"].as_f64().unwrap_or(0.0)
1689 };
1690
1691 let timestamp = data["timestamp"]
1692 .as_i64()
1693 .or_else(|| {
1694 data["vipLevel"]
1695 .as_i64()
1696 .map(|_| chrono::Utc::now().timestamp_millis())
1697 })
1698 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1699
1700 if let Some(sym) = symbol {
1701 Ok(BorrowRate::new_isolated(
1702 currency.to_string(),
1703 sym.to_string(),
1704 rate,
1705 timestamp,
1706 data.clone(),
1707 ))
1708 } else {
1709 Ok(BorrowRate::new_cross(
1710 currency.to_string(),
1711 rate,
1712 timestamp,
1713 data.clone(),
1714 ))
1715 }
1716}
1717
1718pub fn parse_margin_loan(data: &Value) -> Result<MarginLoan> {
1728 let id = data["tranId"]
1729 .as_i64()
1730 .or_else(|| data["txId"].as_i64())
1731 .map(|id| id.to_string())
1732 .unwrap_or_default();
1733
1734 let currency = data["asset"]
1735 .as_str()
1736 .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
1737 .to_string();
1738
1739 let symbol = data["symbol"].as_str().map(|s| s.to_string());
1740
1741 let amount = if let Some(amount_str) = data["amount"].as_str() {
1742 amount_str.parse::<f64>().unwrap_or(0.0)
1743 } else {
1744 data["amount"].as_f64().unwrap_or(0.0)
1745 };
1746
1747 let timestamp = data["timestamp"]
1748 .as_i64()
1749 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1750
1751 let status = data["status"].as_str().unwrap_or("CONFIRMED").to_string();
1752
1753 Ok(MarginLoan::new(
1754 id,
1755 currency,
1756 symbol,
1757 amount,
1758 timestamp,
1759 status,
1760 data.clone(),
1761 ))
1762}
1763
1764pub fn parse_borrow_interest(data: &Value) -> Result<BorrowInterest> {
1774 let id = data["txId"]
1775 .as_i64()
1776 .or_else(|| {
1777 data["isolatedSymbol"]
1778 .as_str()
1779 .map(|_| chrono::Utc::now().timestamp_millis())
1780 })
1781 .map(|id| id.to_string())
1782 .unwrap_or_default();
1783
1784 let currency = data["asset"]
1785 .as_str()
1786 .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
1787 .to_string();
1788
1789 let symbol = data["isolatedSymbol"].as_str().map(|s| s.to_string());
1790
1791 let interest = if let Some(interest_str) = data["interest"].as_str() {
1792 interest_str.parse::<f64>().unwrap_or(0.0)
1793 } else {
1794 data["interest"].as_f64().unwrap_or(0.0)
1795 };
1796
1797 let interest_rate = if let Some(rate_str) = data["interestRate"].as_str() {
1798 rate_str.parse::<f64>().unwrap_or(0.0)
1799 } else {
1800 data["interestRate"].as_f64().unwrap_or(0.0)
1801 };
1802
1803 let principal = if let Some(principal_str) = data["principal"].as_str() {
1804 principal_str.parse::<f64>().unwrap_or(0.0)
1805 } else {
1806 data["principal"].as_f64().unwrap_or(0.0)
1807 };
1808
1809 let timestamp = data["interestAccuredTime"]
1810 .as_i64()
1811 .or_else(|| data["timestamp"].as_i64())
1812 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1813
1814 Ok(BorrowInterest::new(
1815 id,
1816 currency,
1817 symbol,
1818 interest,
1819 interest_rate,
1820 principal,
1821 timestamp,
1822 data.clone(),
1823 ))
1824}
1825
1826pub fn parse_margin_adjustment(data: &Value) -> Result<MarginAdjustment> {
1836 let id = data["tranId"]
1837 .as_i64()
1838 .or_else(|| data["txId"].as_i64())
1839 .map(|id| id.to_string())
1840 .unwrap_or_default();
1841
1842 let symbol = data["symbol"].as_str().map(|s| s.to_string());
1843
1844 let currency = data["asset"]
1845 .as_str()
1846 .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
1847 .to_string();
1848
1849 let amount = if let Some(amount_str) = data["amount"].as_str() {
1850 amount_str.parse::<f64>().unwrap_or(0.0)
1851 } else {
1852 data["amount"].as_f64().unwrap_or(0.0)
1853 };
1854
1855 let transfer_type = data["type"]
1856 .as_str()
1857 .or_else(|| data["transFrom"].as_str())
1858 .map(|t| {
1859 if t.contains("MAIN") || t.eq("1") || t.eq("ROLL_IN") {
1860 "IN"
1861 } else {
1862 "OUT"
1863 }
1864 })
1865 .unwrap_or("IN")
1866 .to_string();
1867
1868 let timestamp = data["timestamp"]
1869 .as_i64()
1870 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1871
1872 let status = data["status"].as_str().unwrap_or("SUCCESS").to_string();
1873
1874 Ok(MarginAdjustment::new(
1875 id,
1876 symbol,
1877 currency,
1878 amount,
1879 transfer_type,
1880 timestamp,
1881 status,
1882 data.clone(),
1883 ))
1884}
1885pub fn parse_futures_transfer_type(transfer_type: i32) -> Result<(&'static str, &'static str)> {
1914 match transfer_type {
1915 1 => Ok(("spot", "future")),
1916 2 => Ok(("future", "spot")),
1917 3 => Ok(("spot", "delivery")),
1918 4 => Ok(("delivery", "spot")),
1919 _ => Err(Error::invalid_request(format!(
1920 "Invalid futures transfer type: {}. Must be between 1 and 4",
1921 transfer_type
1922 ))),
1923 }
1924}
1925
1926pub fn parse_transfer(data: &Value) -> Result<Transfer> {
1955 let id = data["tranId"]
1956 .as_i64()
1957 .or_else(|| data["txId"].as_i64())
1958 .or_else(|| data["transactionId"].as_i64())
1959 .map(|id| id.to_string());
1960
1961 let timestamp = data["timestamp"]
1962 .as_i64()
1963 .or_else(|| data["transactionTime"].as_i64())
1964 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1965
1966 let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
1967 .map(|dt| dt.to_rfc3339())
1968 .unwrap_or_default();
1969
1970 let currency = data["asset"]
1971 .as_str()
1972 .or_else(|| data["currency"].as_str())
1973 .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
1974 .to_string();
1975
1976 let amount = if let Some(amount_str) = data["amount"].as_str() {
1977 amount_str.parse::<f64>().unwrap_or(0.0)
1978 } else {
1979 data["amount"].as_f64().unwrap_or(0.0)
1980 };
1981
1982 let mut from_account = data["fromAccountType"].as_str().map(|s| s.to_string());
1983
1984 let mut to_account = data["toAccountType"].as_str().map(|s| s.to_string());
1985
1986 if from_account.is_none() || to_account.is_none() {
1988 if let Some(type_str) = data["type"].as_str() {
1989 let parts: Vec<&str> = type_str.split('_').collect();
1990 if parts.len() == 2 {
1991 from_account = Some(parts[0].to_lowercase());
1992 to_account = Some(parts[1].to_lowercase());
1993 }
1994 }
1995 }
1996
1997 let status = data["status"].as_str().unwrap_or("SUCCESS").to_lowercase();
1998
1999 Ok(Transfer {
2000 id,
2001 timestamp: timestamp,
2002 datetime,
2003 currency,
2004 amount,
2005 from_account,
2006 to_account,
2007 status,
2008 info: Some(data.clone()),
2009 })
2010}
2011
2012pub fn parse_max_borrowable(
2024 data: &Value,
2025 currency: &str,
2026 symbol: Option<String>,
2027) -> Result<MaxBorrowable> {
2028 let amount = if let Some(amount_str) = data["amount"].as_str() {
2029 amount_str.parse::<f64>().unwrap_or(0.0)
2030 } else {
2031 data["amount"].as_f64().unwrap_or(0.0)
2032 };
2033
2034 let borrow_limit = if let Some(limit_str) = data["borrowLimit"].as_str() {
2035 Some(limit_str.parse::<f64>().unwrap_or(0.0))
2036 } else {
2037 data["borrowLimit"].as_f64()
2038 };
2039
2040 let timestamp = chrono::Utc::now().timestamp_millis();
2041 let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
2042 .map(|dt| dt.to_rfc3339())
2043 .unwrap_or_default();
2044
2045 Ok(MaxBorrowable {
2046 currency: currency.to_string(),
2047 amount,
2048 borrow_limit,
2049 symbol,
2050 timestamp,
2051 datetime,
2052 info: data.clone(),
2053 })
2054}
2055
2056pub fn parse_max_transferable(
2068 data: &Value,
2069 currency: &str,
2070 symbol: Option<String>,
2071) -> Result<MaxTransferable> {
2072 let amount = if let Some(amount_str) = data["amount"].as_str() {
2073 amount_str.parse::<f64>().unwrap_or(0.0)
2074 } else {
2075 data["amount"].as_f64().unwrap_or(0.0)
2076 };
2077
2078 let timestamp = chrono::Utc::now().timestamp_millis();
2079 let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
2080 .map(|dt| dt.to_rfc3339())
2081 .unwrap_or_default();
2082
2083 Ok(MaxTransferable {
2084 currency: currency.to_string(),
2085 amount,
2086 symbol,
2087 timestamp,
2088 datetime,
2089 info: data.clone(),
2090 })
2091}
2092
2093pub fn parse_balance_with_type(data: &Value, account_type: &str) -> Result<Balance> {
2106 let mut balances = HashMap::new();
2107 let _timestamp = chrono::Utc::now().timestamp_millis();
2108
2109 match account_type {
2110 "spot" => {
2111 if let Some(balances_array) = data["balances"].as_array() {
2112 for item in balances_array {
2113 if let Some(asset) = item["asset"].as_str() {
2114 let free = if let Some(free_str) = item["free"].as_str() {
2115 free_str.parse::<f64>().unwrap_or(0.0)
2116 } else {
2117 item["free"].as_f64().unwrap_or(0.0)
2118 };
2119
2120 let locked = if let Some(locked_str) = item["locked"].as_str() {
2121 locked_str.parse::<f64>().unwrap_or(0.0)
2122 } else {
2123 item["locked"].as_f64().unwrap_or(0.0)
2124 };
2125
2126 balances.insert(
2127 asset.to_string(),
2128 BalanceEntry {
2129 free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2130 used: Decimal::from_f64(locked).unwrap_or(Decimal::ZERO),
2131 total: Decimal::from_f64(free + locked).unwrap_or(Decimal::ZERO),
2132 },
2133 );
2134 }
2135 }
2136 }
2137 }
2138 "margin" | "cross" => {
2139 if let Some(user_assets) = data["userAssets"].as_array() {
2140 for item in user_assets {
2141 if let Some(asset) = item["asset"].as_str() {
2142 let free = if let Some(free_str) = item["free"].as_str() {
2143 free_str.parse::<f64>().unwrap_or(0.0)
2144 } else {
2145 item["free"].as_f64().unwrap_or(0.0)
2146 };
2147
2148 let locked = if let Some(locked_str) = item["locked"].as_str() {
2149 locked_str.parse::<f64>().unwrap_or(0.0)
2150 } else {
2151 item["locked"].as_f64().unwrap_or(0.0)
2152 };
2153
2154 balances.insert(
2155 asset.to_string(),
2156 BalanceEntry {
2157 free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2158 used: Decimal::from_f64(locked).unwrap_or(Decimal::ZERO),
2159 total: Decimal::from_f64(free + locked).unwrap_or(Decimal::ZERO),
2160 },
2161 );
2162 }
2163 }
2164 }
2165 }
2166 "isolated" => {
2167 if let Some(assets) = data["assets"].as_array() {
2168 for item in assets {
2169 if let Some(base_asset) = item["baseAsset"].as_object() {
2170 if let Some(asset) = base_asset["asset"].as_str() {
2171 let free = if let Some(free_str) = base_asset["free"].as_str() {
2172 free_str.parse::<f64>().unwrap_or(0.0)
2173 } else {
2174 base_asset["free"].as_f64().unwrap_or(0.0)
2175 };
2176
2177 let locked = if let Some(locked_str) = base_asset["locked"].as_str() {
2178 locked_str.parse::<f64>().unwrap_or(0.0)
2179 } else {
2180 base_asset["locked"].as_f64().unwrap_or(0.0)
2181 };
2182
2183 balances.insert(
2184 asset.to_string(),
2185 BalanceEntry {
2186 free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2187 used: Decimal::from_f64(locked).unwrap_or(Decimal::ZERO),
2188 total: Decimal::from_f64(free + locked)
2189 .unwrap_or(Decimal::ZERO),
2190 },
2191 );
2192 }
2193 }
2194
2195 if let Some(quote_asset) = item["quoteAsset"].as_object() {
2196 if let Some(asset) = quote_asset["asset"].as_str() {
2197 let free = if let Some(free_str) = quote_asset["free"].as_str() {
2198 free_str.parse::<f64>().unwrap_or(0.0)
2199 } else {
2200 quote_asset["free"].as_f64().unwrap_or(0.0)
2201 };
2202
2203 let locked = if let Some(locked_str) = quote_asset["locked"].as_str() {
2204 locked_str.parse::<f64>().unwrap_or(0.0)
2205 } else {
2206 quote_asset["locked"].as_f64().unwrap_or(0.0)
2207 };
2208
2209 balances.insert(
2210 asset.to_string(),
2211 BalanceEntry {
2212 free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2213 used: Decimal::from_f64(locked).unwrap_or(Decimal::ZERO),
2214 total: Decimal::from_f64(free + locked)
2215 .unwrap_or(Decimal::ZERO),
2216 },
2217 );
2218 }
2219 }
2220 }
2221 }
2222 }
2223 "linear" | "future" => {
2224 let assets = if let Some(arr) = data.as_array() {
2226 arr.clone()
2227 } else if let Some(arr) = data["assets"].as_array() {
2228 arr.clone()
2229 } else {
2230 vec![]
2231 };
2232
2233 for item in &assets {
2234 if let Some(asset) = item["asset"].as_str() {
2235 let available_balance =
2236 if let Some(balance_str) = item["availableBalance"].as_str() {
2237 balance_str.parse::<f64>().unwrap_or(0.0)
2238 } else {
2239 item["availableBalance"].as_f64().unwrap_or(0.0)
2240 };
2241
2242 let wallet_balance = if let Some(balance_str) = item["walletBalance"].as_str() {
2243 balance_str.parse::<f64>().unwrap_or(0.0)
2244 } else {
2245 item["walletBalance"].as_f64().unwrap_or(0.0)
2246 };
2247
2248 let wallet_balance = if wallet_balance == 0.0 {
2250 if let Some(balance_str) = item["balance"].as_str() {
2251 balance_str.parse::<f64>().unwrap_or(0.0)
2252 } else {
2253 item["balance"].as_f64().unwrap_or(wallet_balance)
2254 }
2255 } else {
2256 wallet_balance
2257 };
2258
2259 let used = wallet_balance - available_balance;
2260
2261 balances.insert(
2262 asset.to_string(),
2263 BalanceEntry {
2264 free: Decimal::from_f64(available_balance).unwrap_or(Decimal::ZERO),
2265 used: Decimal::from_f64(used).unwrap_or(Decimal::ZERO),
2266 total: Decimal::from_f64(wallet_balance).unwrap_or(Decimal::ZERO),
2267 },
2268 );
2269 }
2270 }
2271 }
2272 "inverse" | "delivery" => {
2273 let assets = if let Some(arr) = data.as_array() {
2275 arr.clone()
2276 } else if let Some(arr) = data["assets"].as_array() {
2277 arr.clone()
2278 } else {
2279 vec![]
2280 };
2281
2282 for item in &assets {
2283 if let Some(asset) = item["asset"].as_str() {
2284 let available_balance =
2285 if let Some(balance_str) = item["availableBalance"].as_str() {
2286 balance_str.parse::<f64>().unwrap_or(0.0)
2287 } else {
2288 item["availableBalance"].as_f64().unwrap_or(0.0)
2289 };
2290
2291 let wallet_balance = if let Some(balance_str) = item["walletBalance"].as_str() {
2292 balance_str.parse::<f64>().unwrap_or(0.0)
2293 } else {
2294 item["walletBalance"].as_f64().unwrap_or(0.0)
2295 };
2296
2297 let wallet_balance = if wallet_balance == 0.0 {
2299 if let Some(balance_str) = item["balance"].as_str() {
2300 balance_str.parse::<f64>().unwrap_or(0.0)
2301 } else {
2302 item["balance"].as_f64().unwrap_or(wallet_balance)
2303 }
2304 } else {
2305 wallet_balance
2306 };
2307
2308 let used = wallet_balance - available_balance;
2309
2310 balances.insert(
2311 asset.to_string(),
2312 BalanceEntry {
2313 free: Decimal::from_f64(available_balance).unwrap_or(Decimal::ZERO),
2314 used: Decimal::from_f64(used).unwrap_or(Decimal::ZERO),
2315 total: Decimal::from_f64(wallet_balance).unwrap_or(Decimal::ZERO),
2316 },
2317 );
2318 }
2319 }
2320 }
2321 "funding" => {
2322 if let Some(assets) = data.as_array() {
2323 for item in assets {
2324 if let Some(asset) = item["asset"].as_str() {
2325 let free = if let Some(free_str) = item["free"].as_str() {
2326 free_str.parse::<f64>().unwrap_or(0.0)
2327 } else {
2328 item["free"].as_f64().unwrap_or(0.0)
2329 };
2330
2331 let locked = if let Some(locked_str) = item["locked"].as_str() {
2332 locked_str.parse::<f64>().unwrap_or(0.0)
2333 } else {
2334 item["locked"].as_f64().unwrap_or(0.0)
2335 };
2336
2337 let total = free + locked;
2338
2339 balances.insert(
2340 asset.to_string(),
2341 BalanceEntry {
2342 free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2343 used: Decimal::from_f64(locked).unwrap_or(Decimal::ZERO),
2344 total: Decimal::from_f64(total).unwrap_or(Decimal::ZERO),
2345 },
2346 );
2347 }
2348 }
2349 }
2350 }
2351 "option" => {
2352 if let Some(asset) = data["asset"].as_str() {
2354 let equity = if let Some(equity_str) = data["equity"].as_str() {
2355 equity_str.parse::<f64>().unwrap_or(0.0)
2356 } else {
2357 data["equity"].as_f64().unwrap_or(0.0)
2358 };
2359
2360 let available = if let Some(available_str) = data["available"].as_str() {
2361 available_str.parse::<f64>().unwrap_or(0.0)
2362 } else {
2363 data["available"].as_f64().unwrap_or(0.0)
2364 };
2365
2366 let used = equity - available;
2367
2368 balances.insert(
2369 asset.to_string(),
2370 BalanceEntry {
2371 free: Decimal::from_f64(available).unwrap_or(Decimal::ZERO),
2372 used: Decimal::from_f64(used).unwrap_or(Decimal::ZERO),
2373 total: Decimal::from_f64(equity).unwrap_or(Decimal::ZERO),
2374 },
2375 );
2376 }
2377 }
2378 "portfolio" => {
2379 let assets = if let Some(arr) = data.as_array() {
2381 arr.clone()
2382 } else if data.is_object() {
2383 vec![data.clone()]
2384 } else {
2385 vec![]
2386 };
2387
2388 for item in &assets {
2389 if let Some(asset) = item["asset"].as_str() {
2390 let total_wallet_balance =
2391 if let Some(balance_str) = item["totalWalletBalance"].as_str() {
2392 balance_str.parse::<f64>().unwrap_or(0.0)
2393 } else {
2394 item["totalWalletBalance"].as_f64().unwrap_or(0.0)
2395 };
2396
2397 let available_balance =
2398 if let Some(balance_str) = item["availableBalance"].as_str() {
2399 balance_str.parse::<f64>().unwrap_or(0.0)
2400 } else {
2401 item["availableBalance"].as_f64().unwrap_or(0.0)
2402 };
2403
2404 let free = if available_balance > 0.0 {
2406 available_balance
2407 } else if let Some(cross_str) = item["crossWalletBalance"].as_str() {
2408 cross_str.parse::<f64>().unwrap_or(0.0)
2409 } else {
2410 item["crossWalletBalance"].as_f64().unwrap_or(0.0)
2411 };
2412
2413 let used = total_wallet_balance - free;
2414
2415 balances.insert(
2416 asset.to_string(),
2417 BalanceEntry {
2418 free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2419 used: Decimal::from_f64(used).unwrap_or(Decimal::ZERO),
2420 total: Decimal::from_f64(total_wallet_balance).unwrap_or(Decimal::ZERO),
2421 },
2422 );
2423 }
2424 }
2425 }
2426 _ => {
2427 return Err(Error::from(ParseError::invalid_value(
2428 "account_type",
2429 format!("Unsupported account type: {}", account_type),
2430 )));
2431 }
2432 }
2433
2434 let mut info_map = HashMap::new();
2435 if let Some(obj) = data.as_object() {
2436 for (k, v) in obj {
2437 info_map.insert(k.clone(), v.clone());
2438 }
2439 }
2440
2441 Ok(Balance {
2442 balances,
2443 info: info_map,
2444 })
2445}
2446
2447pub fn parse_bid_ask(data: &Value) -> Result<ccxt_core::types::BidAsk> {
2461 use ccxt_core::types::BidAsk;
2462
2463 let symbol = data["symbol"]
2464 .as_str()
2465 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
2466 .to_string();
2467
2468 let formatted_symbol = if symbol.len() >= 6 {
2469 let quote_currencies = ["USDT", "BUSD", "USDC", "BTC", "ETH", "BNB"];
2470 let mut found = false;
2471 let mut formatted = symbol.clone();
2472
2473 for quote in "e_currencies {
2474 if symbol.ends_with(quote) {
2475 let base = &symbol[..symbol.len() - quote.len()];
2476 formatted = format!("{}/{}", base, quote);
2477 found = true;
2478 break;
2479 }
2480 }
2481
2482 if !found { symbol.clone() } else { formatted }
2483 } else {
2484 symbol.clone()
2485 };
2486
2487 let bid_price = parse_decimal(data, "bidPrice").unwrap_or(Decimal::ZERO);
2488 let bid_quantity = parse_decimal(data, "bidQty").unwrap_or(Decimal::ZERO);
2489 let ask_price = parse_decimal(data, "askPrice").unwrap_or(Decimal::ZERO);
2490 let ask_quantity = parse_decimal(data, "askQty").unwrap_or(Decimal::ZERO);
2491
2492 let timestamp = data["time"]
2493 .as_i64()
2494 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
2495
2496 Ok(BidAsk {
2497 symbol: formatted_symbol,
2498 bid_price,
2499 bid_quantity,
2500 ask_price,
2501 ask_quantity,
2502 timestamp,
2503 })
2504}
2505
2506pub fn parse_bids_asks(data: &Value) -> Result<Vec<ccxt_core::types::BidAsk>> {
2516 if let Some(array) = data.as_array() {
2517 array.iter().map(|item| parse_bid_ask(item)).collect()
2518 } else {
2519 Ok(vec![parse_bid_ask(data)?])
2520 }
2521}
2522
2523pub fn parse_last_price(data: &Value) -> Result<ccxt_core::types::LastPrice> {
2533 use ccxt_core::types::LastPrice;
2534
2535 let symbol = data["symbol"]
2536 .as_str()
2537 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
2538 .to_string();
2539
2540 let formatted_symbol = if symbol.len() >= 6 {
2541 let quote_currencies = ["USDT", "BUSD", "USDC", "BTC", "ETH", "BNB"];
2542 let mut found = false;
2543 let mut formatted = symbol.clone();
2544
2545 for quote in "e_currencies {
2546 if symbol.ends_with(quote) {
2547 let base = &symbol[..symbol.len() - quote.len()];
2548 formatted = format!("{}/{}", base, quote);
2549 found = true;
2550 break;
2551 }
2552 }
2553
2554 if !found { symbol.clone() } else { formatted }
2555 } else {
2556 symbol.clone()
2557 };
2558
2559 let price = parse_decimal(data, "price").unwrap_or(Decimal::ZERO);
2560
2561 let timestamp = data["time"]
2562 .as_i64()
2563 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
2564
2565 let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
2566 .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string())
2567 .unwrap_or_default();
2568
2569 Ok(LastPrice {
2570 symbol: formatted_symbol,
2571 price,
2572 timestamp,
2573 datetime,
2574 })
2575}
2576
2577pub fn parse_last_prices(data: &Value) -> Result<Vec<ccxt_core::types::LastPrice>> {
2587 if let Some(array) = data.as_array() {
2588 array.iter().map(|item| parse_last_price(item)).collect()
2589 } else {
2590 Ok(vec![parse_last_price(data)?])
2591 }
2592}
2593
2594pub fn parse_mark_price(data: &Value) -> Result<ccxt_core::types::MarkPrice> {
2604 use ccxt_core::types::MarkPrice;
2605
2606 let symbol = data["symbol"]
2607 .as_str()
2608 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
2609 .to_string();
2610
2611 let formatted_symbol = if symbol.len() >= 6 {
2612 let quote_currencies = ["USDT", "BUSD", "USDC", "BTC", "ETH", "BNB"];
2613 let mut found = false;
2614 let mut formatted = symbol.clone();
2615
2616 for quote in "e_currencies {
2617 if symbol.ends_with(quote) {
2618 let base = &symbol[..symbol.len() - quote.len()];
2619 formatted = format!("{}/{}", base, quote);
2620 found = true;
2621 break;
2622 }
2623 }
2624
2625 if !found { symbol.clone() } else { formatted }
2626 } else {
2627 symbol.clone()
2628 };
2629
2630 let mark_price = parse_decimal(data, "markPrice").unwrap_or(Decimal::ZERO);
2631 let index_price = parse_decimal(data, "indexPrice");
2632 let estimated_settle_price = parse_decimal(data, "estimatedSettlePrice");
2633 let last_funding_rate = parse_decimal(data, "lastFundingRate");
2634
2635 let next_funding_time = data["nextFundingTime"].as_i64();
2636
2637 let timestamp = data["time"]
2638 .as_i64()
2639 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
2640
2641 Ok(MarkPrice {
2642 symbol: formatted_symbol,
2643 mark_price,
2644 index_price,
2645 estimated_settle_price,
2646 last_funding_rate,
2647 next_funding_time,
2648 interest_rate: None,
2649 timestamp,
2650 })
2651}
2652
2653pub fn parse_mark_prices(data: &Value) -> Result<Vec<ccxt_core::types::MarkPrice>> {
2663 if let Some(array) = data.as_array() {
2664 array.iter().map(|item| parse_mark_price(item)).collect()
2665 } else {
2666 Ok(vec![parse_mark_price(data)?])
2667 }
2668}
2669
2670pub fn parse_ohlcv(data: &Value) -> Result<ccxt_core::types::OHLCV> {
2695 use ccxt_core::error::{Error, ParseError};
2696 use ccxt_core::types::OHLCV;
2697
2698 if let Some(array) = data.as_array() {
2699 if array.len() < 6 {
2700 return Err(Error::from(ParseError::invalid_format(
2701 "data",
2702 "OHLCV array length insufficient",
2703 )));
2704 }
2705
2706 let timestamp = array[0]
2707 .as_i64()
2708 .ok_or_else(|| Error::from(ParseError::invalid_format("data", "无效的时间戳")))?;
2709
2710 let open = array[1]
2711 .as_str()
2712 .and_then(|s| s.parse::<f64>().ok())
2713 .or_else(|| array[1].as_f64())
2714 .ok_or_else(|| Error::from(ParseError::invalid_format("data", "Invalid open price")))?;
2715
2716 let high = array[2]
2717 .as_str()
2718 .and_then(|s| s.parse::<f64>().ok())
2719 .or_else(|| array[2].as_f64())
2720 .ok_or_else(|| Error::from(ParseError::invalid_format("data", "Invalid high price")))?;
2721
2722 let low = array[3]
2723 .as_str()
2724 .and_then(|s| s.parse::<f64>().ok())
2725 .or_else(|| array[3].as_f64())
2726 .ok_or_else(|| Error::from(ParseError::invalid_format("data", "Invalid low price")))?;
2727
2728 let close = array[4]
2729 .as_str()
2730 .and_then(|s| s.parse::<f64>().ok())
2731 .or_else(|| array[4].as_f64())
2732 .ok_or_else(|| {
2733 Error::from(ParseError::invalid_format("data", "Invalid close price"))
2734 })?;
2735
2736 let volume = array[5]
2737 .as_str()
2738 .and_then(|s| s.parse::<f64>().ok())
2739 .or_else(|| array[5].as_f64())
2740 .ok_or_else(|| Error::from(ParseError::invalid_format("data", "Invalid volume")))?;
2741
2742 Ok(OHLCV::new(timestamp, open, high, low, close, volume))
2743 } else {
2744 Err(Error::from(ParseError::invalid_format(
2745 "data",
2746 "OHLCV data must be in array format",
2747 )))
2748 }
2749}
2750
2751pub fn parse_ohlcvs(data: &Value) -> Result<Vec<ccxt_core::types::OHLCV>> {
2757 use ccxt_core::error::{Error, ParseError};
2758
2759 if let Some(array) = data.as_array() {
2760 array.iter().map(|item| parse_ohlcv(item)).collect()
2761 } else {
2762 Err(Error::from(ParseError::invalid_format(
2763 "data",
2764 "OHLCV data list must be in array format",
2765 )))
2766 }
2767}
2768
2769pub fn parse_trading_fee(data: &Value) -> Result<FeeTradingFee> {
2785 use ccxt_core::error::{Error, ParseError};
2786
2787 let symbol = data["symbol"]
2788 .as_str()
2789 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
2790 .to_string();
2791
2792 let maker = data["makerCommission"]
2793 .as_str()
2794 .and_then(|s| Decimal::from_str(s).ok())
2795 .or_else(|| data["makerCommission"].as_f64().and_then(Decimal::from_f64))
2796 .ok_or_else(|| {
2797 Error::from(ParseError::invalid_format(
2798 "data",
2799 "Invalid maker commission",
2800 ))
2801 })?;
2802
2803 let taker = data["takerCommission"]
2804 .as_str()
2805 .and_then(|s| Decimal::from_str(s).ok())
2806 .or_else(|| data["takerCommission"].as_f64().and_then(Decimal::from_f64))
2807 .ok_or_else(|| {
2808 Error::from(ParseError::invalid_format(
2809 "data",
2810 "Invalid taker commission",
2811 ))
2812 })?;
2813
2814 Ok(FeeTradingFee::new(symbol, maker, taker))
2815}
2816
2817pub fn parse_trading_fees(data: &Value) -> Result<Vec<FeeTradingFee>> {
2823 if let Some(array) = data.as_array() {
2824 array.iter().map(|item| parse_trading_fee(item)).collect()
2825 } else {
2826 Ok(vec![parse_trading_fee(data)?])
2827 }
2828}
2829
2830pub fn parse_server_time(data: &Value) -> Result<ccxt_core::types::ServerTime> {
2844 use ccxt_core::error::{Error, ParseError};
2845 use ccxt_core::types::ServerTime;
2846
2847 let server_time = data["serverTime"]
2848 .as_i64()
2849 .ok_or_else(|| Error::from(ParseError::missing_field("serverTime")))?;
2850
2851 Ok(ServerTime::new(server_time))
2852}
2853
2854pub fn parse_order_trades(
2879 data: &Value,
2880 market: Option<&Market>,
2881) -> Result<Vec<ccxt_core::types::Trade>> {
2882 if let Some(array) = data.as_array() {
2883 array.iter().map(|item| parse_trade(item, market)).collect()
2884 } else {
2885 Ok(vec![parse_trade(data, market)?])
2886 }
2887}
2888
2889pub fn parse_edit_order_result(data: &Value, market: Option<&Market>) -> Result<Order> {
2915 let new_order_data = data.get("newOrderResponse").ok_or_else(|| {
2916 Error::from(ParseError::invalid_format(
2917 "data",
2918 "Missing newOrderResponse field",
2919 ))
2920 })?;
2921
2922 parse_order(new_order_data, market)
2923}
2924
2925pub fn parse_ws_ticker(data: &Value, market: Option<&Market>) -> Result<Ticker> {
2998 let market_id = data["s"]
2999 .as_str()
3000 .or_else(|| data["symbol"].as_str())
3001 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?;
3002
3003 let symbol = if let Some(m) = market {
3004 m.symbol.clone()
3005 } else {
3006 market_id.to_string()
3007 };
3008
3009 let event = data["e"].as_str().unwrap_or("bookTicker");
3010
3011 if event == "markPriceUpdate" {
3012 let timestamp = data["E"].as_i64().unwrap_or(0);
3013 return Ok(Ticker {
3014 symbol,
3015 timestamp,
3016 datetime: Some(
3017 chrono::DateTime::from_timestamp_millis(timestamp)
3018 .map(|dt| dt.to_rfc3339())
3019 .unwrap_or_default(),
3020 ),
3021 high: None,
3022 low: None,
3023 bid: None,
3024 bid_volume: None,
3025 ask: None,
3026 ask_volume: None,
3027 vwap: None,
3028 open: None,
3029 close: parse_decimal(data, "p").map(Price::from),
3030 last: parse_decimal(data, "p").map(Price::from),
3031 previous_close: None,
3032 change: None,
3033 percentage: None,
3034 average: None,
3035 base_volume: None,
3036 quote_volume: None,
3037 info: value_to_hashmap(data),
3038 });
3039 }
3040
3041 let timestamp = if event == "bookTicker" {
3042 data["E"]
3043 .as_i64()
3044 .or_else(|| data["time"].as_i64())
3045 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis())
3046 } else {
3047 data["C"]
3048 .as_i64()
3049 .or_else(|| data["E"].as_i64())
3050 .or_else(|| data["time"].as_i64())
3051 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis())
3052 };
3053
3054 let last = parse_decimal_multi(data, &["c", "price"]);
3055
3056 Ok(Ticker {
3057 symbol,
3058 timestamp,
3059 datetime: Some(
3060 chrono::DateTime::from_timestamp_millis(timestamp)
3061 .map(|dt| dt.to_rfc3339())
3062 .unwrap_or_default(),
3063 ),
3064 high: parse_decimal(data, "h").map(Price::from),
3065 low: parse_decimal(data, "l").map(Price::from),
3066 bid: parse_decimal_multi(data, &["b", "bidPrice"]).map(Price::from),
3067 bid_volume: parse_decimal_multi(data, &["B", "bidQty"]).map(Amount::from),
3068 ask: parse_decimal_multi(data, &["a", "askPrice"]).map(Price::from),
3069 ask_volume: parse_decimal_multi(data, &["A", "askQty"]).map(Amount::from),
3070 vwap: parse_decimal(data, "w").map(Price::from),
3071 open: parse_decimal(data, "o").map(Price::from),
3072 close: last.map(Price::from),
3073 last: last.map(Price::from),
3074 previous_close: parse_decimal(data, "x").map(Price::from),
3075 change: parse_decimal(data, "p").map(Price::from),
3076 percentage: parse_decimal(data, "P"),
3077 average: None,
3078 base_volume: parse_decimal(data, "v").map(Amount::from),
3079 quote_volume: parse_decimal(data, "q").map(Amount::from),
3080 info: value_to_hashmap(data),
3081 })
3082}
3083
3084pub fn parse_ws_trade(data: &Value, market: Option<&Market>) -> Result<Trade> {
3133 let market_id = data["s"]
3134 .as_str()
3135 .or_else(|| data["symbol"].as_str())
3136 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?;
3137
3138 let symbol = if let Some(m) = market {
3139 m.symbol.clone()
3140 } else {
3141 market_id.to_string()
3142 };
3143
3144 let id = data["t"]
3145 .as_u64()
3146 .or_else(|| data["a"].as_u64())
3147 .map(|i| i.to_string());
3148
3149 let timestamp = data["T"].as_i64().unwrap_or(0);
3150
3151 let price = parse_f64(data, "L")
3152 .or_else(|| parse_f64(data, "p"))
3153 .and_then(Decimal::from_f64_retain);
3154
3155 let amount = parse_f64(data, "q").and_then(Decimal::from_f64_retain);
3156
3157 let cost = match (price, amount) {
3158 (Some(p), Some(a)) => Some(p * a),
3159 _ => None,
3160 };
3161
3162 let side = if data["m"].as_bool().unwrap_or(false) {
3163 OrderSide::Sell
3164 } else {
3165 OrderSide::Buy
3166 };
3167
3168 let taker_or_maker = if data["m"].as_bool().unwrap_or(false) {
3169 Some(TakerOrMaker::Maker)
3170 } else {
3171 Some(TakerOrMaker::Taker)
3172 };
3173
3174 Ok(Trade {
3175 id,
3176 order: data["orderId"]
3177 .as_u64()
3178 .or_else(|| data["orderid"].as_u64())
3179 .map(|i| i.to_string()),
3180 timestamp,
3181 datetime: Some(
3182 chrono::DateTime::from_timestamp_millis(timestamp)
3183 .map(|dt| dt.to_rfc3339())
3184 .unwrap_or_default(),
3185 ),
3186 symbol,
3187 trade_type: None,
3188 side,
3189 taker_or_maker: taker_or_maker,
3190 price: Price::from(price.unwrap_or(Decimal::ZERO)),
3191 amount: Amount::from(amount.unwrap_or(Decimal::ZERO)),
3192 cost: cost.map(Cost::from),
3193 fee: None,
3194 info: value_to_hashmap(data),
3195 })
3196}
3197
3198pub fn parse_ws_orderbook(data: &Value, symbol: String) -> Result<OrderBook> {
3227 let timestamp = data["E"]
3228 .as_i64()
3229 .or_else(|| data["T"].as_i64())
3230 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
3231
3232 let bids = parse_orderbook_side(&data["b"])?;
3233 let asks = parse_orderbook_side(&data["a"])?;
3234
3235 Ok(OrderBook {
3236 symbol,
3237 timestamp,
3238 datetime: Some(
3239 chrono::DateTime::from_timestamp_millis(timestamp)
3240 .map(|dt| dt.to_rfc3339())
3241 .unwrap_or_default(),
3242 ),
3243 nonce: data["u"].as_i64(),
3244 bids,
3245 asks,
3246 buffered_deltas: std::collections::VecDeque::new(),
3247 bids_map: std::collections::BTreeMap::new(),
3248 asks_map: std::collections::BTreeMap::new(),
3249 is_synced: false,
3250 needs_resync: false,
3251 last_resync_time: 0,
3252 info: value_to_hashmap(data),
3253 })
3254}
3255
3256pub fn parse_ws_ohlcv(data: &Value) -> Result<OHLCV> {
3294 let kline = data["k"]
3295 .as_object()
3296 .ok_or_else(|| Error::from(ParseError::missing_field("k")))?;
3297
3298 let timestamp = kline
3299 .get("t")
3300 .and_then(|v| v.as_i64())
3301 .ok_or_else(|| Error::from(ParseError::missing_field("t")))?;
3302
3303 let open = kline
3304 .get("o")
3305 .and_then(|v| v.as_str())
3306 .and_then(|s| s.parse::<f64>().ok())
3307 .ok_or_else(|| Error::from(ParseError::missing_field("o")))?;
3308
3309 let high = kline
3310 .get("h")
3311 .and_then(|v| v.as_str())
3312 .and_then(|s| s.parse::<f64>().ok())
3313 .ok_or_else(|| Error::from(ParseError::missing_field("h")))?;
3314
3315 let low = kline
3316 .get("l")
3317 .and_then(|v| v.as_str())
3318 .and_then(|s| s.parse::<f64>().ok())
3319 .ok_or_else(|| Error::from(ParseError::missing_field("l")))?;
3320
3321 let close = kline
3322 .get("c")
3323 .and_then(|v| v.as_str())
3324 .and_then(|s| s.parse::<f64>().ok())
3325 .ok_or_else(|| Error::from(ParseError::missing_field("c")))?;
3326
3327 let volume = kline
3328 .get("v")
3329 .and_then(|v| v.as_str())
3330 .and_then(|s| s.parse::<f64>().ok())
3331 .ok_or_else(|| Error::from(ParseError::missing_field("v")))?;
3332
3333 Ok(OHLCV {
3334 timestamp,
3335 open,
3336 high,
3337 low,
3338 close,
3339 volume,
3340 })
3341}
3342
3343pub fn parse_ws_bid_ask(data: &Value) -> Result<BidAsk> {
3366 let symbol = data["s"]
3367 .as_str()
3368 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
3369 .to_string();
3370
3371 let bid_price = data["b"]
3372 .as_str()
3373 .and_then(|s| s.parse::<Decimal>().ok())
3374 .ok_or_else(|| Error::from(ParseError::missing_field("bid_price")))?;
3375
3376 let bid_quantity = data["B"]
3377 .as_str()
3378 .and_then(|s| s.parse::<Decimal>().ok())
3379 .ok_or_else(|| Error::from(ParseError::missing_field("bid_quantity")))?;
3380
3381 let ask_price = data["a"]
3382 .as_str()
3383 .and_then(|s| s.parse::<Decimal>().ok())
3384 .ok_or_else(|| Error::from(ParseError::missing_field("ask_price")))?;
3385
3386 let ask_quantity = data["A"]
3387 .as_str()
3388 .and_then(|s| s.parse::<Decimal>().ok())
3389 .ok_or_else(|| Error::from(ParseError::missing_field("ask_quantity")))?;
3390
3391 let timestamp = data["E"].as_i64().unwrap_or(0);
3392
3393 Ok(BidAsk {
3394 symbol,
3395 bid_price,
3396 bid_quantity,
3397 ask_price,
3398 ask_quantity,
3399 timestamp,
3400 })
3401}
3402
3403pub fn parse_ws_mark_price(data: &Value) -> Result<MarkPrice> {
3419 let symbol = data["s"]
3420 .as_str()
3421 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
3422 .to_string();
3423
3424 let mark_price = data["p"]
3425 .as_str()
3426 .and_then(|s| s.parse::<Decimal>().ok())
3427 .ok_or_else(|| Error::from(ParseError::missing_field("mark_price")))?;
3428
3429 let index_price = data["i"].as_str().and_then(|s| s.parse::<Decimal>().ok());
3430
3431 let estimated_settle_price = data["P"].as_str().and_then(|s| s.parse::<Decimal>().ok());
3432
3433 let last_funding_rate = data["r"].as_str().and_then(|s| s.parse::<Decimal>().ok());
3434
3435 let next_funding_time = data["T"].as_i64();
3436
3437 let interest_rate = None;
3438
3439 let timestamp = data["E"].as_i64().unwrap_or(0);
3440
3441 Ok(MarkPrice {
3442 symbol,
3443 mark_price,
3444 index_price,
3445 estimated_settle_price,
3446 last_funding_rate,
3447 next_funding_time,
3448 interest_rate,
3449 timestamp,
3450 })
3451}
3452
3453pub fn parse_deposit_withdraw_fee(data: &Value) -> Result<ccxt_core::types::DepositWithdrawFee> {
3494 let currency = data["coin"]
3495 .as_str()
3496 .ok_or_else(|| Error::from(ParseError::missing_field("coin")))?
3497 .to_string();
3498
3499 let mut networks = Vec::new();
3500 let mut withdraw_fee = 0.0;
3501 let mut withdraw_min = 0.0;
3502 let mut withdraw_max = 0.0;
3503 let mut deposit_enable = false;
3504 let mut withdraw_enable = false;
3505
3506 if let Some(network_list) = data["networkList"].as_array() {
3507 for network_data in network_list {
3508 let network = parse_network_info(network_data)?;
3509
3510 if network_data["isDefault"].as_bool().unwrap_or(false) {
3511 withdraw_fee = network.withdraw_fee;
3512 withdraw_min = network.withdraw_min;
3513 withdraw_max = network.withdraw_max;
3514 deposit_enable = network.deposit_enable;
3515 withdraw_enable = network.withdraw_enable;
3516 }
3517
3518 networks.push(network);
3519 }
3520 }
3521
3522 if !networks.is_empty() && withdraw_fee == 0.0 {
3523 let first = &networks[0];
3524 withdraw_fee = first.withdraw_fee;
3525 withdraw_min = first.withdraw_min;
3526 withdraw_max = first.withdraw_max;
3527 deposit_enable = first.deposit_enable;
3528 withdraw_enable = first.withdraw_enable;
3529 }
3530
3531 Ok(DepositWithdrawFee {
3532 currency,
3533 withdraw_fee,
3534 withdraw_min,
3535 withdraw_max,
3536 deposit_enable,
3537 withdraw_enable,
3538 networks,
3539 info: Some(data.clone()),
3540 })
3541}
3542
3543pub fn parse_network_info(data: &Value) -> Result<ccxt_core::types::NetworkInfo> {
3571 use ccxt_core::error::{Error, ParseError};
3572 use ccxt_core::types::NetworkInfo;
3573
3574 let network = data["network"]
3575 .as_str()
3576 .ok_or_else(|| Error::from(ParseError::missing_field("network")))?
3577 .to_string();
3578
3579 let name = data["name"].as_str().unwrap_or(&network).to_string();
3580
3581 let withdraw_fee = data["withdrawFee"]
3582 .as_str()
3583 .and_then(|s| s.parse::<f64>().ok())
3584 .or_else(|| data["withdrawFee"].as_f64())
3585 .unwrap_or(0.0);
3586
3587 let withdraw_min = data["withdrawMin"]
3588 .as_str()
3589 .and_then(|s| s.parse::<f64>().ok())
3590 .or_else(|| data["withdrawMin"].as_f64())
3591 .unwrap_or(0.0);
3592
3593 let withdraw_max = data["withdrawMax"]
3594 .as_str()
3595 .and_then(|s| s.parse::<f64>().ok())
3596 .or_else(|| data["withdrawMax"].as_f64())
3597 .unwrap_or(0.0);
3598
3599 let deposit_enable = data["depositEnable"].as_bool().unwrap_or(false);
3600
3601 let withdraw_enable = data["withdrawEnable"].as_bool().unwrap_or(false);
3602
3603 let _is_default = data["isDefault"].as_bool().unwrap_or(false);
3604
3605 let _min_confirm = data["minConfirm"].as_i64().map(|v| v as u32);
3606
3607 let _unlock_confirm = data["unLockConfirm"].as_i64().map(|v| v as u32);
3608
3609 let deposit_confirmations = data["minConfirm"].as_u64().map(|v| v as u32);
3610
3611 let withdraw_confirmations = data["unlockConfirm"].as_u64().map(|v| v as u32);
3612
3613 Ok(NetworkInfo {
3614 network,
3615 name,
3616 withdraw_fee,
3617 withdraw_min,
3618 withdraw_max,
3619 deposit_enable,
3620 withdraw_enable,
3621 deposit_confirmations,
3622 withdraw_confirmations,
3623 })
3624}
3625
3626pub fn parse_deposit_withdraw_fees(
3632 data: &Value,
3633) -> Result<Vec<ccxt_core::types::DepositWithdrawFee>> {
3634 if let Some(array) = data.as_array() {
3635 array
3636 .iter()
3637 .map(|item| parse_deposit_withdraw_fee(item))
3638 .collect()
3639 } else {
3640 Ok(vec![parse_deposit_withdraw_fee(data)?])
3641 }
3642}
3643
3644#[cfg(test)]
3645mod tests {
3646 use super::*;
3647 use serde_json::json;
3648
3649 #[test]
3650 fn test_parse_market() {
3651 let data = json!({
3652 "symbol": "BTCUSDT",
3653 "baseAsset": "BTC",
3654 "quoteAsset": "USDT",
3655 "status": "TRADING",
3656 "isMarginTradingAllowed": true,
3657 "filters": [
3658 {
3659 "filterType": "PRICE_FILTER",
3660 "tickSize": "0.01"
3661 },
3662 {
3663 "filterType": "LOT_SIZE",
3664 "stepSize": "0.00001",
3665 "minQty": "0.00001",
3666 "maxQty": "9000"
3667 },
3668 {
3669 "filterType": "MIN_NOTIONAL",
3670 "minNotional": "10.0"
3671 }
3672 ]
3673 });
3674
3675 let market = parse_market(&data).unwrap();
3676 assert_eq!(market.symbol, "BTC/USDT");
3677 assert_eq!(market.base, "BTC");
3678 assert_eq!(market.quote, "USDT");
3679 assert!(market.active);
3680 assert!(market.margin);
3681 assert_eq!(
3682 market.precision.price,
3683 Some(Decimal::from_str_radix("0.01", 10).unwrap())
3684 );
3685 assert_eq!(
3686 market.precision.amount,
3687 Some(Decimal::from_str_radix("0.00001", 10).unwrap())
3688 );
3689 }
3690
3691 #[test]
3692 fn test_parse_ticker() {
3693 let data = json!({
3694 "symbol": "BTCUSDT",
3695 "lastPrice": "50000.00",
3696 "openPrice": "49000.00",
3697 "highPrice": "51000.00",
3698 "lowPrice": "48500.00",
3699 "volume": "1000.5",
3700 "quoteVolume": "50000000.0",
3701 "bidPrice": "49999.00",
3702 "bidQty": "1.5",
3703 "askPrice": "50001.00",
3704 "askQty": "2.0",
3705 "closeTime": 1609459200000u64,
3706 "priceChange": "1000.00",
3707 "priceChangePercent": "2.04"
3708 });
3709
3710 let ticker = parse_ticker(&data, None).unwrap();
3711 assert_eq!(
3712 ticker.last,
3713 Some(Price::new(Decimal::from_str_radix("50000.00", 10).unwrap()))
3714 );
3715 assert_eq!(
3716 ticker.high,
3717 Some(Price::new(Decimal::from_str_radix("51000.00", 10).unwrap()))
3718 );
3719 assert_eq!(
3720 ticker.low,
3721 Some(Price::new(Decimal::from_str_radix("48500.00", 10).unwrap()))
3722 );
3723 assert_eq!(
3724 ticker.bid,
3725 Some(Price::new(Decimal::from_str_radix("49999.00", 10).unwrap()))
3726 );
3727 assert_eq!(
3728 ticker.ask,
3729 Some(Price::new(Decimal::from_str_radix("50001.00", 10).unwrap()))
3730 );
3731 }
3732
3733 #[test]
3734 fn test_parse_trade() {
3735 let data = json!({
3736 "id": 12345,
3737 "price": "50000.00",
3738 "qty": "0.5",
3739 "time": 1609459200000u64,
3740 "isBuyerMaker": false,
3741 "symbol": "BTCUSDT"
3742 });
3743
3744 let trade = parse_trade(&data, None).unwrap();
3745 assert_eq!(trade.id, Some("12345".to_string()));
3746 assert_eq!(
3747 trade.price,
3748 Price::new(Decimal::from_str_radix("50000.00", 10).unwrap())
3749 );
3750 assert_eq!(
3751 trade.amount,
3752 Amount::new(Decimal::from_str_radix("0.5", 10).unwrap())
3753 );
3754 assert_eq!(trade.side, OrderSide::Buy);
3755 }
3756
3757 #[test]
3758 fn test_parse_order() {
3759 let data = json!({
3760 "orderId": 12345,
3761 "symbol": "BTCUSDT",
3762 "status": "FILLED",
3763 "side": "BUY",
3764 "type": "LIMIT",
3765
3766 "price": "50000.00",
3767 "origQty": "0.5",
3768 "executedQty": "0.5",
3769 "cummulativeQuoteQty": "25000.00",
3770 "time": 1609459200000u64,
3771 "updateTime": 1609459200000u64
3772 });
3773
3774 let order = parse_order(&data, None).unwrap();
3775 assert_eq!(order.id, "12345".to_string());
3776 assert_eq!(order.symbol, "BTCUSDT");
3777 assert_eq!(order.order_type, OrderType::Limit);
3778 assert_eq!(order.side, OrderSide::Buy);
3779 assert_eq!(
3780 order.price,
3781 Some(Decimal::from_str_radix("50000.00", 10).unwrap())
3782 );
3783 assert_eq!(order.amount, Decimal::from_str_radix("0.5", 10).unwrap());
3784 assert_eq!(
3785 order.filled,
3786 Some(Decimal::from_str_radix("0.5", 10).unwrap())
3787 );
3788 }
3789
3790 #[test]
3791 fn test_parse_balance() {
3792 let data = json!({
3793 "balances": [
3794 {
3795 "asset": "BTC",
3796 "free": "1.5",
3797 "locked": "0.5"
3798 }
3799 ]
3800 });
3801
3802 let balance = parse_balance(&data).unwrap();
3803 let btc_balance = balance.balances.get("BTC").unwrap();
3804 assert_eq!(
3805 btc_balance.free,
3806 Decimal::from_str_radix("1.5", 10).unwrap()
3807 );
3808 assert_eq!(
3809 btc_balance.used,
3810 Decimal::from_str_radix("0.5", 10).unwrap()
3811 );
3812 assert_eq!(
3813 btc_balance.total,
3814 Decimal::from_str_radix("2.0", 10).unwrap()
3815 );
3816 }
3817
3818 #[test]
3819 fn test_parse_market_with_filters() {
3820 let data = json!({
3821 "symbol": "ETHUSDT",
3822 "baseAsset": "ETH",
3823 "quoteAsset": "USDT",
3824 "status": "TRADING",
3825 "filters": [
3826 {
3827 "filterType": "PRICE_FILTER",
3828 "tickSize": "0.01",
3829 "minPrice": "0.01",
3830 "maxPrice": "1000000.00"
3831 },
3832 {
3833 "filterType": "LOT_SIZE",
3834 "stepSize": "0.0001",
3835 "minQty": "0.0001",
3836 "maxQty": "90000"
3837 },
3838 {
3839 "filterType": "MIN_NOTIONAL",
3840 "minNotional": "10.0"
3841 },
3842 {
3843 "filterType": "MARKET_LOT_SIZE",
3844 "stepSize": "0.0001",
3845 "minQty": "0.0001",
3846 "maxQty": "50000"
3847 }
3848 ]
3849 });
3850
3851 let market = parse_market(&data).unwrap();
3852 assert_eq!(market.symbol, "ETH/USDT");
3853 assert!(market.limits.amount.is_some());
3854 assert!(market.limits.amount.as_ref().unwrap().min.is_some());
3855 assert!(market.limits.amount.as_ref().unwrap().max.is_some());
3856 assert!(market.limits.price.is_some());
3857 assert!(market.limits.price.as_ref().unwrap().min.is_some());
3858 assert!(market.limits.price.as_ref().unwrap().max.is_some());
3859 assert_eq!(
3860 market.limits.cost.as_ref().unwrap().min,
3861 Some(Decimal::from_str_radix("10.0", 10).unwrap())
3862 );
3863 }
3864
3865 #[test]
3866 fn test_parse_ticker_edge_cases() {
3867 let data = json!({
3868 "symbol": "BTCUSDT",
3869 "lastPrice": "50000.00",
3870 "closeTime": 1609459200000u64
3871 });
3872
3873 let ticker = parse_ticker(&data, None).unwrap();
3874 assert_eq!(
3875 ticker.last,
3876 Some(Price::new(Decimal::from_str_radix("50000.00", 10).unwrap()))
3877 );
3878 assert_eq!(ticker.symbol, "BTCUSDT");
3879 assert_eq!(ticker.bid, None);
3880 assert_eq!(ticker.ask, None);
3881 }
3882
3883 #[test]
3884 fn test_parse_trade_timestamp() {
3885 let data = json!({
3886 "id": 99999,
3887 "price": "45000.50",
3888 "qty": "1.25",
3889 "time": 1609459200000u64,
3890 "isBuyerMaker": true,
3891 "symbol": "BTCUSDT"
3892 });
3893
3894 let trade = parse_trade(&data, None).unwrap();
3895 assert_eq!(trade.timestamp, 1609459200000);
3896 assert_eq!(trade.side, OrderSide::Sell);
3897 }
3898
3899 #[test]
3900 fn test_parse_order_status() {
3901 let statuses = vec![
3902 ("NEW", "open"),
3903 ("PARTIALLY_FILLED", "open"),
3904 ("FILLED", "closed"),
3905 ("CANCELED", "canceled"),
3906 ("REJECTED", "rejected"),
3907 ("EXPIRED", "expired"),
3908 ];
3909
3910 for (binance_status, expected_status) in statuses {
3911 let data = json!({
3912 "orderId": 123,
3913 "symbol": "BTCUSDT",
3914 "status": binance_status,
3915 "side": "BUY",
3916 "type": "LIMIT",
3917 "price": "50000.00",
3918 "origQty": "1.0",
3919 "executedQty": "0.0",
3920 "time": 1609459200000u64
3921 });
3922
3923 let order = parse_order(&data, None).unwrap();
3924 let status_enum = match expected_status {
3926 "open" => OrderStatus::Open,
3927 "closed" => OrderStatus::Closed,
3928 "canceled" | "cancelled" => OrderStatus::Cancelled,
3929 "expired" => OrderStatus::Expired,
3930 "rejected" => OrderStatus::Rejected,
3931 _ => OrderStatus::Open,
3932 };
3933 assert_eq!(order.status, status_enum);
3934 }
3935 }
3936
3937 #[test]
3938 fn test_parse_balance_locked() {
3939 let data = json!({
3940 "balances": [
3941 {
3942 "asset": "USDT",
3943 "free": "10000.50",
3944 "locked": "500.25"
3945 }
3946 ]
3947 });
3948
3949 let balance = parse_balance(&data).unwrap();
3950 let usdt_balance = balance.balances.get("USDT").unwrap();
3951 assert_eq!(
3952 usdt_balance.free,
3953 Decimal::from_str_radix("10000.50", 10).unwrap()
3954 );
3955 assert_eq!(
3956 usdt_balance.used,
3957 Decimal::from_str_radix("500.25", 10).unwrap()
3958 );
3959 assert_eq!(
3960 usdt_balance.total,
3961 Decimal::from_str_radix("10500.75", 10).unwrap()
3962 );
3963 }
3964
3965 #[test]
3966 fn test_parse_empty_response() {
3967 let data = json!({
3968 "lastUpdateId": 12345,
3969 "bids": [],
3970 "asks": []
3971 });
3972
3973 let orderbook = parse_orderbook(&data, "BTC/USDT".to_string()).unwrap();
3974 assert_eq!(orderbook.bids.len(), 0);
3975 assert_eq!(orderbook.asks.len(), 0);
3976 }
3977
3978 #[test]
3979 fn test_currency_precision() {
3980 let data = json!({
3981 "symbol": "BTCUSDT",
3982 "baseAsset": "BTC",
3983 "quoteAsset": "USDT",
3984 "status": "TRADING",
3985 "filters": [
3986 {
3987 "filterType": "LOT_SIZE",
3988 "stepSize": "0.00000001"
3989 },
3990 {
3991 "filterType": "PRICE_FILTER",
3992 "tickSize": "0.01"
3993 }
3994 ]
3995 });
3996
3997 let market = parse_market(&data).unwrap();
3998 assert_eq!(
3999 market.precision.amount,
4000 Some(Decimal::from_str_radix("0.00000001", 10).unwrap())
4001 );
4002 assert_eq!(
4003 market.precision.price,
4004 Some(Decimal::from_str_radix("0.01", 10).unwrap())
4005 );
4006 }
4007
4008 #[test]
4009 fn test_market_limits() {
4010 let data = json!({
4011 "symbol": "ETHBTC",
4012 "baseAsset": "ETH",
4013 "quoteAsset": "BTC",
4014 "status": "TRADING",
4015 "filters": [
4016 {
4017 "filterType": "LOT_SIZE",
4018 "minQty": "0.001",
4019 "maxQty": "100000",
4020 "stepSize": "0.001"
4021 },
4022 {
4023 "filterType": "PRICE_FILTER",
4024 "minPrice": "0.00000100",
4025 "maxPrice": "100000.00000000",
4026 "tickSize": "0.00000100"
4027 },
4028 {
4029 "filterType": "MIN_NOTIONAL",
4030 "minNotional": "0.0001"
4031 }
4032 ]
4033 });
4034
4035 let market = parse_market(&data).unwrap();
4036 assert_eq!(
4037 market.limits.amount.as_ref().unwrap().min,
4038 Some(Decimal::from_str_radix("0.001", 10).unwrap())
4039 );
4040 assert_eq!(
4041 market.limits.amount.as_ref().unwrap().max,
4042 Some(Decimal::from_str_radix("100000.0", 10).unwrap())
4043 );
4044 assert_eq!(
4045 market.limits.price.as_ref().unwrap().min,
4046 Some(Decimal::from_str_radix("0.000001", 10).unwrap())
4047 );
4048 assert_eq!(
4049 market.limits.price.as_ref().unwrap().max,
4050 Some(Decimal::from_str_radix("100000.0", 10).unwrap())
4051 );
4052 assert_eq!(
4053 market.limits.cost.as_ref().unwrap().min,
4054 Some(Decimal::from_str_radix("0.0001", 10).unwrap())
4055 );
4056 }
4057
4058 #[test]
4059 fn test_symbol_normalization() {
4060 let symbols = vec![
4061 ("BTCUSDT", "BTC/USDT"),
4062 ("ETHBTC", "ETH/BTC"),
4063 ("BNBBUSD", "BNB/BUSD"),
4064 ];
4065
4066 for (binance_symbol, ccxt_symbol) in symbols {
4067 let data = json!({
4068 "symbol": binance_symbol,
4069 "baseAsset": &ccxt_symbol[..ccxt_symbol.find('/').unwrap()],
4070 "quoteAsset": &ccxt_symbol[ccxt_symbol.find('/').unwrap() + 1..],
4071 "status": "TRADING"
4072 });
4073
4074 let market = parse_market(&data).unwrap();
4075 assert_eq!(market.symbol, ccxt_symbol);
4076 }
4077 }
4078
4079 #[test]
4080 fn test_timeframe_conversion() {
4081 let timeframes = vec![
4082 ("1m", 60000),
4083 ("5m", 300000),
4084 ("15m", 900000),
4085 ("1h", 3600000),
4086 ("4h", 14400000),
4087 ("1d", 86400000),
4088 ];
4089
4090 for (tf_str, expected_ms) in timeframes {
4091 let ms = match tf_str {
4092 "1m" => 60000,
4093 "5m" => 300000,
4094 "15m" => 900000,
4095 "1h" => 3600000,
4096 "4h" => 14400000,
4097 "1d" => 86400000,
4098 _ => 0,
4099 };
4100 assert_eq!(ms, expected_ms);
4101 }
4102 }
4103}
4104
4105pub fn is_fiat_currency(currency: &str) -> bool {
4119 matches!(
4120 currency.to_uppercase().as_str(),
4121 "USD" | "EUR" | "GBP" | "JPY" | "CNY" | "KRW" | "AUD" | "CAD" | "CHF" | "HKD" | "SGD"
4122 )
4123}
4124
4125pub fn extract_internal_transfer_id(txid: &str) -> String {
4137 const PREFIX: &str = "Internal transfer ";
4138 if txid.starts_with(PREFIX) {
4139 txid[PREFIX.len()..].to_string()
4140 } else {
4141 txid.to_string()
4142 }
4143}
4144
4145pub fn parse_transaction_status_by_type(
4158 status_value: &Value,
4159 is_deposit: bool,
4160) -> ccxt_core::types::TransactionStatus {
4161 use ccxt_core::types::TransactionStatus;
4162
4163 if let Some(status_int) = status_value.as_i64() {
4164 if is_deposit {
4165 match status_int {
4166 0 => TransactionStatus::Pending,
4167 1 => TransactionStatus::Ok,
4168 6 => TransactionStatus::Ok,
4169 _ => TransactionStatus::Pending,
4170 }
4171 } else {
4172 match status_int {
4173 0 => TransactionStatus::Pending,
4174 1 => TransactionStatus::Canceled,
4175 2 => TransactionStatus::Pending,
4176 3 => TransactionStatus::Failed,
4177 4 => TransactionStatus::Pending,
4178 5 => TransactionStatus::Failed,
4179 6 => TransactionStatus::Ok,
4180 _ => TransactionStatus::Pending,
4181 }
4182 }
4183 } else if let Some(status_str) = status_value.as_str() {
4184 match status_str {
4185 "Processing" => TransactionStatus::Pending,
4186 "Failed" => TransactionStatus::Failed,
4187 "Successful" => TransactionStatus::Ok,
4188 "Refunding" => TransactionStatus::Canceled,
4189 "Refunded" => TransactionStatus::Canceled,
4190 "Refund Failed" => TransactionStatus::Failed,
4191 _ => TransactionStatus::Pending,
4192 }
4193 } else {
4194 TransactionStatus::Pending
4195 }
4196}
4197
4198pub fn parse_transaction(
4246 data: &Value,
4247 transaction_type: ccxt_core::types::TransactionType,
4248) -> Result<ccxt_core::types::Transaction> {
4249 use ccxt_core::types::{Transaction, TransactionFee, TransactionStatus, TransactionType};
4250
4251 let is_deposit = matches!(transaction_type, TransactionType::Deposit);
4252
4253 let id = if is_deposit {
4255 data["id"]
4256 .as_str()
4257 .or_else(|| data["orderNo"].as_str())
4258 .unwrap_or("")
4259 .to_string()
4260 } else {
4261 data["id"]
4262 .as_str()
4263 .or_else(|| data["withdrawOrderId"].as_str())
4264 .unwrap_or("")
4265 .to_string()
4266 };
4267
4268 let currency = data["coin"]
4270 .as_str()
4271 .or_else(|| data["fiatCurrency"].as_str())
4272 .unwrap_or("")
4273 .to_string();
4274
4275 let amount = data["amount"]
4276 .as_str()
4277 .and_then(|s| Decimal::from_str(s).ok())
4278 .unwrap_or_else(|| Decimal::ZERO);
4279
4280 let fee = if is_deposit {
4281 None
4282 } else {
4283 data["transactionFee"]
4284 .as_str()
4285 .or_else(|| data["totalFee"].as_str())
4286 .and_then(|s| Decimal::from_str(s).ok())
4287 .map(|cost| TransactionFee {
4288 currency: currency.clone(),
4289 cost,
4290 })
4291 };
4292
4293 let timestamp = if is_deposit {
4294 data["insertTime"].as_i64()
4295 } else {
4296 data["createTime"].as_i64().or_else(|| {
4297 data["applyTime"].as_str().and_then(|s| {
4298 chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
4299 .ok()
4300 .map(|dt| dt.and_utc().timestamp_millis())
4301 })
4302 })
4303 };
4304
4305 let datetime = timestamp.and_then(|ts| ccxt_core::time::iso8601(ts).ok());
4306
4307 let network = data["network"].as_str().map(|s| s.to_string());
4308
4309 let address = data["address"]
4310 .as_str()
4311 .or_else(|| data["depositAddress"].as_str())
4312 .map(|s| s.to_string());
4313
4314 let tag = data["addressTag"]
4315 .as_str()
4316 .or_else(|| data["tag"].as_str())
4317 .filter(|s| !s.is_empty())
4318 .map(|s| s.to_string());
4319
4320 let mut txid = data["txId"]
4321 .as_str()
4322 .or_else(|| data["hash"].as_str())
4323 .map(|s| s.to_string());
4324
4325 let transfer_type = data["transferType"].as_i64();
4326 let is_internal = transfer_type == Some(1);
4327
4328 if is_internal {
4329 if let Some(ref tx) = txid {
4330 txid = Some(extract_internal_transfer_id(tx));
4331 }
4332 }
4333
4334 let status = if let Some(status_value) = data.get("status") {
4335 parse_transaction_status_by_type(status_value, is_deposit)
4336 } else {
4337 TransactionStatus::Pending
4338 };
4339
4340 let updated = data["updateTime"].as_i64();
4341
4342 let comment = data["info"]
4343 .as_str()
4344 .or_else(|| data["comment"].as_str())
4345 .map(|s| s.to_string());
4346
4347 Ok(Transaction {
4348 info: Some(data.clone()),
4349 id,
4350 txid,
4351 timestamp,
4352 datetime,
4353 network,
4354 address: address.clone(),
4355 address_to: if is_deposit { address.clone() } else { None },
4356 address_from: if !is_deposit { address } else { None },
4357 tag: tag.clone(),
4358 tag_to: if is_deposit { tag.clone() } else { None },
4359 tag_from: if !is_deposit { tag } else { None },
4360 transaction_type,
4361 amount,
4362 currency,
4363 status,
4364 updated,
4365 internal: Some(is_internal),
4366 comment,
4367 fee,
4368 })
4369}
4370
4371pub fn parse_deposit_address(data: &Value) -> Result<ccxt_core::types::DepositAddress> {
4392 use ccxt_core::types::DepositAddress;
4393
4394 let currency = data["coin"]
4395 .as_str()
4396 .ok_or_else(|| Error::from(ParseError::missing_field("coin")))?
4397 .to_string();
4398
4399 let address = data["address"]
4400 .as_str()
4401 .ok_or_else(|| Error::from(ParseError::missing_field("address")))?
4402 .to_string();
4403
4404 let network = data["network"].as_str().map(|s| s.to_string()).or_else(|| {
4405 data["url"].as_str().and_then(|url| {
4406 if url.contains("btc.com") {
4407 Some("BTC".to_string())
4408 } else if url.contains("etherscan.io") {
4409 Some("ETH".to_string())
4410 } else if url.contains("tronscan.org") {
4411 Some("TRX".to_string())
4412 } else {
4413 None
4414 }
4415 })
4416 });
4417
4418 let tag = data["tag"]
4419 .as_str()
4420 .or_else(|| data["addressTag"].as_str())
4421 .filter(|s| !s.is_empty())
4422 .map(|s| s.to_string());
4423
4424 Ok(DepositAddress {
4425 info: Some(data.clone()),
4426 currency,
4427 network,
4428 address,
4429 tag,
4430 })
4431}
4432pub fn parse_currency(data: &Value) -> Result<ccxt_core::types::Currency> {
4477 use ccxt_core::types::{Currency, CurrencyNetwork, MinMax};
4478
4479 let code = data["coin"]
4480 .as_str()
4481 .ok_or_else(|| Error::from(ParseError::missing_field("coin")))?
4482 .to_string();
4483
4484 let id = code.clone();
4485 let name = data["name"].as_str().map(|s| s.to_string());
4486
4487 let active = data["trading"].as_bool().unwrap_or(true);
4488
4489 let mut networks = HashMap::new();
4490 let mut global_deposit = false;
4491 let mut global_withdraw = false;
4492 let mut global_fee = None;
4493 let mut global_precision = None;
4494 let mut global_limits = MinMax::default();
4495
4496 if let Some(network_list) = data["networkList"].as_array() {
4497 for network_data in network_list {
4498 let network_id = network_data["network"]
4499 .as_str()
4500 .unwrap_or(&code)
4501 .to_string();
4502
4503 let is_default = network_data["isDefault"].as_bool().unwrap_or(false);
4504 let deposit_enable = network_data["depositEnable"].as_bool().unwrap_or(false);
4505 let withdraw_enable = network_data["withdrawEnable"].as_bool().unwrap_or(false);
4506
4507 if is_default {
4508 global_deposit = deposit_enable;
4509 global_withdraw = withdraw_enable;
4510 }
4511
4512 let fee = network_data["withdrawFee"]
4513 .as_str()
4514 .and_then(|s| Decimal::from_str(s).ok());
4515
4516 if is_default && fee.is_some() {
4517 global_fee = fee;
4518 }
4519
4520 let precision = network_data["withdrawIntegerMultiple"]
4521 .as_str()
4522 .and_then(|s| Decimal::from_str(s).ok());
4523
4524 if is_default && precision.is_some() {
4525 global_precision = precision;
4526 }
4527
4528 let withdraw_min = network_data["withdrawMin"]
4529 .as_str()
4530 .and_then(|s| Decimal::from_str(s).ok());
4531
4532 let withdraw_max = network_data["withdrawMax"]
4533 .as_str()
4534 .and_then(|s| Decimal::from_str(s).ok());
4535
4536 let limits = MinMax {
4537 min: withdraw_min,
4538 max: withdraw_max,
4539 };
4540
4541 if is_default {
4542 global_limits = limits.clone();
4543 }
4544
4545 let network = CurrencyNetwork {
4546 network: network_id.clone(),
4547 id: Some(network_id.clone()),
4548 name: network_data["name"].as_str().map(|s| s.to_string()),
4549 active: deposit_enable && withdraw_enable,
4550 deposit: deposit_enable,
4551 withdraw: withdraw_enable,
4552 fee,
4553 precision,
4554 limits,
4555 info: value_to_hashmap(network_data),
4556 };
4557
4558 networks.insert(network_id, network);
4559 }
4560 }
4561
4562 Ok(Currency {
4563 code,
4564 id,
4565 name,
4566 active,
4567 deposit: global_deposit,
4568 withdraw: global_withdraw,
4569 fee: global_fee,
4570 precision: global_precision,
4571 limits: global_limits,
4572 networks,
4573 currency_type: if data["isLegalMoney"].as_bool().unwrap_or(false) {
4574 Some("fiat".to_string())
4575 } else {
4576 Some("crypto".to_string())
4577 },
4578 info: value_to_hashmap(data),
4579 })
4580}
4581
4582pub fn parse_currencies(data: &Value) -> Result<Vec<ccxt_core::types::Currency>> {
4592 if let Some(array) = data.as_array() {
4593 array.iter().map(parse_currency).collect()
4594 } else {
4595 Ok(vec![parse_currency(data)?])
4596 }
4597}
4598
4599pub fn parse_stats_24hr(data: &Value) -> Result<ccxt_core::types::Stats24hr> {
4636 use ccxt_core::types::Stats24hr;
4637
4638 let symbol = data["symbol"]
4639 .as_str()
4640 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
4641 .to_string();
4642
4643 let price_change = data["priceChange"]
4644 .as_str()
4645 .and_then(|s| Decimal::from_str(s).ok());
4646
4647 let price_change_percent = data["priceChangePercent"]
4648 .as_str()
4649 .and_then(|s| Decimal::from_str(s).ok());
4650
4651 let weighted_avg_price = data["weightedAvgPrice"]
4652 .as_str()
4653 .and_then(|s| Decimal::from_str(s).ok());
4654
4655 let prev_close_price = data["prevClosePrice"]
4656 .as_str()
4657 .and_then(|s| Decimal::from_str(s).ok());
4658
4659 let last_price = data["lastPrice"]
4660 .as_str()
4661 .and_then(|s| Decimal::from_str(s).ok());
4662
4663 let last_qty = data["lastQty"]
4664 .as_str()
4665 .and_then(|s| Decimal::from_str(s).ok());
4666
4667 let bid_price = data["bidPrice"]
4668 .as_str()
4669 .and_then(|s| Decimal::from_str(s).ok());
4670
4671 let bid_qty = data["bidQty"]
4672 .as_str()
4673 .and_then(|s| Decimal::from_str(s).ok());
4674
4675 let ask_price = data["askPrice"]
4676 .as_str()
4677 .and_then(|s| Decimal::from_str(s).ok());
4678
4679 let ask_qty = data["askQty"]
4680 .as_str()
4681 .and_then(|s| Decimal::from_str(s).ok());
4682
4683 let open_price = data["openPrice"]
4685 .as_str()
4686 .and_then(|s| Decimal::from_str(s).ok());
4687
4688 let high_price = data["highPrice"]
4689 .as_str()
4690 .and_then(|s| Decimal::from_str(s).ok());
4691
4692 let low_price = data["lowPrice"]
4693 .as_str()
4694 .and_then(|s| Decimal::from_str(s).ok());
4695
4696 let volume = data["volume"]
4698 .as_str()
4699 .and_then(|s| Decimal::from_str(s).ok());
4700
4701 let quote_volume = data["quoteVolume"]
4702 .as_str()
4703 .and_then(|s| Decimal::from_str(s).ok());
4704
4705 let open_time = data["openTime"].as_i64();
4706 let close_time = data["closeTime"].as_i64();
4707 let first_id = data["firstId"].as_i64();
4708 let last_id = data["lastId"].as_i64();
4709 let count = data["count"].as_i64();
4710
4711 Ok(Stats24hr {
4712 symbol,
4713 price_change,
4714 price_change_percent,
4715 weighted_avg_price,
4716 prev_close_price,
4717 last_price,
4718 last_qty,
4719 bid_price,
4720 bid_qty,
4721 ask_price,
4722 ask_qty,
4723 open_price,
4724 high_price,
4725 low_price,
4726 volume,
4727 quote_volume,
4728 open_time,
4729 close_time,
4730 first_id,
4731 last_id,
4732 count,
4733 info: value_to_hashmap(data),
4734 })
4735}
4736
4737pub fn parse_agg_trade(data: &Value, symbol: Option<String>) -> Result<ccxt_core::types::AggTrade> {
4762 use ccxt_core::types::AggTrade;
4763
4764 let agg_id = data["a"]
4765 .as_i64()
4766 .ok_or_else(|| Error::from(ParseError::missing_field("a")))?;
4767
4768 let price = data["p"]
4769 .as_str()
4770 .and_then(|s| Decimal::from_str(s).ok())
4771 .ok_or_else(|| Error::from(ParseError::missing_field("p")))?;
4772
4773 let quantity = data["q"]
4774 .as_str()
4775 .and_then(|s| Decimal::from_str(s).ok())
4776 .ok_or_else(|| Error::from(ParseError::missing_field("q")))?;
4777
4778 let first_trade_id = data["f"]
4779 .as_i64()
4780 .ok_or_else(|| Error::from(ParseError::missing_field("f")))?;
4781
4782 let last_trade_id = data["l"]
4783 .as_i64()
4784 .ok_or_else(|| Error::from(ParseError::missing_field("l")))?;
4785
4786 let timestamp = data["T"]
4787 .as_i64()
4788 .ok_or_else(|| Error::from(ParseError::missing_field("T")))?;
4789
4790 let is_buyer_maker = data["m"].as_bool().unwrap_or(false);
4791 let is_best_match = data["M"].as_bool();
4792
4793 Ok(AggTrade {
4794 agg_id,
4795 price,
4796 quantity,
4797 first_trade_id,
4798 last_trade_id,
4799 timestamp,
4800 is_buyer_maker,
4801 is_best_match,
4802 symbol,
4803 })
4804}
4805
4806pub fn parse_trading_limits(
4842 data: &Value,
4843 _symbol: String,
4844) -> Result<ccxt_core::types::TradingLimits> {
4845 use ccxt_core::types::{MinMax, TradingLimits};
4846
4847 let mut price_limits = MinMax::default();
4848 let mut amount_limits = MinMax::default();
4849 let mut cost_limits = MinMax::default();
4850
4851 if let Some(filters) = data["filters"].as_array() {
4852 for filter in filters {
4853 let filter_type = filter["filterType"].as_str().unwrap_or("");
4854
4855 match filter_type {
4856 "PRICE_FILTER" => {
4857 price_limits.min = filter["minPrice"]
4858 .as_str()
4859 .and_then(|s| Decimal::from_str(s).ok());
4860 price_limits.max = filter["maxPrice"]
4861 .as_str()
4862 .and_then(|s| Decimal::from_str(s).ok());
4863 }
4864 "LOT_SIZE" => {
4865 amount_limits.min = filter["minQty"]
4866 .as_str()
4867 .and_then(|s| Decimal::from_str(s).ok());
4868 amount_limits.max = filter["maxQty"]
4869 .as_str()
4870 .and_then(|s| Decimal::from_str(s).ok());
4871 }
4872 "MIN_NOTIONAL" | "NOTIONAL" => {
4873 cost_limits.min = filter["minNotional"]
4874 .as_str()
4875 .and_then(|s| Decimal::from_str(s).ok());
4876 }
4877 _ => {}
4878 }
4879 }
4880 }
4881
4882 Ok(TradingLimits {
4883 min: None,
4884 max: None,
4885 amount: Some(amount_limits),
4886 price: Some(price_limits),
4887 cost: Some(cost_limits),
4888 })
4889}
4890pub fn parse_leverage_tier(data: &Value, market: &Market) -> Result<LeverageTier> {
4901 let tier = data["bracket"]
4902 .as_i64()
4903 .or_else(|| data["tier"].as_i64())
4904 .unwrap_or(0) as i32;
4905
4906 let min_notional = parse_decimal(data, "notionalFloor")
4907 .or_else(|| parse_decimal(data, "minNotional"))
4908 .unwrap_or(Decimal::ZERO);
4909
4910 let max_notional = parse_decimal(data, "notionalCap")
4911 .or_else(|| parse_decimal(data, "maxNotional"))
4912 .unwrap_or(Decimal::MAX);
4913
4914 let maintenance_margin_rate = parse_decimal(data, "maintMarginRatio")
4915 .or_else(|| parse_decimal(data, "maintenanceMarginRate"))
4916 .unwrap_or(Decimal::ZERO);
4917
4918 let max_leverage = data["initialLeverage"]
4919 .as_i64()
4920 .or_else(|| data["maxLeverage"].as_i64())
4921 .unwrap_or(1) as i32;
4922
4923 Ok(LeverageTier {
4924 info: data.clone(),
4925 tier,
4926 symbol: market.symbol.clone(),
4927 currency: market.quote.clone(),
4928 min_notional,
4929 max_notional,
4930 maintenance_margin_rate,
4931 max_leverage,
4932 })
4933}
4934
4935pub fn parse_isolated_borrow_rates(
4945 data: &Value,
4946) -> Result<std::collections::HashMap<String, ccxt_core::types::IsolatedBorrowRate>> {
4947 use ccxt_core::types::IsolatedBorrowRate;
4948 use std::collections::HashMap;
4949
4950 let mut rates_map = HashMap::new();
4951
4952 if let Some(array) = data.as_array() {
4953 for item in array {
4954 let symbol = item["symbol"].as_str().unwrap_or("");
4955 let base = item["base"].as_str().unwrap_or("");
4956 let quote = item["quote"].as_str().unwrap_or("");
4957
4958 let base_rate = item["dailyInterestRate"]
4959 .as_str()
4960 .and_then(|s| s.parse::<f64>().ok())
4961 .unwrap_or(0.0);
4962
4963 let quote_rate = item["quoteDailyInterestRate"]
4964 .as_str()
4965 .and_then(|s| s.parse::<f64>().ok())
4966 .or_else(|| {
4967 item["dailyInterestRate"]
4968 .as_str()
4969 .and_then(|s| s.parse::<f64>().ok())
4970 })
4971 .unwrap_or(0.0);
4972
4973 let timestamp = item["timestamp"].as_i64().or_else(|| item["time"].as_i64());
4974
4975 let datetime = timestamp.and_then(|ts| {
4976 chrono::DateTime::from_timestamp_millis(ts).map(|dt| dt.to_rfc3339())
4977 });
4978
4979 let isolated_rate = IsolatedBorrowRate {
4980 symbol: symbol.to_string(),
4981 base: base.to_string(),
4982 base_rate,
4983 quote: quote.to_string(),
4984 quote_rate,
4985 period: 86400000, timestamp,
4987 datetime,
4988 info: item.clone(),
4989 };
4990
4991 rates_map.insert(symbol.to_string(), isolated_rate);
4992 }
4993 }
4994
4995 Ok(rates_map)
4996}
4997
4998pub fn parse_borrow_interests(data: &Value) -> Result<Vec<BorrowInterest>> {
5008 let mut interests = Vec::new();
5009
5010 if let Some(array) = data.as_array() {
5011 for item in array {
5012 match parse_borrow_interest(item) {
5013 Ok(interest) => interests.push(interest),
5014 Err(e) => {
5015 eprintln!("Failed to parse borrow interest: {}", e);
5016 }
5017 }
5018 }
5019 }
5020
5021 Ok(interests)
5022}
5023
5024pub fn parse_borrow_rate_history(data: &Value, currency: &str) -> Result<BorrowRateHistory> {
5035 let timestamp = data["timestamp"]
5036 .as_i64()
5037 .or_else(|| data["time"].as_i64())
5038 .unwrap_or(0);
5039
5040 let rate = data["hourlyInterestRate"]
5041 .as_str()
5042 .or_else(|| data["dailyInterestRate"].as_str())
5043 .or_else(|| data["rate"].as_str())
5044 .and_then(|s| s.parse::<f64>().ok())
5045 .or_else(|| data["hourlyInterestRate"].as_f64())
5046 .or_else(|| data["dailyInterestRate"].as_f64())
5047 .or_else(|| data["rate"].as_f64())
5048 .unwrap_or(0.0);
5049
5050 let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
5051 .map(|dt| dt.to_rfc3339())
5052 .unwrap_or_default();
5053
5054 let symbol = data["symbol"].as_str().map(|s| s.to_string());
5055 let vip_level = data["vipLevel"].as_i64().map(|v| v as i32);
5056
5057 Ok(BorrowRateHistory {
5058 currency: currency.to_string(),
5059 symbol,
5060 rate,
5061 timestamp,
5062 datetime,
5063 vip_level,
5064 info: data.clone(),
5065 })
5066}
5067
5068pub fn parse_ledger_entry(data: &Value) -> Result<LedgerEntry> {
5078 let id = data["tranId"]
5079 .as_i64()
5080 .or_else(|| data["id"].as_i64())
5081 .map(|v| v.to_string())
5082 .or_else(|| data["tranId"].as_str().map(|s| s.to_string()))
5083 .or_else(|| data["id"].as_str().map(|s| s.to_string()))
5084 .unwrap_or_default();
5085
5086 let currency = data["asset"]
5087 .as_str()
5088 .or_else(|| data["currency"].as_str())
5089 .unwrap_or("")
5090 .to_string();
5091
5092 let amount = data["amount"]
5093 .as_str()
5094 .and_then(|s| s.parse::<f64>().ok())
5095 .or_else(|| data["amount"].as_f64())
5096 .or_else(|| data["qty"].as_str().and_then(|s| s.parse::<f64>().ok()))
5097 .or_else(|| data["qty"].as_f64())
5098 .unwrap_or(0.0);
5099
5100 let timestamp = data["timestamp"]
5101 .as_i64()
5102 .or_else(|| data["time"].as_i64())
5103 .unwrap_or(0);
5104
5105 let type_str = data["type"].as_str().unwrap_or("");
5106 let (direction, entry_type) = match type_str {
5107 "DEPOSIT" => (LedgerDirection::In, LedgerEntryType::Deposit),
5108 "WITHDRAW" => (LedgerDirection::Out, LedgerEntryType::Withdrawal),
5109 "BUY" | "SELL" => (
5110 if amount >= 0.0 {
5111 LedgerDirection::In
5112 } else {
5113 LedgerDirection::Out
5114 },
5115 LedgerEntryType::Trade,
5116 ),
5117 "FEE" => (LedgerDirection::Out, LedgerEntryType::Fee),
5118 "REBATE" => (LedgerDirection::In, LedgerEntryType::Rebate),
5119 "TRANSFER" => (
5120 if amount >= 0.0 {
5121 LedgerDirection::In
5122 } else {
5123 LedgerDirection::Out
5124 },
5125 LedgerEntryType::Transfer,
5126 ),
5127 _ => (
5128 if amount >= 0.0 {
5129 LedgerDirection::In
5130 } else {
5131 LedgerDirection::Out
5132 },
5133 LedgerEntryType::Trade,
5134 ),
5135 };
5136
5137 let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
5138 .map(|dt| dt.to_rfc3339())
5139 .unwrap_or_default();
5140
5141 Ok(LedgerEntry {
5142 id,
5143 currency,
5144 account: None,
5145 reference_account: None,
5146 reference_id: None,
5147 type_: entry_type,
5148 direction,
5149 amount: amount.abs(),
5150 timestamp,
5151 datetime,
5152 before: None,
5153 after: None,
5154 status: None,
5155 fee: None,
5156 info: data.clone(),
5157 })
5158}
5159
5160#[cfg(test)]
5161mod transaction_tests {
5162 use super::*;
5163 use ccxt_core::types::{TransactionStatus, TransactionType};
5164 use serde_json::json;
5165
5166 #[test]
5167 fn test_is_fiat_currency() {
5168 assert!(is_fiat_currency("USD"));
5169 assert!(is_fiat_currency("eur"));
5170 assert!(is_fiat_currency("CNY"));
5171 assert!(!is_fiat_currency("BTC"));
5172 assert!(!is_fiat_currency("ETH"));
5173 }
5174
5175 #[test]
5176 fn test_extract_internal_transfer_id() {
5177 assert_eq!(
5178 extract_internal_transfer_id("Internal transfer 123456"),
5179 "123456"
5180 );
5181 assert_eq!(
5182 extract_internal_transfer_id("normal_hash_abc"),
5183 "normal_hash_abc"
5184 );
5185 }
5186
5187 #[test]
5188 fn test_parse_transaction_status_deposit() {
5189 assert_eq!(
5190 parse_transaction_status_by_type(&json!(0), true),
5191 TransactionStatus::Pending
5192 );
5193 assert_eq!(
5194 parse_transaction_status_by_type(&json!(1), true),
5195 TransactionStatus::Ok
5196 );
5197 assert_eq!(
5198 parse_transaction_status_by_type(&json!(6), true),
5199 TransactionStatus::Ok
5200 );
5201 assert_eq!(
5202 parse_transaction_status_by_type(&json!("Processing"), true),
5203 TransactionStatus::Pending
5204 );
5205 assert_eq!(
5206 parse_transaction_status_by_type(&json!("Successful"), true),
5207 TransactionStatus::Ok
5208 );
5209 }
5210
5211 #[test]
5212 fn test_parse_transaction_status_withdrawal() {
5213 assert_eq!(
5214 parse_transaction_status_by_type(&json!(0), false),
5215 TransactionStatus::Pending
5216 );
5217 assert_eq!(
5218 parse_transaction_status_by_type(&json!(1), false),
5219 TransactionStatus::Canceled
5220 );
5221 assert_eq!(
5222 parse_transaction_status_by_type(&json!(6), false),
5223 TransactionStatus::Ok
5224 );
5225 }
5226
5227 #[test]
5228 fn test_parse_deposit_transaction() {
5229 let data = json!({
5230 "id": "deposit123",
5231 "amount": "0.5",
5232 "coin": "BTC",
5233 "network": "BTC",
5234 "status": 1,
5235 "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
5236 "addressTag": "",
5237 "txId": "hash123abc",
5238 "insertTime": 1609459200000i64,
5239 "transferType": 0
5240 });
5241
5242 let tx = parse_transaction(&data, TransactionType::Deposit).unwrap();
5243 assert_eq!(tx.id, "deposit123");
5244 assert_eq!(tx.currency, "BTC");
5245 assert_eq!(tx.amount, Decimal::from_str("0.5").unwrap());
5246 assert_eq!(tx.status, TransactionStatus::Ok);
5247 assert_eq!(tx.txid, Some("hash123abc".to_string()));
5248 assert_eq!(tx.internal, Some(false));
5249 assert!(tx.is_deposit());
5250 }
5251
5252 #[test]
5253 fn test_parse_withdrawal_transaction() {
5254 let data = json!({
5255 "id": "withdrawal456",
5256 "amount": "0.3",
5257 "transactionFee": "0.0005",
5258 "coin": "BTC",
5259 "status": 6,
5260 "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
5261 "txId": "hash456def",
5262 "applyTime": "2021-01-01 00:00:00",
5263 "network": "BTC",
5264 "transferType": 0
5265 });
5266
5267 let tx = parse_transaction(&data, TransactionType::Withdrawal).unwrap();
5268 assert_eq!(tx.id, "withdrawal456");
5269 assert_eq!(tx.currency, "BTC");
5270 assert_eq!(tx.amount, Decimal::from_str("0.3").unwrap());
5271 assert_eq!(tx.status, TransactionStatus::Ok);
5272 assert!(tx.fee.is_some());
5273 assert_eq!(
5274 tx.fee.as_ref().unwrap().cost,
5275 Decimal::from_str("0.0005").unwrap()
5276 );
5277 assert!(tx.is_withdrawal());
5278 }
5279
5280 #[test]
5281 fn test_parse_internal_transfer() {
5282 let data = json!({
5283 "id": "internal789",
5284 "amount": "1.0",
5285 "coin": "USDT",
5286 "status": 1,
5287 "txId": "Internal transfer 789xyz",
5288 "insertTime": 1609459200000i64,
5289 "transferType": 1
5290 });
5291
5292 let tx = parse_transaction(&data, TransactionType::Deposit).unwrap();
5293 assert_eq!(tx.internal, Some(true));
5294 assert_eq!(tx.txid, Some("789xyz".to_string()));
5295 }
5296
5297 #[test]
5298 fn test_parse_deposit_address() {
5299 let data = json!({
5300 "coin": "BTC",
5301 "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
5302 "tag": "",
5303 "network": "BTC",
5304 "url": "https://btc.com/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
5305 });
5306
5307 let addr = parse_deposit_address(&data).unwrap();
5308 assert_eq!(addr.currency, "BTC");
5309 assert_eq!(addr.address, "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa");
5310 assert_eq!(addr.network, Some("BTC".to_string()));
5311 assert_eq!(addr.tag, None);
5312 }
5313
5314 #[test]
5315 fn test_parse_deposit_address_with_tag() {
5316 let data = json!({
5317 "coin": "XRP",
5318 "address": "rLHzPsX6oXkzU9rKmLwCdxoEFdLQsSz6Xg",
5319 "tag": "123456",
5320 "network": "XRP"
5321 });
5322
5323 let addr = parse_deposit_address(&data).unwrap();
5324 assert_eq!(addr.currency, "XRP");
5325 assert_eq!(addr.tag, Some("123456".to_string()));
5326 }
5327}
5328
5329#[cfg(test)]
5334mod ws_parser_tests {
5335 use super::*;
5336 use rust_decimal_macros::dec;
5337 use serde_json::json;
5338
5339 #[test]
5340 fn test_parse_ws_ticker_24hr() {
5341 let data = json!({
5342 "e": "24hrTicker",
5343 "E": 1609459200000i64,
5344 "s": "BTCUSDT",
5345 "p": "1000.00",
5346 "P": "2.04",
5347 "c": "50000.00",
5348 "o": "49000.00",
5349 "h": "51000.00",
5350 "l": "48500.00",
5351 "v": "1000.5",
5352 "q": "50000000.0",
5353 "b": "49999.00",
5354 "B": "1.5",
5355 "a": "50001.00",
5356 "A": "2.0"
5357 });
5358
5359 let ticker = parse_ws_ticker(&data, None).unwrap();
5360 assert_eq!(ticker.symbol, "BTCUSDT");
5361 assert_eq!(
5362 ticker.last,
5363 Some(Price::new(Decimal::from_str_radix("50000.00", 10).unwrap()))
5364 );
5365 assert_eq!(
5366 ticker.open,
5367 Some(Price::new(Decimal::from_str_radix("49000.00", 10).unwrap()))
5368 );
5369 assert_eq!(
5370 ticker.high,
5371 Some(Price::new(Decimal::from_str_radix("51000.00", 10).unwrap()))
5372 );
5373 assert_eq!(
5374 ticker.low,
5375 Some(Price::new(Decimal::from_str_radix("48500.00", 10).unwrap()))
5376 );
5377 assert_eq!(
5378 ticker.bid,
5379 Some(Price::new(Decimal::from_str_radix("49999.00", 10).unwrap()))
5380 );
5381 assert_eq!(
5382 ticker.ask,
5383 Some(Price::new(Decimal::from_str_radix("50001.00", 10).unwrap()))
5384 );
5385 assert_eq!(ticker.timestamp, 1609459200000);
5386 }
5387
5388 #[test]
5389 fn test_parse_ws_ticker_mark_price() {
5390 let data = json!({
5391 "e": "markPriceUpdate",
5392 "E": 1609459200000i64,
5393 "s": "BTCUSDT",
5394 "p": "50250.50",
5395 "i": "50000.00",
5396 "r": "0.00010000",
5397 "T": 1609459300000i64
5398 });
5399
5400 let ticker = parse_ws_ticker(&data, None).unwrap();
5401 assert_eq!(ticker.symbol, "BTCUSDT");
5402 assert_eq!(
5403 ticker.last,
5404 Some(Price::new(Decimal::from_str_radix("50250.50", 10).unwrap()))
5405 );
5406 assert_eq!(ticker.timestamp, 1609459200000);
5407 }
5408
5409 #[test]
5410 fn test_parse_ws_ticker_book_ticker() {
5411 let data = json!({
5412 "s": "BTCUSDT",
5413 "b": "49999.00",
5414 "B": "1.5",
5415 "a": "50001.00",
5416 "A": "2.0",
5417 "E": 1609459200000i64
5418 });
5419
5420 let ticker = parse_ws_ticker(&data, None).unwrap();
5421 assert_eq!(ticker.symbol, "BTCUSDT");
5422 assert_eq!(
5423 ticker.bid,
5424 Some(Price::new(Decimal::from_str_radix("49999.00", 10).unwrap()))
5425 );
5426 assert_eq!(
5427 ticker.ask,
5428 Some(Price::new(Decimal::from_str_radix("50001.00", 10).unwrap()))
5429 );
5430 assert_eq!(ticker.timestamp, 1609459200000);
5431 }
5432
5433 #[test]
5434 fn test_parse_ws_trade() {
5435 let data = json!({
5436 "e": "trade",
5437 "E": 1609459200000i64,
5438 "s": "BTCUSDT",
5439 "t": 12345,
5440 "p": "50000.00",
5441 "q": "0.5",
5442 "T": 1609459200000i64,
5443 "m": false
5444 });
5445
5446 let trade = parse_ws_trade(&data, None).unwrap();
5447 assert_eq!(trade.id, Some("12345".to_string()));
5448 assert_eq!(trade.symbol, "BTCUSDT");
5449 assert_eq!(
5450 trade.price,
5451 Price::new(Decimal::from_str_radix("50000.00", 10).unwrap())
5452 );
5453 assert_eq!(
5454 trade.amount,
5455 Amount::new(Decimal::from_str_radix("0.5", 10).unwrap())
5456 );
5457 assert_eq!(trade.timestamp, 1609459200000);
5458 assert_eq!(trade.side, OrderSide::Buy); }
5460
5461 #[test]
5462 fn test_parse_ws_trade_agg() {
5463 let data = json!({
5464 "e": "aggTrade",
5465 "E": 1609459200000i64,
5466 "s": "BTCUSDT",
5467 "a": 67890,
5468 "p": "50000.00",
5469 "q": "0.5",
5470 "T": 1609459200000i64,
5471 "m": true
5472 });
5473
5474 let trade = parse_ws_trade(&data, None).unwrap();
5475 assert_eq!(trade.id, Some("67890".to_string()));
5476 assert_eq!(trade.symbol, "BTCUSDT");
5477 assert_eq!(trade.side, OrderSide::Sell); }
5479
5480 #[test]
5481 fn test_parse_ws_orderbook() {
5482 let data = json!({
5483 "e": "depthUpdate",
5484 "E": 1609459200000i64,
5485 "s": "BTCUSDT",
5486 "U": 157,
5487 "u": 160,
5488 "b": [
5489 ["49999.00", "1.5"],
5490 ["49998.00", "2.0"]
5491 ],
5492 "a": [
5493 ["50001.00", "2.0"],
5494 ["50002.00", "1.5"]
5495 ]
5496 });
5497
5498 let orderbook = parse_ws_orderbook(&data, "BTCUSDT".to_string()).unwrap();
5499 assert_eq!(orderbook.symbol, "BTCUSDT");
5500 assert_eq!(orderbook.bids.len(), 2);
5501 assert_eq!(orderbook.asks.len(), 2);
5502 assert_eq!(
5503 orderbook.bids[0].price,
5504 Price::new(Decimal::from_str_radix("49999.00", 10).unwrap())
5505 );
5506 assert_eq!(
5507 orderbook.bids[0].amount,
5508 Amount::new(Decimal::from_str_radix("1.5", 10).unwrap())
5509 );
5510 assert_eq!(
5511 orderbook.asks[0].price,
5512 Price::new(Decimal::from_str_radix("50001.00", 10).unwrap())
5513 );
5514 assert_eq!(
5515 orderbook.asks[0].amount,
5516 Amount::new(Decimal::from_str_radix("2.0", 10).unwrap())
5517 );
5518 assert_eq!(orderbook.timestamp, 1609459200000);
5519 }
5520
5521 #[test]
5522 fn test_parse_ws_ohlcv() {
5523 let data = json!({
5524 "e": "kline",
5525 "E": 1609459200000i64,
5526 "s": "BTCUSDT",
5527 "k": {
5528 "t": 1609459200000i64,
5529 "o": "49000.00",
5530 "h": "51000.00",
5531 "l": "48500.00",
5532 "c": "50000.00",
5533 "v": "1000.5"
5534 }
5535 });
5536
5537 let ohlcv = parse_ws_ohlcv(&data).unwrap();
5538 assert_eq!(ohlcv.timestamp, 1609459200000);
5539 assert_eq!(ohlcv.open, 49000.00);
5540 assert_eq!(ohlcv.high, 51000.00);
5541 assert_eq!(ohlcv.low, 48500.00);
5542 assert_eq!(ohlcv.close, 50000.00);
5543 assert_eq!(ohlcv.volume, 1000.5);
5544 }
5545
5546 #[test]
5547 fn test_parse_ws_bid_ask() {
5548 let data = json!({
5549 "s": "BTCUSDT",
5550 "b": "49999.00",
5551 "B": "1.5",
5552 "a": "50001.00",
5553 "A": "2.0",
5554 "E": 1609459200000i64
5555 });
5556
5557 let bid_ask = parse_ws_bid_ask(&data).unwrap();
5558 assert_eq!(bid_ask.symbol, "BTCUSDT");
5559 assert_eq!(bid_ask.bid_price, dec!(49999.00));
5560 assert_eq!(bid_ask.bid_quantity, dec!(1.5));
5561 assert_eq!(bid_ask.ask_price, dec!(50001.00));
5562 assert_eq!(bid_ask.ask_quantity, dec!(2.0));
5563 assert_eq!(bid_ask.timestamp, 1609459200000);
5564
5565 let spread = bid_ask.spread();
5567 assert_eq!(spread, dec!(2.0));
5568
5569 let mid_price = bid_ask.mid_price();
5570 assert_eq!(mid_price, dec!(50000.0));
5571 }
5572 #[test]
5573 fn test_parse_ws_mark_price() {
5574 let data = json!({
5575 "e": "markPriceUpdate",
5576 "E": 1609459200000i64,
5577 "s": "BTCUSDT",
5578 "p": "50250.50",
5579 "i": "50000.00",
5580 "P": "50500.00",
5581 "r": "0.00010000",
5582 "T": 1609459300000i64
5583 });
5584
5585 let mark_price = parse_ws_mark_price(&data).unwrap();
5586 assert_eq!(mark_price.symbol, "BTCUSDT");
5587 assert_eq!(mark_price.mark_price, dec!(50250.50));
5588 assert_eq!(mark_price.index_price, Some(dec!(50000.00)));
5589 assert_eq!(mark_price.estimated_settle_price, Some(dec!(50500.00)));
5590 assert_eq!(mark_price.last_funding_rate, Some(dec!(0.0001)));
5591 assert_eq!(mark_price.next_funding_time, Some(1609459300000));
5592 assert_eq!(mark_price.timestamp, 1609459200000);
5593
5594 let basis = mark_price.basis();
5596 assert_eq!(basis, Some(dec!(250.50)));
5597
5598 let funding_rate_pct = mark_price.funding_rate_percent();
5599 assert_eq!(funding_rate_pct, Some(dec!(0.01)));
5600 }
5601}