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