1use ccxt_core::{
6 Result,
7 error::{Error, ParseError},
8 types::{
9 AccountConfig, Balance, BalanceEntry, BidAsk, BorrowInterest, BorrowRate,
10 BorrowRateHistory, CommissionRate, DepositWithdrawFee, FeeFundingRate,
11 FeeFundingRateHistory, FeeTradingFee, FundingFee, FundingHistory, IndexPrice,
12 LedgerDirection, LedgerEntry, LedgerEntryType, Leverage, LeverageTier, Liquidation,
13 MarginAdjustment, MarginLoan, MarginType, MarkPrice, Market, MaxBorrowable, MaxLeverage,
14 MaxTransferable, NextFundingRate, OHLCV, OcoOrder, OcoOrderInfo, OpenInterest,
15 OpenInterestHistory, Order, OrderBook, OrderBookEntry, OrderReport, OrderSide, OrderStatus,
16 OrderType, Position, PremiumIndex, TakerOrMaker, Ticker, TimeInForce, Trade, Transfer,
17 financial::{Amount, Cost, Price},
18 },
19};
20use rust_decimal::Decimal;
21use rust_decimal::prelude::{FromPrimitive, FromStr, ToPrimitive};
22use serde_json::Value;
23use std::collections::HashMap;
24
25fn parse_f64(data: &Value, key: &str) -> Option<f64> {
31 data.get(key).and_then(|v| {
32 v.as_f64()
33 .or_else(|| v.as_str().and_then(|s| s.parse::<f64>().ok()))
34 })
35}
36
37fn parse_decimal(data: &Value, key: &str) -> Option<Decimal> {
39 data.get(key).and_then(|v| {
40 if let Some(num) = v.as_f64() {
41 Decimal::from_f64(num)
42 } else if let Some(s) = v.as_str() {
43 Decimal::from_str(s).ok()
44 } else {
45 None
46 }
47 })
48}
49
50fn value_to_hashmap(data: &Value) -> HashMap<String, Value> {
52 data.as_object()
53 .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
54 .unwrap_or_default()
55}
56
57#[allow(dead_code)]
59fn parse_order_book_entries(data: &Value) -> Vec<OrderBookEntry> {
60 data.as_array()
61 .map(|arr| {
62 arr.iter()
63 .filter_map(|item| {
64 let price = if let Some(arr) = item.as_array() {
65 arr.get(0)
66 .and_then(|v| v.as_str())
67 .and_then(|s| Decimal::from_str(s).ok())
68 } else {
69 None
70 }?;
71
72 let amount = if let Some(arr) = item.as_array() {
73 arr.get(1)
74 .and_then(|v| v.as_str())
75 .and_then(|s| Decimal::from_str(s).ok())
76 } else {
77 None
78 }?;
79
80 Some(OrderBookEntry {
81 price: Price::new(price),
82 amount: Amount::new(amount),
83 })
84 })
85 .collect()
86 })
87 .unwrap_or_default()
88}
89
90pub fn parse_market(data: &Value) -> Result<Market> {
108 use ccxt_core::types::{MarketLimits, MarketPrecision, MarketType, MinMax};
109
110 let symbol = data["symbol"]
111 .as_str()
112 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
113 .to_string();
114
115 let base_asset = data["baseAsset"]
116 .as_str()
117 .ok_or_else(|| Error::from(ParseError::missing_field("baseAsset")))?
118 .to_string();
119
120 let quote_asset = data["quoteAsset"]
121 .as_str()
122 .ok_or_else(|| Error::from(ParseError::missing_field("quoteAsset")))?
123 .to_string();
124
125 let status = data["status"]
126 .as_str()
127 .ok_or_else(|| Error::from(ParseError::missing_field("status")))?;
128
129 let active = Some(status == "TRADING");
130
131 let margin = data["isMarginTradingAllowed"].as_bool().unwrap_or(false);
133
134 let mut price_precision: Option<Decimal> = None;
136 let mut amount_precision: Option<Decimal> = None;
137 let mut min_amount: Option<Decimal> = None;
138 let mut max_amount: Option<Decimal> = None;
139 let mut min_cost: Option<Decimal> = None;
140 let mut min_price: Option<Decimal> = None;
141 let mut max_price: Option<Decimal> = None;
142
143 if let Some(filters) = data["filters"].as_array() {
144 for filter in filters {
145 let filter_type = filter["filterType"].as_str().unwrap_or("");
146
147 match filter_type {
148 "PRICE_FILTER" => {
149 if let Some(tick_size) = filter["tickSize"].as_str() {
150 if let Ok(dec) = Decimal::from_str(tick_size) {
151 price_precision = Some(dec);
152 }
153 }
154 if let Some(min) = filter["minPrice"].as_str() {
155 min_price = Decimal::from_str(min).ok();
156 }
157 if let Some(max) = filter["maxPrice"].as_str() {
158 max_price = Decimal::from_str(max).ok();
159 }
160 }
161 "LOT_SIZE" => {
162 if let Some(step_size) = filter["stepSize"].as_str() {
163 if let Ok(dec) = Decimal::from_str(step_size) {
164 amount_precision = Some(dec);
165 }
166 }
167 if let Some(min) = filter["minQty"].as_str() {
168 min_amount = Decimal::from_str(min).ok();
169 }
170 if let Some(max) = filter["maxQty"].as_str() {
171 max_amount = Decimal::from_str(max).ok();
172 }
173 }
174 "MIN_NOTIONAL" | "NOTIONAL" => {
175 if let Some(min) = filter["minNotional"].as_str() {
176 min_cost = Decimal::from_str(min).ok();
177 }
178 }
179 _ => {}
180 }
181 }
182 }
183
184 let unified_symbol = format!("{}/{}", base_asset, quote_asset);
186 let parsed_symbol = ccxt_core::symbol::SymbolParser::parse(&unified_symbol).ok();
188
189 Ok(Market {
190 id: symbol.clone(),
191 symbol: unified_symbol,
192 parsed_symbol,
193 base: base_asset.clone(),
194 quote: quote_asset.clone(),
195 settle: None,
196 base_id: Some(base_asset),
197 quote_id: Some(quote_asset),
198 settle_id: None,
199 market_type: MarketType::Spot,
200 active: active.unwrap_or(true),
201 margin,
202 contract: Some(false),
203 linear: None,
204 inverse: None,
205 taker: Decimal::from_str("0.001").ok(),
207 maker: Decimal::from_str("0.001").ok(),
208 contract_size: None,
209 expiry: None,
210 expiry_datetime: None,
211 strike: None,
212 option_type: None,
213 percentage: Some(true),
214 tier_based: Some(false),
215 fee_side: Some("quote".to_string()),
216 precision: MarketPrecision {
217 price: price_precision,
218 amount: amount_precision,
219 base: None,
220 quote: None,
221 },
222 limits: MarketLimits {
223 amount: Some(MinMax {
224 min: min_amount,
225 max: max_amount,
226 }),
227 price: Some(MinMax {
228 min: min_price,
229 max: max_price,
230 }),
231 cost: Some(MinMax {
232 min: min_cost,
233 max: None,
234 }),
235 leverage: None,
236 },
237 info: value_to_hashmap(data),
238 })
239}
240
241pub fn parse_ticker(data: &Value, market: Option<&Market>) -> Result<Ticker> {
256 let symbol = if let Some(m) = market {
257 m.symbol.clone()
258 } else {
259 data["symbol"]
260 .as_str()
261 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
262 .to_string()
263 };
264
265 let timestamp = data["closeTime"].as_i64();
266
267 Ok(Ticker {
268 symbol,
269 timestamp: timestamp.unwrap_or(0),
270 datetime: timestamp.map(|t| {
271 chrono::DateTime::from_timestamp(t / 1000, 0)
272 .map(|dt| dt.to_rfc3339())
273 .unwrap_or_default()
274 }),
275 high: parse_decimal(data, "highPrice").map(Price::new),
276 low: parse_decimal(data, "lowPrice").map(Price::new),
277 bid: parse_decimal(data, "bidPrice").map(Price::new),
278 bid_volume: parse_decimal(data, "bidQty").map(Amount::new),
279 ask: parse_decimal(data, "askPrice").map(Price::new),
280 ask_volume: parse_decimal(data, "askQty").map(Amount::new),
281 vwap: parse_decimal(data, "weightedAvgPrice").map(Price::new),
282 open: parse_decimal(data, "openPrice").map(Price::new),
283 close: parse_decimal(data, "lastPrice").map(Price::new),
284 last: parse_decimal(data, "lastPrice").map(Price::new),
285 previous_close: parse_decimal(data, "prevClosePrice").map(Price::new),
286 change: parse_decimal(data, "priceChange").map(Price::new),
287 percentage: parse_decimal(data, "priceChangePercent"),
288 average: None,
289 base_volume: parse_decimal(data, "volume").map(Amount::new),
290 quote_volume: parse_decimal(data, "quoteVolume").map(Amount::new),
291 info: value_to_hashmap(data),
292 })
293}
294
295pub fn parse_trade(data: &Value, market: Option<&Market>) -> Result<Trade> {
310 let symbol = if let Some(m) = market {
311 m.symbol.clone()
312 } else {
313 data["symbol"]
314 .as_str()
315 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
316 .to_string()
317 };
318
319 let id = data["id"]
320 .as_u64()
321 .or_else(|| data["a"].as_u64())
322 .map(|i| i.to_string());
323
324 let timestamp = data["time"].as_i64().or_else(|| data["T"].as_i64());
325
326 let side = if data["isBuyerMaker"].as_bool().unwrap_or(false) {
327 OrderSide::Sell
328 } else if data["m"].as_bool().unwrap_or(false) {
329 OrderSide::Sell
330 } else {
331 OrderSide::Buy
332 };
333
334 let price = parse_f64(data, "price")
335 .or_else(|| parse_f64(data, "p"))
336 .and_then(Decimal::from_f64_retain);
337 let amount = parse_f64(data, "qty")
338 .or_else(|| parse_f64(data, "q"))
339 .and_then(Decimal::from_f64_retain);
340
341 let cost = match (price, amount) {
342 (Some(p), Some(a)) => Some(p * a),
343 _ => None,
344 };
345
346 Ok(Trade {
347 id,
348 order: data["orderId"]
349 .as_u64()
350 .or_else(|| data["orderid"].as_u64())
351 .map(|i| i.to_string()),
352 timestamp: timestamp.unwrap_or(0),
353 datetime: timestamp.map(|t| {
354 chrono::DateTime::from_timestamp(t / 1000, 0)
355 .map(|dt| dt.to_rfc3339())
356 .unwrap_or_default()
357 }),
358 symbol,
359 trade_type: None,
360 side,
361 taker_or_maker: if data["isBuyerMaker"].as_bool().unwrap_or(false) {
362 Some(TakerOrMaker::Maker)
363 } else {
364 Some(TakerOrMaker::Taker)
365 },
366 price: Price::new(price.unwrap_or(Decimal::ZERO)),
367 amount: Amount::new(amount.unwrap_or(Decimal::ZERO)),
368 cost: cost.map(Cost::new),
369 fee: None,
370 info: value_to_hashmap(data),
371 })
372}
373
374pub fn parse_order(data: &Value, market: Option<&Market>) -> Result<Order> {
389 let symbol = if let Some(m) = market {
390 m.symbol.clone()
391 } else {
392 data["symbol"]
393 .as_str()
394 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
395 .to_string()
396 };
397
398 let id = data["orderId"]
399 .as_u64()
400 .ok_or_else(|| Error::from(ParseError::missing_field("orderId")))?
401 .to_string();
402
403 let timestamp = data["time"]
404 .as_i64()
405 .or_else(|| data["transactTime"].as_i64());
406
407 let status_str = data["status"]
408 .as_str()
409 .ok_or_else(|| Error::from(ParseError::missing_field("status")))?;
410
411 let status = match status_str {
412 "NEW" | "PARTIALLY_FILLED" => OrderStatus::Open,
413 "FILLED" => OrderStatus::Closed,
414 "CANCELED" => OrderStatus::Canceled,
415 "EXPIRED" => OrderStatus::Expired,
416 "REJECTED" => OrderStatus::Rejected,
417 _ => OrderStatus::Open,
418 };
419
420 let side = match data["side"].as_str() {
421 Some("BUY") => OrderSide::Buy,
422 Some("SELL") => OrderSide::Sell,
423 _ => return Err(Error::from(ParseError::invalid_format("data", "side"))),
424 };
425
426 let order_type = match data["type"].as_str() {
427 Some("MARKET") => OrderType::Market,
428 Some("LIMIT") => OrderType::Limit,
429 Some("STOP_LOSS") => OrderType::StopLoss,
430 Some("STOP_LOSS_LIMIT") => OrderType::StopLossLimit,
431 Some("TAKE_PROFIT") => OrderType::TakeProfit,
432 Some("TAKE_PROFIT_LIMIT") => OrderType::TakeProfitLimit,
433 Some("STOP_MARKET") | Some("STOP") => OrderType::StopMarket,
434 Some("TAKE_PROFIT_MARKET") => OrderType::TakeProfitLimit,
435 Some("TRAILING_STOP_MARKET") => OrderType::TrailingStop,
436 Some("LIMIT_MAKER") => OrderType::LimitMaker,
437 _ => OrderType::Limit,
438 };
439
440 let time_in_force = match data["timeInForce"].as_str() {
441 Some("GTC") => Some(TimeInForce::GTC),
442 Some("IOC") => Some(TimeInForce::IOC),
443 Some("FOK") => Some(TimeInForce::FOK),
444 Some("GTX") => Some(TimeInForce::PO),
445 _ => None,
446 };
447
448 let price = parse_f64(data, "price");
449 let amount = parse_f64(data, "origQty");
450 let filled = parse_f64(data, "executedQty");
451 let remaining = match (amount, filled) {
452 (Some(a), Some(f)) => Some(a - f),
453 _ => None,
454 };
455
456 let cost = parse_f64(data, "cummulativeQuoteQty");
457
458 let average = match (cost, filled) {
459 (Some(c), Some(f)) if f > 0.0 => Some(c / f),
460 _ => None,
461 };
462
463 Ok(Order {
464 id,
465 client_order_id: data["clientOrderId"].as_str().map(|s| s.to_string()),
466 timestamp,
467 datetime: timestamp.map(|t| {
468 chrono::DateTime::from_timestamp(t / 1000, 0)
469 .map(|dt| dt.to_rfc3339())
470 .unwrap_or_default()
471 }),
472 last_trade_timestamp: data["updateTime"].as_i64(),
473 status,
474 symbol,
475 order_type,
476 time_in_force: time_in_force.map(|t| t.to_string()),
477 side,
478 price: price.map(Decimal::from_f64_retain).flatten(),
479 average: average.map(Decimal::from_f64_retain).flatten(),
480 amount: amount
481 .map(Decimal::from_f64_retain)
482 .flatten()
483 .ok_or_else(|| Error::from(ParseError::missing_field("amount")))?,
484 filled: filled.map(Decimal::from_f64_retain).flatten(),
485 remaining: remaining.map(Decimal::from_f64_retain).flatten(),
486 cost: cost.map(Decimal::from_f64_retain).flatten(),
487 trades: None,
488 fee: None,
489 post_only: None,
490 reduce_only: data["reduceOnly"].as_bool(),
491 trigger_price: parse_f64(data, "triggerPrice").and_then(Decimal::from_f64_retain),
492 stop_price: parse_f64(data, "stopPrice").and_then(Decimal::from_f64_retain),
493 take_profit_price: parse_f64(data, "takeProfitPrice").and_then(Decimal::from_f64_retain),
494 stop_loss_price: parse_f64(data, "stopLossPrice").and_then(Decimal::from_f64_retain),
495 trailing_delta: parse_f64(data, "trailingDelta").and_then(Decimal::from_f64_retain),
496 trailing_percent: parse_f64(data, "trailingPercent")
497 .or_else(|| parse_f64(data, "callbackRate"))
498 .and_then(Decimal::from_f64_retain),
499 activation_price: parse_f64(data, "activationPrice")
500 .or_else(|| parse_f64(data, "activatePrice"))
501 .and_then(Decimal::from_f64_retain),
502 callback_rate: parse_f64(data, "callbackRate").and_then(Decimal::from_f64_retain),
503 working_type: data["workingType"].as_str().map(|s| s.to_string()),
504 fees: Some(Vec::new()),
505 info: value_to_hashmap(data),
506 })
507}
508pub fn parse_oco_order(data: &Value) -> Result<OcoOrder> {
522 let order_list_id = data["orderListId"]
523 .as_i64()
524 .ok_or_else(|| Error::from(ParseError::missing_field("orderListId")))?;
525
526 let list_client_order_id = data["listClientOrderId"].as_str().map(|s| s.to_string());
527
528 let symbol = data["symbol"]
529 .as_str()
530 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
531 .to_string();
532
533 let list_status = data["listStatusType"]
534 .as_str()
535 .ok_or_else(|| Error::from(ParseError::missing_field("listStatusType")))?
536 .to_string();
537
538 let list_order_status = data["listOrderStatus"]
539 .as_str()
540 .ok_or_else(|| Error::from(ParseError::missing_field("listOrderStatus")))?
541 .to_string();
542
543 let transaction_time = data["transactionTime"]
544 .as_u64()
545 .ok_or_else(|| Error::from(ParseError::missing_field("transactionTime")))?;
546
547 let datetime = chrono::DateTime::from_timestamp((transaction_time / 1000) as i64, 0)
548 .map(|dt| dt.to_rfc3339())
549 .unwrap_or_default();
550
551 let mut orders = Vec::new();
552 if let Some(orders_array) = data["orders"].as_array() {
553 for order in orders_array {
554 let order_info = OcoOrderInfo {
555 symbol: order["symbol"].as_str().unwrap_or(&symbol).to_string(),
556 order_id: order["orderId"]
557 .as_i64()
558 .ok_or_else(|| Error::from(ParseError::missing_field("orderId")))?,
559 client_order_id: order["clientOrderId"].as_str().map(|s| s.to_string()),
560 };
561 orders.push(order_info);
562 }
563 }
564
565 let order_reports = if let Some(reports_array) = data["orderReports"].as_array() {
566 let mut reports = Vec::new();
567 for report in reports_array {
568 let order_report = OrderReport {
569 symbol: report["symbol"].as_str().unwrap_or(&symbol).to_string(),
570 order_id: report["orderId"]
571 .as_i64()
572 .ok_or_else(|| Error::from(ParseError::missing_field("orderId")))?,
573 order_list_id: report["orderListId"].as_i64().unwrap_or(order_list_id),
574 client_order_id: report["clientOrderId"].as_str().map(|s| s.to_string()),
575 transact_time: report["transactTime"].as_u64().unwrap_or(transaction_time),
576 price: report["price"].as_str().unwrap_or("0").to_string(),
577 orig_qty: report["origQty"].as_str().unwrap_or("0").to_string(),
578 executed_qty: report["executedQty"].as_str().unwrap_or("0").to_string(),
579 cummulative_quote_qty: report["cummulativeQuoteQty"]
580 .as_str()
581 .unwrap_or("0")
582 .to_string(),
583 status: report["status"].as_str().unwrap_or("NEW").to_string(),
584 time_in_force: report["timeInForce"].as_str().unwrap_or("GTC").to_string(),
585 type_: report["type"].as_str().unwrap_or("LIMIT").to_string(),
586 side: report["side"].as_str().unwrap_or("SELL").to_string(),
587 stop_price: report["stopPrice"].as_str().map(|s| s.to_string()),
588 };
589 reports.push(order_report);
590 }
591 Some(reports)
592 } else {
593 None
594 };
595
596 Ok(OcoOrder {
597 info: Some(data.clone()),
598 order_list_id,
599 list_client_order_id,
600 symbol,
601 list_status,
602 list_order_status,
603 transaction_time,
604 datetime,
605 orders,
606 order_reports,
607 })
608}
609
610pub fn parse_balance(data: &Value) -> Result<Balance> {
620 let mut balances = HashMap::new();
621
622 if let Some(balances_array) = data["balances"].as_array() {
623 for balance in balances_array {
624 let currency = balance["asset"]
625 .as_str()
626 .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
627 .to_string();
628
629 let free = parse_f64(balance, "free").unwrap_or(0.0);
630 let locked = parse_f64(balance, "locked").unwrap_or(0.0);
631 let total = free + locked;
632
633 if total > 0.0 {
634 balances.insert(
635 currency,
636 BalanceEntry {
637 free: Decimal::from_f64_retain(free).unwrap_or(Decimal::ZERO),
638 used: Decimal::from_f64_retain(locked).unwrap_or(Decimal::ZERO),
639 total: Decimal::from_f64_retain(total).unwrap_or(Decimal::ZERO),
640 },
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_funding_rate(data: &Value, market: Option<&Market>) -> Result<FeeFundingRate> {
740 let symbol = if let Some(m) = market {
741 m.symbol.clone()
742 } else {
743 data["symbol"]
744 .as_str()
745 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
746 .to_string()
747 };
748
749 let funding_rate =
750 parse_decimal(data, "lastFundingRate").or_else(|| parse_decimal(data, "fundingRate"));
751
752 let mark_price = parse_decimal(data, "markPrice");
753 let index_price = parse_decimal(data, "indexPrice");
754 let interest_rate = parse_decimal(data, "interestRate");
755
756 let next_funding_time = data["nextFundingTime"].as_i64();
757 let funding_timestamp = next_funding_time.or_else(|| data["fundingTime"].as_i64());
758
759 Ok(FeeFundingRate {
760 info: data.clone(),
761 symbol,
762 mark_price,
763 index_price,
764 interest_rate,
765 estimated_settle_price: None,
766 funding_rate,
767 funding_timestamp,
768 funding_datetime: funding_timestamp.map(|t| {
769 chrono::DateTime::from_timestamp(t / 1000, 0)
770 .map(|dt| dt.to_rfc3339())
771 .unwrap_or_default()
772 }),
773 next_funding_rate: None,
774 next_funding_timestamp: None,
775 next_funding_datetime: None,
776 previous_funding_rate: None,
777 previous_funding_timestamp: None,
778 previous_funding_datetime: None,
779 timestamp: None,
780 datetime: None,
781 interval: None,
782 })
783}
784
785pub fn parse_funding_rate_history(
796 data: &Value,
797 market: Option<&Market>,
798) -> Result<FeeFundingRateHistory> {
799 let symbol = if let Some(m) = market {
800 m.symbol.clone()
801 } else {
802 data["symbol"]
803 .as_str()
804 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
805 .to_string()
806 };
807
808 let funding_rate = parse_decimal(data, "fundingRate");
809 let funding_time = data["fundingTime"].as_i64();
810
811 Ok(FeeFundingRateHistory {
812 info: data.clone(),
813 symbol,
814 funding_rate,
815 funding_timestamp: funding_time,
816 funding_datetime: funding_time.map(|t| {
817 chrono::DateTime::from_timestamp(t / 1000, 0)
818 .map(|dt| dt.to_rfc3339())
819 .unwrap_or_default()
820 }),
821 timestamp: funding_time,
822 datetime: funding_time.map(|t| {
823 chrono::DateTime::from_timestamp(t / 1000, 0)
824 .map(|dt| dt.to_rfc3339())
825 .unwrap_or_default()
826 }),
827 })
828}
829
830pub fn parse_position(data: &Value, market: Option<&Market>) -> Result<Position> {
841 let symbol = if let Some(m) = market {
842 m.symbol.clone()
843 } else {
844 data["symbol"]
845 .as_str()
846 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
847 .to_string()
848 };
849
850 let position_side = data["positionSide"].as_str().unwrap_or("BOTH");
851
852 let side = match position_side {
853 "LONG" => Some("long".to_string()),
854 "SHORT" => Some("short".to_string()),
855 "BOTH" => {
856 let position_amt = parse_f64(data, "positionAmt").unwrap_or(0.0);
858 if position_amt > 0.0 {
859 Some("long".to_string())
860 } else if position_amt < 0.0 {
861 Some("short".to_string())
862 } else {
863 None
864 }
865 }
866 _ => None,
867 };
868
869 let contracts = parse_f64(data, "positionAmt").map(|v| v.abs());
870
871 let contract_size = Some(1.0); let entry_price = parse_f64(data, "entryPrice");
874 let mark_price = parse_f64(data, "markPrice");
875 let notional = parse_f64(data, "notional").map(|v| v.abs());
876
877 let leverage = parse_f64(data, "leverage");
878
879 let collateral =
880 parse_f64(data, "isolatedWallet").or_else(|| parse_f64(data, "positionInitialMargin"));
881
882 let initial_margin =
883 parse_f64(data, "initialMargin").or_else(|| parse_f64(data, "positionInitialMargin"));
884
885 let maintenance_margin =
886 parse_f64(data, "maintMargin").or_else(|| parse_f64(data, "positionMaintMargin"));
887
888 let unrealized_pnl =
889 parse_f64(data, "unrealizedProfit").or_else(|| parse_f64(data, "unRealizedProfit"));
890
891 let liquidation_price = parse_f64(data, "liquidationPrice");
892
893 let margin_ratio = parse_f64(data, "marginRatio");
894
895 let margin_mode = data["marginType"]
896 .as_str()
897 .or_else(|| data["marginMode"].as_str())
898 .map(|s| s.to_lowercase());
899
900 let hedged = position_side != "BOTH";
901
902 let percentage = match (unrealized_pnl, collateral) {
903 (Some(pnl), Some(col)) if col > 0.0 => Some((pnl / col) * 100.0),
904 _ => None,
905 };
906
907 let initial_margin_percentage = parse_f64(data, "initialMarginPercentage");
908 let maintenance_margin_percentage = parse_f64(data, "maintMarginPercentage");
909
910 let update_time = data["updateTime"].as_i64();
911
912 Ok(Position {
913 info: data.clone(),
914 id: None,
915 symbol,
916 side,
917 contracts,
918 contract_size,
919 entry_price,
920 mark_price,
921 notional,
922 leverage,
923 collateral,
924 initial_margin,
925 initial_margin_percentage,
926 maintenance_margin,
927 maintenance_margin_percentage,
928 unrealized_pnl,
929 realized_pnl: None, liquidation_price,
931 margin_ratio,
932 margin_mode,
933 hedged: Some(hedged),
934 percentage,
935 position_side: None,
936 dual_side_position: None,
937 timestamp: update_time.map(|t| t as u64),
938 datetime: update_time.map(|t| {
939 chrono::DateTime::from_timestamp(t / 1000, 0)
940 .map(|dt| dt.to_rfc3339())
941 .unwrap_or_default()
942 }),
943 })
944}
945pub fn parse_leverage(data: &Value, _market: Option<&Market>) -> Result<Leverage> {
956 let market_id = data.get("symbol").and_then(|v| v.as_str()).unwrap_or("");
957
958 let margin_mode = if let Some(isolated) = data.get("isolated").and_then(|v| v.as_bool()) {
959 Some(if isolated {
960 MarginType::Isolated
961 } else {
962 MarginType::Cross
963 })
964 } else if let Some(margin_type) = data.get("marginType").and_then(|v| v.as_str()) {
965 Some(if margin_type == "crossed" {
966 MarginType::Cross
967 } else {
968 MarginType::Isolated
969 })
970 } else {
971 None
972 };
973
974 let side = data
975 .get("positionSide")
976 .and_then(|v| v.as_str())
977 .map(|s| s.to_lowercase());
978
979 let leverage_value = data.get("leverage").and_then(|v| v.as_i64());
980
981 let (long_leverage, short_leverage) = match side.as_deref() {
983 None | Some("both") => (leverage_value, leverage_value),
984 Some("long") => (leverage_value, None),
985 Some("short") => (None, leverage_value),
986 _ => (None, None),
987 };
988
989 Ok(Leverage {
990 info: data.clone(),
991 symbol: market_id.to_string(),
992 margin_mode,
993 long_leverage,
994 short_leverage,
995 timestamp: None,
996 datetime: None,
997 })
998}
999
1000pub fn parse_funding_history(data: &Value, market: Option<&Market>) -> Result<FundingHistory> {
1011 let symbol = if let Some(m) = market {
1012 m.symbol.clone()
1013 } else {
1014 data["symbol"]
1015 .as_str()
1016 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
1017 .to_string()
1018 };
1019
1020 let id = data["tranId"]
1021 .as_u64()
1022 .or_else(|| data["id"].as_u64())
1023 .map(|i| i.to_string());
1024
1025 let amount = parse_f64(data, "income");
1026 let code = data["asset"].as_str().map(|s| s.to_string());
1027 let timestamp = data["time"].as_i64();
1028
1029 Ok(FundingHistory {
1030 info: data.clone(),
1031 id,
1032 symbol,
1033 code,
1034 amount,
1035 timestamp: timestamp.map(|t| t as u64),
1036 datetime: timestamp.map(|t| {
1037 chrono::DateTime::from_timestamp((t / 1000) as i64, 0)
1038 .map(|dt| dt.to_rfc3339())
1039 .unwrap_or_default()
1040 }),
1041 })
1042}
1043
1044pub fn parse_funding_fee(data: &Value, market: Option<&Market>) -> Result<FundingFee> {
1055 let symbol = if let Some(m) = market {
1056 m.symbol.clone()
1057 } else {
1058 data["symbol"]
1059 .as_str()
1060 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
1061 .to_string()
1062 };
1063
1064 let income = parse_f64(data, "income").unwrap_or(0.0);
1065 let asset = data["asset"]
1066 .as_str()
1067 .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
1068 .to_string();
1069
1070 let time = data["time"]
1071 .as_u64()
1072 .ok_or_else(|| Error::from(ParseError::missing_field("time")))?;
1073
1074 let funding_rate = parse_f64(data, "fundingRate");
1075 let mark_price = parse_f64(data, "markPrice");
1076
1077 let datetime = Some(
1078 chrono::DateTime::from_timestamp((time / 1000) as i64, 0)
1079 .map(|dt| dt.to_rfc3339())
1080 .unwrap_or_default(),
1081 );
1082
1083 Ok(FundingFee {
1084 info: data.clone(),
1085 symbol,
1086 income,
1087 asset,
1088 time,
1089 datetime,
1090 funding_rate,
1091 mark_price,
1092 })
1093}
1094
1095pub fn parse_next_funding_rate(data: &Value, market: &Market) -> Result<NextFundingRate> {
1106 let symbol = market.symbol.clone();
1107
1108 let mark_price = parse_f64(data, "markPrice")
1109 .ok_or_else(|| Error::from(ParseError::missing_field("markPrice")))?;
1110
1111 let index_price = parse_f64(data, "indexPrice");
1112
1113 let current_funding_rate = parse_f64(data, "lastFundingRate").unwrap_or(0.0);
1114
1115 let next_funding_rate = parse_f64(data, "interestRate")
1116 .or_else(|| parse_f64(data, "estimatedSettlePrice"))
1117 .unwrap_or(current_funding_rate);
1118
1119 let next_funding_time = data["nextFundingTime"]
1120 .as_u64()
1121 .ok_or_else(|| Error::from(ParseError::missing_field("nextFundingTime")))?;
1122
1123 let next_funding_datetime = Some(
1124 chrono::DateTime::from_timestamp((next_funding_time / 1000) as i64, 0)
1125 .map(|dt| dt.to_rfc3339())
1126 .unwrap_or_default(),
1127 );
1128
1129 Ok(NextFundingRate {
1130 info: data.clone(),
1131 symbol,
1132 mark_price,
1133 index_price,
1134 current_funding_rate,
1135 next_funding_rate,
1136 next_funding_time,
1137 next_funding_datetime,
1138 })
1139}
1140pub fn parse_account_config(data: &Value) -> Result<AccountConfig> {
1154 let multi_assets_margin = data["multiAssetsMargin"].as_bool().unwrap_or(false);
1155
1156 let fee_tier = data["feeTier"].as_i64().unwrap_or(0) as i32;
1157
1158 let can_trade = data["canTrade"].as_bool().unwrap_or(true);
1159
1160 let can_deposit = data["canDeposit"].as_bool().unwrap_or(true);
1161
1162 let can_withdraw = data["canWithdraw"].as_bool().unwrap_or(true);
1163
1164 let update_time = data["updateTime"].as_u64().unwrap_or(0);
1165
1166 Ok(AccountConfig {
1167 info: Some(data.clone()),
1168 multi_assets_margin,
1169 fee_tier,
1170 can_trade,
1171 can_deposit,
1172 can_withdraw,
1173 update_time,
1174 })
1175}
1176
1177pub fn parse_commission_rate(data: &Value, market: &Market) -> Result<CommissionRate> {
1188 let maker_commission_rate = data["makerCommissionRate"]
1189 .as_str()
1190 .and_then(|s| s.parse::<f64>().ok())
1191 .unwrap_or(0.0);
1192
1193 let taker_commission_rate = data["takerCommissionRate"]
1194 .as_str()
1195 .and_then(|s| s.parse::<f64>().ok())
1196 .unwrap_or(0.0);
1197
1198 Ok(CommissionRate {
1199 info: Some(data.clone()),
1200 symbol: market.symbol.clone(),
1201 maker_commission_rate,
1202 taker_commission_rate,
1203 })
1204}
1205
1206pub fn parse_open_interest(data: &Value, market: &Market) -> Result<OpenInterest> {
1231 let open_interest = data["openInterest"]
1232 .as_str()
1233 .and_then(|s| s.parse::<f64>().ok())
1234 .or_else(|| data["openInterest"].as_f64())
1235 .unwrap_or(0.0);
1236
1237 let timestamp = data["time"]
1238 .as_u64()
1239 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis() as u64);
1240
1241 let contract_size = market
1242 .contract_size
1243 .unwrap_or_else(|| rust_decimal::Decimal::from(1))
1244 .to_f64()
1245 .unwrap_or(1.0);
1246 let open_interest_value = open_interest * contract_size;
1247
1248 Ok(OpenInterest {
1249 info: Some(data.clone()),
1250 symbol: market.symbol.clone(),
1251 open_interest,
1252 open_interest_value,
1253 timestamp,
1254 })
1255}
1256
1257pub fn parse_open_interest_history(
1281 data: &Value,
1282 market: &Market,
1283) -> Result<Vec<OpenInterestHistory>> {
1284 let array = data.as_array().ok_or_else(|| {
1285 Error::from(ParseError::invalid_value(
1286 "data",
1287 "Expected array for open interest history",
1288 ))
1289 })?;
1290
1291 let mut result = Vec::new();
1292
1293 for item in array {
1294 let sum_open_interest = item["sumOpenInterest"]
1295 .as_str()
1296 .and_then(|s| s.parse::<f64>().ok())
1297 .or_else(|| item["sumOpenInterest"].as_f64())
1298 .unwrap_or(0.0);
1299
1300 let sum_open_interest_value = item["sumOpenInterestValue"]
1301 .as_str()
1302 .and_then(|s| s.parse::<f64>().ok())
1303 .or_else(|| item["sumOpenInterestValue"].as_f64())
1304 .unwrap_or(0.0);
1305
1306 let timestamp = item["timestamp"]
1307 .as_u64()
1308 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis() as u64);
1309
1310 result.push(OpenInterestHistory {
1311 info: Some(item.clone()),
1312 symbol: market.symbol.clone(),
1313 sum_open_interest,
1314 sum_open_interest_value,
1315 timestamp,
1316 });
1317 }
1318
1319 Ok(result)
1320}
1321
1322pub fn parse_max_leverage(data: &Value, market: &Market) -> Result<MaxLeverage> {
1352 let target_data = if let Some(array) = data.as_array() {
1353 array
1354 .iter()
1355 .find(|item| item["symbol"].as_str().unwrap_or("") == market.id)
1356 .ok_or_else(|| {
1357 Error::from(ParseError::invalid_value(
1358 "symbol",
1359 format!("Symbol {} not found in leverage brackets", market.id),
1360 ))
1361 })?
1362 } else {
1363 data
1364 };
1365
1366 let brackets = target_data["brackets"]
1367 .as_array()
1368 .ok_or_else(|| Error::from(ParseError::missing_field("brackets")))?;
1369
1370 if brackets.is_empty() {
1371 return Err(Error::from(ParseError::invalid_value(
1372 "data",
1373 "Empty brackets array",
1374 )));
1375 }
1376
1377 let first_bracket = &brackets[0];
1378 let max_leverage = first_bracket["initialLeverage"].as_i64().unwrap_or(1) as i32;
1379
1380 let notional = first_bracket["notionalCap"]
1381 .as_f64()
1382 .or_else(|| {
1383 first_bracket["notionalCap"]
1384 .as_str()
1385 .and_then(|s| s.parse::<f64>().ok())
1386 })
1387 .unwrap_or(0.0);
1388
1389 Ok(MaxLeverage {
1390 info: Some(data.clone()),
1391 symbol: market.symbol.clone(),
1392 max_leverage,
1393 notional,
1394 })
1395}
1396
1397pub fn parse_index_price(data: &Value, market: &Market) -> Result<IndexPrice> {
1412 let index_price = data["indexPrice"]
1413 .as_f64()
1414 .or_else(|| {
1415 data["indexPrice"]
1416 .as_str()
1417 .and_then(|s| s.parse::<f64>().ok())
1418 })
1419 .ok_or_else(|| Error::from(ParseError::missing_field("indexPrice")))?;
1420
1421 let timestamp = data["time"]
1422 .as_u64()
1423 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis() as u64);
1424
1425 Ok(IndexPrice {
1426 info: Some(data.clone()),
1427 symbol: market.symbol.clone(),
1428 index_price,
1429 timestamp,
1430 })
1431}
1432
1433pub fn parse_premium_index(data: &Value, market: &Market) -> Result<PremiumIndex> {
1444 let mark_price = data["markPrice"]
1445 .as_f64()
1446 .or_else(|| {
1447 data["markPrice"]
1448 .as_str()
1449 .and_then(|s| s.parse::<f64>().ok())
1450 })
1451 .ok_or_else(|| Error::from(ParseError::missing_field("markPrice")))?;
1452
1453 let index_price = data["indexPrice"]
1454 .as_f64()
1455 .or_else(|| {
1456 data["indexPrice"]
1457 .as_str()
1458 .and_then(|s| s.parse::<f64>().ok())
1459 })
1460 .ok_or_else(|| Error::from(ParseError::missing_field("indexPrice")))?;
1461
1462 let estimated_settle_price = data["estimatedSettlePrice"]
1463 .as_f64()
1464 .or_else(|| {
1465 data["estimatedSettlePrice"]
1466 .as_str()
1467 .and_then(|s| s.parse::<f64>().ok())
1468 })
1469 .unwrap_or(0.0);
1470
1471 let last_funding_rate = data["lastFundingRate"]
1472 .as_f64()
1473 .or_else(|| {
1474 data["lastFundingRate"]
1475 .as_str()
1476 .and_then(|s| s.parse::<f64>().ok())
1477 })
1478 .unwrap_or(0.0);
1479
1480 let next_funding_time = data["nextFundingTime"].as_u64().unwrap_or(0);
1482
1483 let time = data["time"]
1485 .as_u64()
1486 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis() as u64);
1487
1488 Ok(PremiumIndex {
1489 info: Some(data.clone()),
1490 symbol: market.symbol.clone(),
1491 mark_price,
1492 index_price,
1493 estimated_settle_price,
1494 last_funding_rate,
1495 next_funding_time,
1496 time,
1497 })
1498}
1499
1500pub fn parse_liquidation(data: &Value, market: &Market) -> Result<Liquidation> {
1511 let side = data["side"]
1512 .as_str()
1513 .ok_or_else(|| Error::from(ParseError::missing_field("side")))?
1514 .to_string();
1515
1516 let order_type = data["type"].as_str().unwrap_or("LIMIT").to_string();
1517
1518 let time = data["time"]
1519 .as_u64()
1520 .ok_or_else(|| Error::from(ParseError::missing_field("time")))?;
1521
1522 let price = data["price"]
1523 .as_f64()
1524 .or_else(|| data["price"].as_str().and_then(|s| s.parse::<f64>().ok()))
1525 .ok_or_else(|| Error::from(ParseError::missing_field("price")))?;
1526
1527 let quantity = data["origQty"]
1528 .as_f64()
1529 .or_else(|| data["origQty"].as_str().and_then(|s| s.parse::<f64>().ok()))
1530 .ok_or_else(|| Error::from(ParseError::missing_field("origQty")))?;
1531
1532 let average_price = data["averagePrice"]
1533 .as_f64()
1534 .or_else(|| {
1535 data["averagePrice"]
1536 .as_str()
1537 .and_then(|s| s.parse::<f64>().ok())
1538 })
1539 .unwrap_or(price);
1540
1541 Ok(Liquidation {
1542 info: Some(data.clone()),
1543 symbol: market.symbol.clone(),
1544 side,
1545 order_type,
1546 time,
1547 price,
1548 quantity,
1549 average_price,
1550 })
1551}
1552
1553pub fn parse_borrow_rate(data: &Value, currency: &str, symbol: Option<&str>) -> Result<BorrowRate> {
1569 let rate = if let Some(rate_str) = data["dailyInterestRate"].as_str() {
1570 rate_str.parse::<f64>().unwrap_or(0.0)
1571 } else {
1572 data["dailyInterestRate"].as_f64().unwrap_or(0.0)
1573 };
1574
1575 let timestamp = data["timestamp"]
1576 .as_i64()
1577 .or_else(|| {
1578 data["vipLevel"]
1579 .as_i64()
1580 .map(|_| chrono::Utc::now().timestamp_millis())
1581 })
1582 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1583
1584 if let Some(sym) = symbol {
1585 Ok(BorrowRate::new_isolated(
1586 currency.to_string(),
1587 sym.to_string(),
1588 rate,
1589 timestamp,
1590 data.clone(),
1591 ))
1592 } else {
1593 Ok(BorrowRate::new_cross(
1594 currency.to_string(),
1595 rate,
1596 timestamp,
1597 data.clone(),
1598 ))
1599 }
1600}
1601
1602pub fn parse_margin_loan(data: &Value) -> Result<MarginLoan> {
1612 let id = data["tranId"]
1613 .as_i64()
1614 .or_else(|| data["txId"].as_i64())
1615 .map(|id| id.to_string())
1616 .unwrap_or_default();
1617
1618 let currency = data["asset"]
1619 .as_str()
1620 .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
1621 .to_string();
1622
1623 let symbol = data["symbol"].as_str().map(|s| s.to_string());
1624
1625 let amount = if let Some(amount_str) = data["amount"].as_str() {
1626 amount_str.parse::<f64>().unwrap_or(0.0)
1627 } else {
1628 data["amount"].as_f64().unwrap_or(0.0)
1629 };
1630
1631 let timestamp = data["timestamp"]
1632 .as_i64()
1633 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1634
1635 let status = data["status"].as_str().unwrap_or("CONFIRMED").to_string();
1636
1637 Ok(MarginLoan::new(
1638 id,
1639 currency,
1640 symbol,
1641 amount,
1642 timestamp,
1643 status,
1644 data.clone(),
1645 ))
1646}
1647
1648pub fn parse_borrow_interest(data: &Value) -> Result<BorrowInterest> {
1658 let id = data["txId"]
1659 .as_i64()
1660 .or_else(|| {
1661 data["isolatedSymbol"]
1662 .as_str()
1663 .map(|_| chrono::Utc::now().timestamp_millis())
1664 })
1665 .map(|id| id.to_string())
1666 .unwrap_or_default();
1667
1668 let currency = data["asset"]
1669 .as_str()
1670 .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
1671 .to_string();
1672
1673 let symbol = data["isolatedSymbol"].as_str().map(|s| s.to_string());
1674
1675 let interest = if let Some(interest_str) = data["interest"].as_str() {
1676 interest_str.parse::<f64>().unwrap_or(0.0)
1677 } else {
1678 data["interest"].as_f64().unwrap_or(0.0)
1679 };
1680
1681 let interest_rate = if let Some(rate_str) = data["interestRate"].as_str() {
1682 rate_str.parse::<f64>().unwrap_or(0.0)
1683 } else {
1684 data["interestRate"].as_f64().unwrap_or(0.0)
1685 };
1686
1687 let principal = if let Some(principal_str) = data["principal"].as_str() {
1688 principal_str.parse::<f64>().unwrap_or(0.0)
1689 } else {
1690 data["principal"].as_f64().unwrap_or(0.0)
1691 };
1692
1693 let timestamp = data["interestAccuredTime"]
1694 .as_i64()
1695 .or_else(|| data["timestamp"].as_i64())
1696 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1697
1698 Ok(BorrowInterest::new(
1699 id,
1700 currency,
1701 symbol,
1702 interest,
1703 interest_rate,
1704 principal,
1705 timestamp,
1706 data.clone(),
1707 ))
1708}
1709
1710pub fn parse_margin_adjustment(data: &Value) -> Result<MarginAdjustment> {
1720 let id = data["tranId"]
1721 .as_i64()
1722 .or_else(|| data["txId"].as_i64())
1723 .map(|id| id.to_string())
1724 .unwrap_or_default();
1725
1726 let symbol = data["symbol"].as_str().map(|s| s.to_string());
1727
1728 let currency = data["asset"]
1729 .as_str()
1730 .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
1731 .to_string();
1732
1733 let amount = if let Some(amount_str) = data["amount"].as_str() {
1734 amount_str.parse::<f64>().unwrap_or(0.0)
1735 } else {
1736 data["amount"].as_f64().unwrap_or(0.0)
1737 };
1738
1739 let transfer_type = data["type"]
1740 .as_str()
1741 .or_else(|| data["transFrom"].as_str())
1742 .map(|t| {
1743 if t.contains("MAIN") || t.eq("1") || t.eq("ROLL_IN") {
1744 "IN"
1745 } else {
1746 "OUT"
1747 }
1748 })
1749 .unwrap_or("IN")
1750 .to_string();
1751
1752 let timestamp = data["timestamp"]
1753 .as_i64()
1754 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1755
1756 let status = data["status"].as_str().unwrap_or("SUCCESS").to_string();
1757
1758 Ok(MarginAdjustment::new(
1759 id,
1760 symbol,
1761 currency,
1762 amount,
1763 transfer_type,
1764 timestamp,
1765 status,
1766 data.clone(),
1767 ))
1768}
1769pub fn parse_futures_transfer_type(transfer_type: i32) -> Result<(&'static str, &'static str)> {
1797 match transfer_type {
1798 1 => Ok(("spot", "future")),
1799 2 => Ok(("future", "spot")),
1800 3 => Ok(("spot", "delivery")),
1801 4 => Ok(("delivery", "spot")),
1802 _ => Err(Error::invalid_request(format!(
1803 "Invalid futures transfer type: {}. Must be between 1 and 4",
1804 transfer_type
1805 ))),
1806 }
1807}
1808
1809pub fn parse_transfer(data: &Value) -> Result<Transfer> {
1838 let id = data["tranId"]
1839 .as_i64()
1840 .or_else(|| data["txId"].as_i64())
1841 .or_else(|| data["transactionId"].as_i64())
1842 .map(|id| id.to_string());
1843
1844 let timestamp = data["timestamp"]
1845 .as_i64()
1846 .or_else(|| data["transactionTime"].as_i64())
1847 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1848
1849 let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
1850 .map(|dt| dt.to_rfc3339())
1851 .unwrap_or_default();
1852
1853 let currency = data["asset"]
1854 .as_str()
1855 .or_else(|| data["currency"].as_str())
1856 .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
1857 .to_string();
1858
1859 let amount = if let Some(amount_str) = data["amount"].as_str() {
1860 amount_str.parse::<f64>().unwrap_or(0.0)
1861 } else {
1862 data["amount"].as_f64().unwrap_or(0.0)
1863 };
1864
1865 let mut from_account = data["fromAccountType"].as_str().map(|s| s.to_string());
1866
1867 let mut to_account = data["toAccountType"].as_str().map(|s| s.to_string());
1868
1869 if from_account.is_none() || to_account.is_none() {
1871 if let Some(type_str) = data["type"].as_str() {
1872 let parts: Vec<&str> = type_str.split('_').collect();
1873 if parts.len() == 2 {
1874 from_account = Some(parts[0].to_lowercase());
1875 to_account = Some(parts[1].to_lowercase());
1876 }
1877 }
1878 }
1879
1880 let status = data["status"].as_str().unwrap_or("SUCCESS").to_lowercase();
1881
1882 Ok(Transfer {
1883 id,
1884 timestamp: timestamp as u64,
1885 datetime,
1886 currency,
1887 amount,
1888 from_account,
1889 to_account,
1890 status,
1891 info: Some(data.clone()),
1892 })
1893}
1894
1895pub fn parse_max_borrowable(
1907 data: &Value,
1908 currency: &str,
1909 symbol: Option<String>,
1910) -> Result<MaxBorrowable> {
1911 let amount = if let Some(amount_str) = data["amount"].as_str() {
1912 amount_str.parse::<f64>().unwrap_or(0.0)
1913 } else {
1914 data["amount"].as_f64().unwrap_or(0.0)
1915 };
1916
1917 let borrow_limit = if let Some(limit_str) = data["borrowLimit"].as_str() {
1918 Some(limit_str.parse::<f64>().unwrap_or(0.0))
1919 } else {
1920 data["borrowLimit"].as_f64()
1921 };
1922
1923 let timestamp = chrono::Utc::now().timestamp_millis();
1924 let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
1925 .map(|dt| dt.to_rfc3339())
1926 .unwrap_or_default();
1927
1928 Ok(MaxBorrowable {
1929 currency: currency.to_string(),
1930 amount,
1931 borrow_limit,
1932 symbol,
1933 timestamp,
1934 datetime,
1935 info: data.clone(),
1936 })
1937}
1938
1939pub fn parse_max_transferable(
1951 data: &Value,
1952 currency: &str,
1953 symbol: Option<String>,
1954) -> Result<MaxTransferable> {
1955 let amount = if let Some(amount_str) = data["amount"].as_str() {
1956 amount_str.parse::<f64>().unwrap_or(0.0)
1957 } else {
1958 data["amount"].as_f64().unwrap_or(0.0)
1959 };
1960
1961 let timestamp = chrono::Utc::now().timestamp_millis();
1962 let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
1963 .map(|dt| dt.to_rfc3339())
1964 .unwrap_or_default();
1965
1966 Ok(MaxTransferable {
1967 currency: currency.to_string(),
1968 amount,
1969 symbol,
1970 timestamp,
1971 datetime,
1972 info: data.clone(),
1973 })
1974}
1975
1976pub fn parse_balance_with_type(data: &Value, account_type: &str) -> Result<Balance> {
1989 let mut balances = HashMap::new();
1990 let _timestamp = chrono::Utc::now().timestamp_millis();
1991
1992 match account_type {
1993 "spot" => {
1994 if let Some(balances_array) = data["balances"].as_array() {
1995 for item in balances_array {
1996 if let Some(asset) = item["asset"].as_str() {
1997 let free = if let Some(free_str) = item["free"].as_str() {
1998 free_str.parse::<f64>().unwrap_or(0.0)
1999 } else {
2000 item["free"].as_f64().unwrap_or(0.0)
2001 };
2002
2003 let locked = if let Some(locked_str) = item["locked"].as_str() {
2004 locked_str.parse::<f64>().unwrap_or(0.0)
2005 } else {
2006 item["locked"].as_f64().unwrap_or(0.0)
2007 };
2008
2009 balances.insert(
2010 asset.to_string(),
2011 BalanceEntry {
2012 free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2013 used: Decimal::from_f64(locked).unwrap_or(Decimal::ZERO),
2014 total: Decimal::from_f64(free + locked).unwrap_or(Decimal::ZERO),
2015 },
2016 );
2017 }
2018 }
2019 }
2020 }
2021 "margin" | "cross" => {
2022 if let Some(user_assets) = data["userAssets"].as_array() {
2023 for item in user_assets {
2024 if let Some(asset) = item["asset"].as_str() {
2025 let free = if let Some(free_str) = item["free"].as_str() {
2026 free_str.parse::<f64>().unwrap_or(0.0)
2027 } else {
2028 item["free"].as_f64().unwrap_or(0.0)
2029 };
2030
2031 let locked = if let Some(locked_str) = item["locked"].as_str() {
2032 locked_str.parse::<f64>().unwrap_or(0.0)
2033 } else {
2034 item["locked"].as_f64().unwrap_or(0.0)
2035 };
2036
2037 balances.insert(
2038 asset.to_string(),
2039 BalanceEntry {
2040 free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2041 used: Decimal::from_f64(locked).unwrap_or(Decimal::ZERO),
2042 total: Decimal::from_f64(free + locked).unwrap_or(Decimal::ZERO),
2043 },
2044 );
2045 }
2046 }
2047 }
2048 }
2049 "isolated" => {
2050 if let Some(assets) = data["assets"].as_array() {
2051 for item in assets {
2052 if let Some(base_asset) = item["baseAsset"].as_object() {
2053 if let Some(asset) = base_asset["asset"].as_str() {
2054 let free = if let Some(free_str) = base_asset["free"].as_str() {
2055 free_str.parse::<f64>().unwrap_or(0.0)
2056 } else {
2057 base_asset["free"].as_f64().unwrap_or(0.0)
2058 };
2059
2060 let locked = if let Some(locked_str) = base_asset["locked"].as_str() {
2061 locked_str.parse::<f64>().unwrap_or(0.0)
2062 } else {
2063 base_asset["locked"].as_f64().unwrap_or(0.0)
2064 };
2065
2066 balances.insert(
2067 asset.to_string(),
2068 BalanceEntry {
2069 free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2070 used: Decimal::from_f64(locked).unwrap_or(Decimal::ZERO),
2071 total: Decimal::from_f64(free + locked)
2072 .unwrap_or(Decimal::ZERO),
2073 },
2074 );
2075 }
2076 }
2077
2078 if let Some(quote_asset) = item["quoteAsset"].as_object() {
2079 if let Some(asset) = quote_asset["asset"].as_str() {
2080 let free = if let Some(free_str) = quote_asset["free"].as_str() {
2081 free_str.parse::<f64>().unwrap_or(0.0)
2082 } else {
2083 quote_asset["free"].as_f64().unwrap_or(0.0)
2084 };
2085
2086 let locked = if let Some(locked_str) = quote_asset["locked"].as_str() {
2087 locked_str.parse::<f64>().unwrap_or(0.0)
2088 } else {
2089 quote_asset["locked"].as_f64().unwrap_or(0.0)
2090 };
2091
2092 balances.insert(
2093 asset.to_string(),
2094 BalanceEntry {
2095 free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2096 used: Decimal::from_f64(locked).unwrap_or(Decimal::ZERO),
2097 total: Decimal::from_f64(free + locked)
2098 .unwrap_or(Decimal::ZERO),
2099 },
2100 );
2101 }
2102 }
2103 }
2104 }
2105 }
2106 "linear" | "future" => {
2107 if let Some(assets) = data["assets"].as_array() {
2108 for item in assets {
2109 if let Some(asset) = item["asset"].as_str() {
2110 let available_balance =
2111 if let Some(balance_str) = item["availableBalance"].as_str() {
2112 balance_str.parse::<f64>().unwrap_or(0.0)
2113 } else {
2114 item["availableBalance"].as_f64().unwrap_or(0.0)
2115 };
2116
2117 let wallet_balance =
2118 if let Some(balance_str) = item["walletBalance"].as_str() {
2119 balance_str.parse::<f64>().unwrap_or(0.0)
2120 } else {
2121 item["walletBalance"].as_f64().unwrap_or(0.0)
2122 };
2123
2124 let used = wallet_balance - available_balance;
2125
2126 balances.insert(
2127 asset.to_string(),
2128 BalanceEntry {
2129 free: Decimal::from_f64(available_balance).unwrap_or(Decimal::ZERO),
2130 used: Decimal::from_f64(used).unwrap_or(Decimal::ZERO),
2131 total: Decimal::from_f64(wallet_balance).unwrap_or(Decimal::ZERO),
2132 },
2133 );
2134 }
2135 }
2136 }
2137 }
2138 "inverse" | "delivery" => {
2139 if let Some(assets) = data["assets"].as_array() {
2140 for item in assets {
2141 if let Some(asset) = item["asset"].as_str() {
2142 let available_balance =
2143 if let Some(balance_str) = item["availableBalance"].as_str() {
2144 balance_str.parse::<f64>().unwrap_or(0.0)
2145 } else {
2146 item["availableBalance"].as_f64().unwrap_or(0.0)
2147 };
2148
2149 let wallet_balance =
2150 if let Some(balance_str) = item["walletBalance"].as_str() {
2151 balance_str.parse::<f64>().unwrap_or(0.0)
2152 } else {
2153 item["walletBalance"].as_f64().unwrap_or(0.0)
2154 };
2155
2156 let used = wallet_balance - available_balance;
2157
2158 balances.insert(
2159 asset.to_string(),
2160 BalanceEntry {
2161 free: Decimal::from_f64(available_balance).unwrap_or(Decimal::ZERO),
2162 used: Decimal::from_f64(used).unwrap_or(Decimal::ZERO),
2163 total: Decimal::from_f64(wallet_balance).unwrap_or(Decimal::ZERO),
2164 },
2165 );
2166 }
2167 }
2168 }
2169 }
2170 "funding" => {
2171 if let Some(assets) = data.as_array() {
2172 for item in assets {
2173 if let Some(asset) = item["asset"].as_str() {
2174 let free = if let Some(free_str) = item["free"].as_str() {
2175 free_str.parse::<f64>().unwrap_or(0.0)
2176 } else {
2177 item["free"].as_f64().unwrap_or(0.0)
2178 };
2179
2180 let locked = if let Some(locked_str) = item["locked"].as_str() {
2181 locked_str.parse::<f64>().unwrap_or(0.0)
2182 } else {
2183 item["locked"].as_f64().unwrap_or(0.0)
2184 };
2185
2186 let total = free + locked;
2187
2188 balances.insert(
2189 asset.to_string(),
2190 BalanceEntry {
2191 free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2192 used: Decimal::from_f64(locked).unwrap_or(Decimal::ZERO),
2193 total: Decimal::from_f64(total).unwrap_or(Decimal::ZERO),
2194 },
2195 );
2196 }
2197 }
2198 }
2199 }
2200 _ => {
2201 return Err(Error::from(ParseError::invalid_value(
2202 "account_type",
2203 format!("Unsupported account type: {}", account_type),
2204 )));
2205 }
2206 }
2207
2208 let mut info_map = HashMap::new();
2209 if let Some(obj) = data.as_object() {
2210 for (k, v) in obj {
2211 info_map.insert(k.clone(), v.clone());
2212 }
2213 }
2214
2215 Ok(Balance {
2216 balances,
2217 info: info_map,
2218 })
2219}
2220
2221pub fn parse_bid_ask(data: &Value) -> Result<ccxt_core::types::BidAsk> {
2235 use ccxt_core::types::BidAsk;
2236
2237 let symbol = data["symbol"]
2238 .as_str()
2239 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
2240 .to_string();
2241
2242 let formatted_symbol = if symbol.len() >= 6 {
2243 let quote_currencies = ["USDT", "BUSD", "USDC", "BTC", "ETH", "BNB"];
2244 let mut found = false;
2245 let mut formatted = symbol.clone();
2246
2247 for quote in "e_currencies {
2248 if symbol.ends_with(quote) {
2249 let base = &symbol[..symbol.len() - quote.len()];
2250 formatted = format!("{}/{}", base, quote);
2251 found = true;
2252 break;
2253 }
2254 }
2255
2256 if !found { symbol.clone() } else { formatted }
2257 } else {
2258 symbol.clone()
2259 };
2260
2261 let bid_price = parse_f64(data, "bidPrice").unwrap_or(0.0);
2262 let bid_quantity = parse_f64(data, "bidQty").unwrap_or(0.0);
2263 let ask_price = parse_f64(data, "askPrice").unwrap_or(0.0);
2264 let ask_quantity = parse_f64(data, "askQty").unwrap_or(0.0);
2265
2266 let timestamp = data["time"]
2267 .as_i64()
2268 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
2269
2270 Ok(BidAsk {
2271 symbol: formatted_symbol,
2272 bid_price,
2273 bid_quantity,
2274 ask_price,
2275 ask_quantity,
2276 timestamp,
2277 })
2278}
2279
2280pub fn parse_bids_asks(data: &Value) -> Result<Vec<ccxt_core::types::BidAsk>> {
2290 if let Some(array) = data.as_array() {
2291 array.iter().map(|item| parse_bid_ask(item)).collect()
2292 } else {
2293 Ok(vec![parse_bid_ask(data)?])
2294 }
2295}
2296
2297pub fn parse_last_price(data: &Value) -> Result<ccxt_core::types::LastPrice> {
2307 use ccxt_core::types::LastPrice;
2308
2309 let symbol = data["symbol"]
2310 .as_str()
2311 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
2312 .to_string();
2313
2314 let formatted_symbol = if symbol.len() >= 6 {
2315 let quote_currencies = ["USDT", "BUSD", "USDC", "BTC", "ETH", "BNB"];
2316 let mut found = false;
2317 let mut formatted = symbol.clone();
2318
2319 for quote in "e_currencies {
2320 if symbol.ends_with(quote) {
2321 let base = &symbol[..symbol.len() - quote.len()];
2322 formatted = format!("{}/{}", base, quote);
2323 found = true;
2324 break;
2325 }
2326 }
2327
2328 if !found { symbol.clone() } else { formatted }
2329 } else {
2330 symbol.clone()
2331 };
2332
2333 let price = parse_f64(data, "price").unwrap_or(0.0);
2334
2335 let timestamp = data["time"]
2336 .as_u64()
2337 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis() as u64);
2338
2339 let datetime = chrono::DateTime::from_timestamp_millis(timestamp as i64)
2340 .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string())
2341 .unwrap_or_default();
2342
2343 Ok(LastPrice {
2344 symbol: formatted_symbol,
2345 price,
2346 timestamp,
2347 datetime,
2348 })
2349}
2350
2351pub fn parse_last_prices(data: &Value) -> Result<Vec<ccxt_core::types::LastPrice>> {
2361 if let Some(array) = data.as_array() {
2362 array.iter().map(|item| parse_last_price(item)).collect()
2363 } else {
2364 Ok(vec![parse_last_price(data)?])
2365 }
2366}
2367
2368pub fn parse_mark_price(data: &Value) -> Result<ccxt_core::types::MarkPrice> {
2378 use ccxt_core::types::MarkPrice;
2379
2380 let symbol = data["symbol"]
2381 .as_str()
2382 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
2383 .to_string();
2384
2385 let formatted_symbol = if symbol.len() >= 6 {
2386 let quote_currencies = ["USDT", "BUSD", "USDC", "BTC", "ETH", "BNB"];
2387 let mut found = false;
2388 let mut formatted = symbol.clone();
2389
2390 for quote in "e_currencies {
2391 if symbol.ends_with(quote) {
2392 let base = &symbol[..symbol.len() - quote.len()];
2393 formatted = format!("{}/{}", base, quote);
2394 found = true;
2395 break;
2396 }
2397 }
2398
2399 if !found { symbol.clone() } else { formatted }
2400 } else {
2401 symbol.clone()
2402 };
2403
2404 let mark_price = parse_f64(data, "markPrice").unwrap_or(0.0);
2405 let index_price = parse_f64(data, "indexPrice");
2406 let estimated_settle_price = parse_f64(data, "estimatedSettlePrice");
2407 let last_funding_rate = parse_f64(data, "lastFundingRate");
2408
2409 let next_funding_time = data["nextFundingTime"].as_i64();
2410
2411 let timestamp = data["time"]
2412 .as_i64()
2413 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
2414
2415 Ok(MarkPrice {
2416 symbol: formatted_symbol,
2417 mark_price,
2418 index_price,
2419 estimated_settle_price,
2420 last_funding_rate,
2421 next_funding_time,
2422 interest_rate: None,
2423 timestamp,
2424 })
2425}
2426
2427pub fn parse_mark_prices(data: &Value) -> Result<Vec<ccxt_core::types::MarkPrice>> {
2437 if let Some(array) = data.as_array() {
2438 array.iter().map(|item| parse_mark_price(item)).collect()
2439 } else {
2440 Ok(vec![parse_mark_price(data)?])
2441 }
2442}
2443
2444pub fn parse_ohlcv(data: &Value) -> Result<ccxt_core::types::OHLCV> {
2469 use ccxt_core::error::{Error, ParseError};
2470 use ccxt_core::types::OHLCV;
2471
2472 if let Some(array) = data.as_array() {
2473 if array.len() < 6 {
2474 return Err(Error::from(ParseError::invalid_format(
2475 "data",
2476 "OHLCV array length insufficient",
2477 )));
2478 }
2479
2480 let timestamp = array[0]
2481 .as_i64()
2482 .ok_or_else(|| Error::from(ParseError::invalid_format("data", "无效的时间戳")))?;
2483
2484 let open = array[1]
2485 .as_str()
2486 .and_then(|s| s.parse::<f64>().ok())
2487 .or_else(|| array[1].as_f64())
2488 .ok_or_else(|| Error::from(ParseError::invalid_format("data", "Invalid open price")))?;
2489
2490 let high = array[2]
2491 .as_str()
2492 .and_then(|s| s.parse::<f64>().ok())
2493 .or_else(|| array[2].as_f64())
2494 .ok_or_else(|| Error::from(ParseError::invalid_format("data", "Invalid high price")))?;
2495
2496 let low = array[3]
2497 .as_str()
2498 .and_then(|s| s.parse::<f64>().ok())
2499 .or_else(|| array[3].as_f64())
2500 .ok_or_else(|| Error::from(ParseError::invalid_format("data", "Invalid low price")))?;
2501
2502 let close = array[4]
2503 .as_str()
2504 .and_then(|s| s.parse::<f64>().ok())
2505 .or_else(|| array[4].as_f64())
2506 .ok_or_else(|| {
2507 Error::from(ParseError::invalid_format("data", "Invalid close price"))
2508 })?;
2509
2510 let volume = array[5]
2511 .as_str()
2512 .and_then(|s| s.parse::<f64>().ok())
2513 .or_else(|| array[5].as_f64())
2514 .ok_or_else(|| Error::from(ParseError::invalid_format("data", "Invalid volume")))?;
2515
2516 Ok(OHLCV::new(timestamp, open, high, low, close, volume))
2517 } else {
2518 Err(Error::from(ParseError::invalid_format(
2519 "data",
2520 "OHLCV data must be in array format",
2521 )))
2522 }
2523}
2524
2525pub fn parse_ohlcvs(data: &Value) -> Result<Vec<ccxt_core::types::OHLCV>> {
2531 use ccxt_core::error::{Error, ParseError};
2532
2533 if let Some(array) = data.as_array() {
2534 array.iter().map(|item| parse_ohlcv(item)).collect()
2535 } else {
2536 Err(Error::from(ParseError::invalid_format(
2537 "data",
2538 "OHLCV data list must be in array format",
2539 )))
2540 }
2541}
2542
2543pub fn parse_trading_fee(data: &Value) -> Result<FeeTradingFee> {
2559 use ccxt_core::error::{Error, ParseError};
2560
2561 let symbol = data["symbol"]
2562 .as_str()
2563 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
2564 .to_string();
2565
2566 let maker = data["makerCommission"]
2567 .as_str()
2568 .and_then(|s| Decimal::from_str(s).ok())
2569 .or_else(|| data["makerCommission"].as_f64().and_then(Decimal::from_f64))
2570 .ok_or_else(|| {
2571 Error::from(ParseError::invalid_format(
2572 "data",
2573 "Invalid maker commission",
2574 ))
2575 })?;
2576
2577 let taker = data["takerCommission"]
2578 .as_str()
2579 .and_then(|s| Decimal::from_str(s).ok())
2580 .or_else(|| data["takerCommission"].as_f64().and_then(Decimal::from_f64))
2581 .ok_or_else(|| {
2582 Error::from(ParseError::invalid_format(
2583 "data",
2584 "Invalid taker commission",
2585 ))
2586 })?;
2587
2588 Ok(FeeTradingFee::new(symbol, maker, taker))
2589}
2590
2591pub fn parse_trading_fees(data: &Value) -> Result<Vec<FeeTradingFee>> {
2597 if let Some(array) = data.as_array() {
2598 array.iter().map(|item| parse_trading_fee(item)).collect()
2599 } else {
2600 Ok(vec![parse_trading_fee(data)?])
2601 }
2602}
2603
2604pub fn parse_server_time(data: &Value) -> Result<ccxt_core::types::ServerTime> {
2618 use ccxt_core::error::{Error, ParseError};
2619 use ccxt_core::types::ServerTime;
2620
2621 let server_time = data["serverTime"]
2622 .as_i64()
2623 .ok_or_else(|| Error::from(ParseError::missing_field("serverTime")))?;
2624
2625 Ok(ServerTime::new(server_time))
2626}
2627
2628pub fn parse_order_trades(
2653 data: &Value,
2654 market: Option<&Market>,
2655) -> Result<Vec<ccxt_core::types::Trade>> {
2656 if let Some(array) = data.as_array() {
2657 array.iter().map(|item| parse_trade(item, market)).collect()
2658 } else {
2659 Ok(vec![parse_trade(data, market)?])
2660 }
2661}
2662
2663pub fn parse_edit_order_result(data: &Value, market: Option<&Market>) -> Result<Order> {
2689 let new_order_data = data.get("newOrderResponse").ok_or_else(|| {
2690 Error::from(ParseError::invalid_format(
2691 "data",
2692 "Missing newOrderResponse field",
2693 ))
2694 })?;
2695
2696 parse_order(new_order_data, market)
2697}
2698
2699pub fn parse_ws_ticker(data: &Value, market: Option<&Market>) -> Result<Ticker> {
2772 let market_id = data["s"]
2773 .as_str()
2774 .or_else(|| data["symbol"].as_str())
2775 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?;
2776
2777 let symbol = if let Some(m) = market {
2778 m.symbol.clone()
2779 } else {
2780 market_id.to_string()
2781 };
2782
2783 let event = data["e"].as_str().unwrap_or("bookTicker");
2784
2785 if event == "markPriceUpdate" {
2786 let timestamp = data["E"].as_i64().unwrap_or(0);
2787 return Ok(Ticker {
2788 symbol,
2789 timestamp,
2790 datetime: Some(
2791 chrono::DateTime::from_timestamp_millis(timestamp)
2792 .map(|dt| dt.to_rfc3339())
2793 .unwrap_or_default(),
2794 ),
2795 high: None,
2796 low: None,
2797 bid: None,
2798 bid_volume: None,
2799 ask: None,
2800 ask_volume: None,
2801 vwap: None,
2802 open: None,
2803 close: parse_f64(data, "p")
2804 .and_then(Decimal::from_f64_retain)
2805 .map(Price::from),
2806 last: parse_f64(data, "p")
2807 .and_then(Decimal::from_f64_retain)
2808 .map(Price::from),
2809 previous_close: None,
2810 change: None,
2811 percentage: None,
2812 average: None,
2813 base_volume: None,
2814 quote_volume: None,
2815 info: value_to_hashmap(data),
2816 });
2817 }
2818
2819 let timestamp = if event == "bookTicker" {
2820 data["E"]
2821 .as_i64()
2822 .or_else(|| data["time"].as_i64())
2823 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis())
2824 } else {
2825 data["C"]
2826 .as_i64()
2827 .or_else(|| data["E"].as_i64())
2828 .or_else(|| data["time"].as_i64())
2829 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis())
2830 };
2831
2832 let last = parse_f64(data, "c")
2833 .or_else(|| parse_f64(data, "price"))
2834 .and_then(Decimal::from_f64_retain);
2835
2836 Ok(Ticker {
2837 symbol,
2838 timestamp,
2839 datetime: Some(
2840 chrono::DateTime::from_timestamp_millis(timestamp)
2841 .map(|dt| dt.to_rfc3339())
2842 .unwrap_or_default(),
2843 ),
2844 high: parse_f64(data, "h")
2845 .and_then(Decimal::from_f64_retain)
2846 .map(Price::from),
2847 low: parse_f64(data, "l")
2848 .and_then(Decimal::from_f64_retain)
2849 .map(Price::from),
2850 bid: parse_f64(data, "b")
2851 .or_else(|| parse_f64(data, "bidPrice"))
2852 .and_then(Decimal::from_f64_retain)
2853 .map(Price::from),
2854 bid_volume: parse_f64(data, "B")
2855 .or_else(|| parse_f64(data, "bidQty"))
2856 .and_then(Decimal::from_f64_retain)
2857 .map(Amount::from),
2858 ask: parse_f64(data, "a")
2859 .or_else(|| parse_f64(data, "askPrice"))
2860 .and_then(Decimal::from_f64_retain)
2861 .map(Price::from),
2862 ask_volume: parse_f64(data, "A")
2863 .or_else(|| parse_f64(data, "askQty"))
2864 .and_then(Decimal::from_f64_retain)
2865 .map(Amount::from),
2866 vwap: parse_f64(data, "w")
2867 .and_then(Decimal::from_f64_retain)
2868 .map(Price::from),
2869 open: parse_f64(data, "o")
2870 .and_then(Decimal::from_f64_retain)
2871 .map(Price::from),
2872 close: last.map(Price::from),
2873 last: last.map(Price::from),
2874 previous_close: parse_f64(data, "x")
2875 .and_then(Decimal::from_f64_retain)
2876 .map(Price::from),
2877 change: parse_f64(data, "p")
2878 .and_then(Decimal::from_f64_retain)
2879 .map(Price::from),
2880 percentage: parse_f64(data, "P").and_then(Decimal::from_f64_retain),
2881 average: None,
2882 base_volume: parse_f64(data, "v")
2883 .and_then(Decimal::from_f64_retain)
2884 .map(Amount::from),
2885 quote_volume: parse_f64(data, "q")
2886 .and_then(Decimal::from_f64_retain)
2887 .map(Amount::from),
2888 info: value_to_hashmap(data),
2889 })
2890}
2891
2892pub fn parse_ws_trade(data: &Value, market: Option<&Market>) -> Result<Trade> {
2941 let market_id = data["s"]
2942 .as_str()
2943 .or_else(|| data["symbol"].as_str())
2944 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?;
2945
2946 let symbol = if let Some(m) = market {
2947 m.symbol.clone()
2948 } else {
2949 market_id.to_string()
2950 };
2951
2952 let id = data["t"]
2953 .as_u64()
2954 .or_else(|| data["a"].as_u64())
2955 .map(|i| i.to_string());
2956
2957 let timestamp = data["T"].as_i64().unwrap_or(0);
2958
2959 let price = parse_f64(data, "L")
2960 .or_else(|| parse_f64(data, "p"))
2961 .and_then(Decimal::from_f64_retain);
2962
2963 let amount = parse_f64(data, "q").and_then(Decimal::from_f64_retain);
2964
2965 let cost = match (price, amount) {
2966 (Some(p), Some(a)) => Some(p * a),
2967 _ => None,
2968 };
2969
2970 let side = if data["m"].as_bool().unwrap_or(false) {
2971 OrderSide::Sell
2972 } else {
2973 OrderSide::Buy
2974 };
2975
2976 let taker_or_maker = if data["m"].as_bool().unwrap_or(false) {
2977 Some(TakerOrMaker::Maker)
2978 } else {
2979 Some(TakerOrMaker::Taker)
2980 };
2981
2982 Ok(Trade {
2983 id,
2984 order: data["orderId"]
2985 .as_u64()
2986 .or_else(|| data["orderid"].as_u64())
2987 .map(|i| i.to_string()),
2988 timestamp,
2989 datetime: Some(
2990 chrono::DateTime::from_timestamp_millis(timestamp)
2991 .map(|dt| dt.to_rfc3339())
2992 .unwrap_or_default(),
2993 ),
2994 symbol,
2995 trade_type: None,
2996 side,
2997 taker_or_maker: taker_or_maker,
2998 price: Price::from(price.unwrap_or(Decimal::ZERO)),
2999 amount: Amount::from(amount.unwrap_or(Decimal::ZERO)),
3000 cost: cost.map(Cost::from),
3001 fee: None,
3002 info: value_to_hashmap(data),
3003 })
3004}
3005
3006pub fn parse_ws_orderbook(data: &Value, symbol: String) -> Result<OrderBook> {
3035 let timestamp = data["E"]
3036 .as_i64()
3037 .or_else(|| data["T"].as_i64())
3038 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
3039
3040 let bids = parse_orderbook_side(&data["b"])?;
3041 let asks = parse_orderbook_side(&data["a"])?;
3042
3043 Ok(OrderBook {
3044 symbol,
3045 timestamp,
3046 datetime: Some(
3047 chrono::DateTime::from_timestamp_millis(timestamp)
3048 .map(|dt| dt.to_rfc3339())
3049 .unwrap_or_default(),
3050 ),
3051 nonce: data["u"].as_i64(),
3052 bids,
3053 asks,
3054 buffered_deltas: std::collections::VecDeque::new(),
3055 bids_map: std::collections::BTreeMap::new(),
3056 asks_map: std::collections::BTreeMap::new(),
3057 is_synced: false,
3058 needs_resync: false,
3059 last_resync_time: 0,
3060 info: value_to_hashmap(data),
3061 })
3062}
3063
3064pub fn parse_ws_ohlcv(data: &Value) -> Result<OHLCV> {
3102 let kline = data["k"]
3103 .as_object()
3104 .ok_or_else(|| Error::from(ParseError::missing_field("k")))?;
3105
3106 let timestamp = kline
3107 .get("t")
3108 .and_then(|v| v.as_i64())
3109 .ok_or_else(|| Error::from(ParseError::missing_field("t")))?;
3110
3111 let open = kline
3112 .get("o")
3113 .and_then(|v| v.as_str())
3114 .and_then(|s| s.parse::<f64>().ok())
3115 .ok_or_else(|| Error::from(ParseError::missing_field("o")))?;
3116
3117 let high = kline
3118 .get("h")
3119 .and_then(|v| v.as_str())
3120 .and_then(|s| s.parse::<f64>().ok())
3121 .ok_or_else(|| Error::from(ParseError::missing_field("h")))?;
3122
3123 let low = kline
3124 .get("l")
3125 .and_then(|v| v.as_str())
3126 .and_then(|s| s.parse::<f64>().ok())
3127 .ok_or_else(|| Error::from(ParseError::missing_field("l")))?;
3128
3129 let close = kline
3130 .get("c")
3131 .and_then(|v| v.as_str())
3132 .and_then(|s| s.parse::<f64>().ok())
3133 .ok_or_else(|| Error::from(ParseError::missing_field("c")))?;
3134
3135 let volume = kline
3136 .get("v")
3137 .and_then(|v| v.as_str())
3138 .and_then(|s| s.parse::<f64>().ok())
3139 .ok_or_else(|| Error::from(ParseError::missing_field("v")))?;
3140
3141 Ok(OHLCV {
3142 timestamp,
3143 open,
3144 high,
3145 low,
3146 close,
3147 volume,
3148 })
3149}
3150
3151pub fn parse_ws_bid_ask(data: &Value) -> Result<BidAsk> {
3174 let symbol = data["s"]
3175 .as_str()
3176 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
3177 .to_string();
3178
3179 let bid_price = data["b"]
3180 .as_str()
3181 .and_then(|s| s.parse::<f64>().ok())
3182 .ok_or_else(|| Error::from(ParseError::missing_field("bid_price")))?;
3183
3184 let bid_quantity = data["B"]
3185 .as_str()
3186 .and_then(|s| s.parse::<f64>().ok())
3187 .ok_or_else(|| Error::from(ParseError::missing_field("bid_quantity")))?;
3188
3189 let ask_price = data["a"]
3190 .as_str()
3191 .and_then(|s| s.parse::<f64>().ok())
3192 .ok_or_else(|| Error::from(ParseError::missing_field("ask_price")))?;
3193
3194 let ask_quantity = data["A"]
3195 .as_str()
3196 .and_then(|s| s.parse::<f64>().ok())
3197 .ok_or_else(|| Error::from(ParseError::missing_field("ask_quantity")))?;
3198
3199 let timestamp = data["E"].as_i64().unwrap_or(0);
3200
3201 Ok(BidAsk {
3202 symbol,
3203 bid_price,
3204 bid_quantity,
3205 ask_price,
3206 ask_quantity,
3207 timestamp,
3208 })
3209}
3210
3211pub fn parse_ws_mark_price(data: &Value) -> Result<MarkPrice> {
3227 let symbol = data["s"]
3228 .as_str()
3229 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
3230 .to_string();
3231
3232 let mark_price = data["p"]
3233 .as_str()
3234 .and_then(|s| s.parse::<f64>().ok())
3235 .ok_or_else(|| Error::from(ParseError::missing_field("mark_price")))?;
3236
3237 let index_price = data["i"].as_str().and_then(|s| s.parse::<f64>().ok());
3238
3239 let estimated_settle_price = data["P"].as_str().and_then(|s| s.parse::<f64>().ok());
3240
3241 let last_funding_rate = data["r"].as_str().and_then(|s| s.parse::<f64>().ok());
3242
3243 let next_funding_time = data["T"].as_i64();
3244
3245 let interest_rate = None;
3246
3247 let timestamp = data["E"].as_i64().unwrap_or(0);
3248
3249 Ok(MarkPrice {
3250 symbol,
3251 mark_price,
3252 index_price,
3253 estimated_settle_price,
3254 last_funding_rate,
3255 next_funding_time,
3256 interest_rate,
3257 timestamp,
3258 })
3259}
3260
3261pub fn parse_deposit_withdraw_fee(data: &Value) -> Result<ccxt_core::types::DepositWithdrawFee> {
3302 let currency = data["coin"]
3303 .as_str()
3304 .ok_or_else(|| Error::from(ParseError::missing_field("coin")))?
3305 .to_string();
3306
3307 let mut networks = Vec::new();
3308 let mut withdraw_fee = 0.0;
3309 let mut withdraw_min = 0.0;
3310 let mut withdraw_max = 0.0;
3311 let mut deposit_enable = false;
3312 let mut withdraw_enable = false;
3313
3314 if let Some(network_list) = data["networkList"].as_array() {
3315 for network_data in network_list {
3316 let network = parse_network_info(network_data)?;
3317
3318 if network_data["isDefault"].as_bool().unwrap_or(false) {
3319 withdraw_fee = network.withdraw_fee;
3320 withdraw_min = network.withdraw_min;
3321 withdraw_max = network.withdraw_max;
3322 deposit_enable = network.deposit_enable;
3323 withdraw_enable = network.withdraw_enable;
3324 }
3325
3326 networks.push(network);
3327 }
3328 }
3329
3330 if !networks.is_empty() && withdraw_fee == 0.0 {
3331 let first = &networks[0];
3332 withdraw_fee = first.withdraw_fee;
3333 withdraw_min = first.withdraw_min;
3334 withdraw_max = first.withdraw_max;
3335 deposit_enable = first.deposit_enable;
3336 withdraw_enable = first.withdraw_enable;
3337 }
3338
3339 Ok(DepositWithdrawFee {
3340 currency,
3341 withdraw_fee,
3342 withdraw_min,
3343 withdraw_max,
3344 deposit_enable,
3345 withdraw_enable,
3346 networks,
3347 info: Some(data.clone()),
3348 })
3349}
3350
3351pub fn parse_network_info(data: &Value) -> Result<ccxt_core::types::NetworkInfo> {
3379 use ccxt_core::error::{Error, ParseError};
3380 use ccxt_core::types::NetworkInfo;
3381
3382 let network = data["network"]
3383 .as_str()
3384 .ok_or_else(|| Error::from(ParseError::missing_field("network")))?
3385 .to_string();
3386
3387 let name = data["name"].as_str().unwrap_or(&network).to_string();
3388
3389 let withdraw_fee = data["withdrawFee"]
3390 .as_str()
3391 .and_then(|s| s.parse::<f64>().ok())
3392 .or_else(|| data["withdrawFee"].as_f64())
3393 .unwrap_or(0.0);
3394
3395 let withdraw_min = data["withdrawMin"]
3396 .as_str()
3397 .and_then(|s| s.parse::<f64>().ok())
3398 .or_else(|| data["withdrawMin"].as_f64())
3399 .unwrap_or(0.0);
3400
3401 let withdraw_max = data["withdrawMax"]
3402 .as_str()
3403 .and_then(|s| s.parse::<f64>().ok())
3404 .or_else(|| data["withdrawMax"].as_f64())
3405 .unwrap_or(0.0);
3406
3407 let deposit_enable = data["depositEnable"].as_bool().unwrap_or(false);
3408
3409 let withdraw_enable = data["withdrawEnable"].as_bool().unwrap_or(false);
3410
3411 let _is_default = data["isDefault"].as_bool().unwrap_or(false);
3412
3413 let _min_confirm = data["minConfirm"].as_i64().map(|v| v as u32);
3414
3415 let _unlock_confirm = data["unLockConfirm"].as_i64().map(|v| v as u32);
3416
3417 let deposit_confirmations = data["minConfirm"].as_u64().map(|v| v as u32);
3418
3419 let withdraw_confirmations = data["unlockConfirm"].as_u64().map(|v| v as u32);
3420
3421 Ok(NetworkInfo {
3422 network,
3423 name,
3424 withdraw_fee,
3425 withdraw_min,
3426 withdraw_max,
3427 deposit_enable,
3428 withdraw_enable,
3429 deposit_confirmations,
3430 withdraw_confirmations,
3431 })
3432}
3433
3434pub fn parse_deposit_withdraw_fees(
3440 data: &Value,
3441) -> Result<Vec<ccxt_core::types::DepositWithdrawFee>> {
3442 if let Some(array) = data.as_array() {
3443 array
3444 .iter()
3445 .map(|item| parse_deposit_withdraw_fee(item))
3446 .collect()
3447 } else {
3448 Ok(vec![parse_deposit_withdraw_fee(data)?])
3449 }
3450}
3451
3452#[cfg(test)]
3453mod tests {
3454 use super::*;
3455 use serde_json::json;
3456
3457 #[test]
3458 fn test_parse_market() {
3459 let data = json!({
3460 "symbol": "BTCUSDT",
3461 "baseAsset": "BTC",
3462 "quoteAsset": "USDT",
3463 "status": "TRADING",
3464 "isMarginTradingAllowed": true,
3465 "filters": [
3466 {
3467 "filterType": "PRICE_FILTER",
3468 "tickSize": "0.01"
3469 },
3470 {
3471 "filterType": "LOT_SIZE",
3472 "stepSize": "0.00001",
3473 "minQty": "0.00001",
3474 "maxQty": "9000"
3475 },
3476 {
3477 "filterType": "MIN_NOTIONAL",
3478 "minNotional": "10.0"
3479 }
3480 ]
3481 });
3482
3483 let market = parse_market(&data).unwrap();
3484 assert_eq!(market.symbol, "BTC/USDT");
3485 assert_eq!(market.base, "BTC");
3486 assert_eq!(market.quote, "USDT");
3487 assert!(market.active);
3488 assert!(market.margin);
3489 assert_eq!(
3490 market.precision.price,
3491 Some(Decimal::from_str_radix("0.01", 10).unwrap())
3492 );
3493 assert_eq!(
3494 market.precision.amount,
3495 Some(Decimal::from_str_radix("0.00001", 10).unwrap())
3496 );
3497 }
3498
3499 #[test]
3500 fn test_parse_ticker() {
3501 let data = json!({
3502 "symbol": "BTCUSDT",
3503 "lastPrice": "50000.00",
3504 "openPrice": "49000.00",
3505 "highPrice": "51000.00",
3506 "lowPrice": "48500.00",
3507 "volume": "1000.5",
3508 "quoteVolume": "50000000.0",
3509 "bidPrice": "49999.00",
3510 "bidQty": "1.5",
3511 "askPrice": "50001.00",
3512 "askQty": "2.0",
3513 "closeTime": 1609459200000u64,
3514 "priceChange": "1000.00",
3515 "priceChangePercent": "2.04"
3516 });
3517
3518 let ticker = parse_ticker(&data, None).unwrap();
3519 assert_eq!(
3520 ticker.last,
3521 Some(Price::new(Decimal::from_str_radix("50000.00", 10).unwrap()))
3522 );
3523 assert_eq!(
3524 ticker.high,
3525 Some(Price::new(Decimal::from_str_radix("51000.00", 10).unwrap()))
3526 );
3527 assert_eq!(
3528 ticker.low,
3529 Some(Price::new(Decimal::from_str_radix("48500.00", 10).unwrap()))
3530 );
3531 assert_eq!(
3532 ticker.bid,
3533 Some(Price::new(Decimal::from_str_radix("49999.00", 10).unwrap()))
3534 );
3535 assert_eq!(
3536 ticker.ask,
3537 Some(Price::new(Decimal::from_str_radix("50001.00", 10).unwrap()))
3538 );
3539 }
3540
3541 #[test]
3542 fn test_parse_trade() {
3543 let data = json!({
3544 "id": 12345,
3545 "price": "50000.00",
3546 "qty": "0.5",
3547 "time": 1609459200000u64,
3548 "isBuyerMaker": false,
3549 "symbol": "BTCUSDT"
3550 });
3551
3552 let trade = parse_trade(&data, None).unwrap();
3553 assert_eq!(trade.id, Some("12345".to_string()));
3554 assert_eq!(
3555 trade.price,
3556 Price::new(Decimal::from_str_radix("50000.00", 10).unwrap())
3557 );
3558 assert_eq!(
3559 trade.amount,
3560 Amount::new(Decimal::from_str_radix("0.5", 10).unwrap())
3561 );
3562 assert_eq!(trade.side, OrderSide::Buy);
3563 }
3564
3565 #[test]
3566 fn test_parse_order() {
3567 let data = json!({
3568 "orderId": 12345,
3569 "symbol": "BTCUSDT",
3570 "status": "FILLED",
3571 "side": "BUY",
3572 "type": "LIMIT",
3573
3574 "price": "50000.00",
3575 "origQty": "0.5",
3576 "executedQty": "0.5",
3577 "cummulativeQuoteQty": "25000.00",
3578 "time": 1609459200000u64,
3579 "updateTime": 1609459200000u64
3580 });
3581
3582 let order = parse_order(&data, None).unwrap();
3583 assert_eq!(order.id, "12345".to_string());
3584 assert_eq!(order.symbol, "BTCUSDT");
3585 assert_eq!(order.order_type, OrderType::Limit);
3586 assert_eq!(order.side, OrderSide::Buy);
3587 assert_eq!(
3588 order.price,
3589 Some(Decimal::from_str_radix("50000.00", 10).unwrap())
3590 );
3591 assert_eq!(order.amount, Decimal::from_str_radix("0.5", 10).unwrap());
3592 assert_eq!(
3593 order.filled,
3594 Some(Decimal::from_str_radix("0.5", 10).unwrap())
3595 );
3596 }
3597
3598 #[test]
3599 fn test_parse_balance() {
3600 let data = json!({
3601 "balances": [
3602 {
3603 "asset": "BTC",
3604 "free": "1.5",
3605 "locked": "0.5"
3606 }
3607 ]
3608 });
3609
3610 let balance = parse_balance(&data).unwrap();
3611 let btc_balance = balance.balances.get("BTC").unwrap();
3612 assert_eq!(
3613 btc_balance.free,
3614 Decimal::from_str_radix("1.5", 10).unwrap()
3615 );
3616 assert_eq!(
3617 btc_balance.used,
3618 Decimal::from_str_radix("0.5", 10).unwrap()
3619 );
3620 assert_eq!(
3621 btc_balance.total,
3622 Decimal::from_str_radix("2.0", 10).unwrap()
3623 );
3624 }
3625
3626 #[test]
3627 fn test_parse_market_with_filters() {
3628 let data = json!({
3629 "symbol": "ETHUSDT",
3630 "baseAsset": "ETH",
3631 "quoteAsset": "USDT",
3632 "status": "TRADING",
3633 "filters": [
3634 {
3635 "filterType": "PRICE_FILTER",
3636 "tickSize": "0.01",
3637 "minPrice": "0.01",
3638 "maxPrice": "1000000.00"
3639 },
3640 {
3641 "filterType": "LOT_SIZE",
3642 "stepSize": "0.0001",
3643 "minQty": "0.0001",
3644 "maxQty": "90000"
3645 },
3646 {
3647 "filterType": "MIN_NOTIONAL",
3648 "minNotional": "10.0"
3649 },
3650 {
3651 "filterType": "MARKET_LOT_SIZE",
3652 "stepSize": "0.0001",
3653 "minQty": "0.0001",
3654 "maxQty": "50000"
3655 }
3656 ]
3657 });
3658
3659 let market = parse_market(&data).unwrap();
3660 assert_eq!(market.symbol, "ETH/USDT");
3661 assert!(market.limits.amount.is_some());
3662 assert!(market.limits.amount.as_ref().unwrap().min.is_some());
3663 assert!(market.limits.amount.as_ref().unwrap().max.is_some());
3664 assert!(market.limits.price.is_some());
3665 assert!(market.limits.price.as_ref().unwrap().min.is_some());
3666 assert!(market.limits.price.as_ref().unwrap().max.is_some());
3667 assert_eq!(
3668 market.limits.cost.as_ref().unwrap().min,
3669 Some(Decimal::from_str_radix("10.0", 10).unwrap())
3670 );
3671 }
3672
3673 #[test]
3674 fn test_parse_ticker_edge_cases() {
3675 let data = json!({
3676 "symbol": "BTCUSDT",
3677 "lastPrice": "50000.00",
3678 "closeTime": 1609459200000u64
3679 });
3680
3681 let ticker = parse_ticker(&data, None).unwrap();
3682 assert_eq!(
3683 ticker.last,
3684 Some(Price::new(Decimal::from_str_radix("50000.00", 10).unwrap()))
3685 );
3686 assert_eq!(ticker.symbol, "BTCUSDT");
3687 assert_eq!(ticker.bid, None);
3688 assert_eq!(ticker.ask, None);
3689 }
3690
3691 #[test]
3692 fn test_parse_trade_timestamp() {
3693 let data = json!({
3694 "id": 99999,
3695 "price": "45000.50",
3696 "qty": "1.25",
3697 "time": 1609459200000u64,
3698 "isBuyerMaker": true,
3699 "symbol": "BTCUSDT"
3700 });
3701
3702 let trade = parse_trade(&data, None).unwrap();
3703 assert_eq!(trade.timestamp, 1609459200000);
3704 assert_eq!(trade.side, OrderSide::Sell);
3705 }
3706
3707 #[test]
3708 fn test_parse_order_status() {
3709 let statuses = vec![
3710 ("NEW", "open"),
3711 ("PARTIALLY_FILLED", "open"),
3712 ("FILLED", "closed"),
3713 ("CANCELED", "canceled"),
3714 ("REJECTED", "rejected"),
3715 ("EXPIRED", "expired"),
3716 ];
3717
3718 for (binance_status, expected_status) in statuses {
3719 let data = json!({
3720 "orderId": 123,
3721 "symbol": "BTCUSDT",
3722 "status": binance_status,
3723 "side": "BUY",
3724 "type": "LIMIT",
3725 "price": "50000.00",
3726 "origQty": "1.0",
3727 "executedQty": "0.0",
3728 "time": 1609459200000u64
3729 });
3730
3731 let order = parse_order(&data, None).unwrap();
3732 let status_enum = match expected_status {
3734 "open" => OrderStatus::Open,
3735 "closed" => OrderStatus::Closed,
3736 "canceled" | "cancelled" => OrderStatus::Canceled,
3737 "expired" => OrderStatus::Expired,
3738 "rejected" => OrderStatus::Rejected,
3739 _ => OrderStatus::Open,
3740 };
3741 assert_eq!(order.status, status_enum);
3742 }
3743 }
3744
3745 #[test]
3746 fn test_parse_balance_locked() {
3747 let data = json!({
3748 "balances": [
3749 {
3750 "asset": "USDT",
3751 "free": "10000.50",
3752 "locked": "500.25"
3753 }
3754 ]
3755 });
3756
3757 let balance = parse_balance(&data).unwrap();
3758 let usdt_balance = balance.balances.get("USDT").unwrap();
3759 assert_eq!(
3760 usdt_balance.free,
3761 Decimal::from_str_radix("10000.50", 10).unwrap()
3762 );
3763 assert_eq!(
3764 usdt_balance.used,
3765 Decimal::from_str_radix("500.25", 10).unwrap()
3766 );
3767 assert_eq!(
3768 usdt_balance.total,
3769 Decimal::from_str_radix("10500.75", 10).unwrap()
3770 );
3771 }
3772
3773 #[test]
3774 fn test_parse_empty_response() {
3775 let data = json!({
3776 "lastUpdateId": 12345,
3777 "bids": [],
3778 "asks": []
3779 });
3780
3781 let orderbook = parse_orderbook(&data, "BTC/USDT".to_string()).unwrap();
3782 assert_eq!(orderbook.bids.len(), 0);
3783 assert_eq!(orderbook.asks.len(), 0);
3784 }
3785
3786 #[test]
3787 fn test_currency_precision() {
3788 let data = json!({
3789 "symbol": "BTCUSDT",
3790 "baseAsset": "BTC",
3791 "quoteAsset": "USDT",
3792 "status": "TRADING",
3793 "filters": [
3794 {
3795 "filterType": "LOT_SIZE",
3796 "stepSize": "0.00000001"
3797 },
3798 {
3799 "filterType": "PRICE_FILTER",
3800 "tickSize": "0.01"
3801 }
3802 ]
3803 });
3804
3805 let market = parse_market(&data).unwrap();
3806 assert_eq!(
3807 market.precision.amount,
3808 Some(Decimal::from_str_radix("0.00000001", 10).unwrap())
3809 );
3810 assert_eq!(
3811 market.precision.price,
3812 Some(Decimal::from_str_radix("0.01", 10).unwrap())
3813 );
3814 }
3815
3816 #[test]
3817 fn test_market_limits() {
3818 let data = json!({
3819 "symbol": "ETHBTC",
3820 "baseAsset": "ETH",
3821 "quoteAsset": "BTC",
3822 "status": "TRADING",
3823 "filters": [
3824 {
3825 "filterType": "LOT_SIZE",
3826 "minQty": "0.001",
3827 "maxQty": "100000",
3828 "stepSize": "0.001"
3829 },
3830 {
3831 "filterType": "PRICE_FILTER",
3832 "minPrice": "0.00000100",
3833 "maxPrice": "100000.00000000",
3834 "tickSize": "0.00000100"
3835 },
3836 {
3837 "filterType": "MIN_NOTIONAL",
3838 "minNotional": "0.0001"
3839 }
3840 ]
3841 });
3842
3843 let market = parse_market(&data).unwrap();
3844 assert_eq!(
3845 market.limits.amount.as_ref().unwrap().min,
3846 Some(Decimal::from_str_radix("0.001", 10).unwrap())
3847 );
3848 assert_eq!(
3849 market.limits.amount.as_ref().unwrap().max,
3850 Some(Decimal::from_str_radix("100000.0", 10).unwrap())
3851 );
3852 assert_eq!(
3853 market.limits.price.as_ref().unwrap().min,
3854 Some(Decimal::from_str_radix("0.000001", 10).unwrap())
3855 );
3856 assert_eq!(
3857 market.limits.price.as_ref().unwrap().max,
3858 Some(Decimal::from_str_radix("100000.0", 10).unwrap())
3859 );
3860 assert_eq!(
3861 market.limits.cost.as_ref().unwrap().min,
3862 Some(Decimal::from_str_radix("0.0001", 10).unwrap())
3863 );
3864 }
3865
3866 #[test]
3867 fn test_symbol_normalization() {
3868 let symbols = vec![
3869 ("BTCUSDT", "BTC/USDT"),
3870 ("ETHBTC", "ETH/BTC"),
3871 ("BNBBUSD", "BNB/BUSD"),
3872 ];
3873
3874 for (binance_symbol, ccxt_symbol) in symbols {
3875 let data = json!({
3876 "symbol": binance_symbol,
3877 "baseAsset": &ccxt_symbol[..ccxt_symbol.find('/').unwrap()],
3878 "quoteAsset": &ccxt_symbol[ccxt_symbol.find('/').unwrap() + 1..],
3879 "status": "TRADING"
3880 });
3881
3882 let market = parse_market(&data).unwrap();
3883 assert_eq!(market.symbol, ccxt_symbol);
3884 }
3885 }
3886
3887 #[test]
3888 fn test_timeframe_conversion() {
3889 let timeframes = vec![
3890 ("1m", 60000),
3891 ("5m", 300000),
3892 ("15m", 900000),
3893 ("1h", 3600000),
3894 ("4h", 14400000),
3895 ("1d", 86400000),
3896 ];
3897
3898 for (tf_str, expected_ms) in timeframes {
3899 let ms = match tf_str {
3900 "1m" => 60000,
3901 "5m" => 300000,
3902 "15m" => 900000,
3903 "1h" => 3600000,
3904 "4h" => 14400000,
3905 "1d" => 86400000,
3906 _ => 0,
3907 };
3908 assert_eq!(ms, expected_ms);
3909 }
3910 }
3911}
3912
3913pub fn is_fiat_currency(currency: &str) -> bool {
3927 matches!(
3928 currency.to_uppercase().as_str(),
3929 "USD" | "EUR" | "GBP" | "JPY" | "CNY" | "KRW" | "AUD" | "CAD" | "CHF" | "HKD" | "SGD"
3930 )
3931}
3932
3933pub fn extract_internal_transfer_id(txid: &str) -> String {
3945 const PREFIX: &str = "Internal transfer ";
3946 if txid.starts_with(PREFIX) {
3947 txid[PREFIX.len()..].to_string()
3948 } else {
3949 txid.to_string()
3950 }
3951}
3952
3953pub fn parse_transaction_status_by_type(
3966 status_value: &Value,
3967 is_deposit: bool,
3968) -> ccxt_core::types::TransactionStatus {
3969 use ccxt_core::types::TransactionStatus;
3970
3971 if let Some(status_int) = status_value.as_i64() {
3972 if is_deposit {
3973 match status_int {
3974 0 => TransactionStatus::Pending,
3975 1 => TransactionStatus::Ok,
3976 6 => TransactionStatus::Ok,
3977 _ => TransactionStatus::Pending,
3978 }
3979 } else {
3980 match status_int {
3981 0 => TransactionStatus::Pending,
3982 1 => TransactionStatus::Canceled,
3983 2 => TransactionStatus::Pending,
3984 3 => TransactionStatus::Failed,
3985 4 => TransactionStatus::Pending,
3986 5 => TransactionStatus::Failed,
3987 6 => TransactionStatus::Ok,
3988 _ => TransactionStatus::Pending,
3989 }
3990 }
3991 } else if let Some(status_str) = status_value.as_str() {
3992 match status_str {
3993 "Processing" => TransactionStatus::Pending,
3994 "Failed" => TransactionStatus::Failed,
3995 "Successful" => TransactionStatus::Ok,
3996 "Refunding" => TransactionStatus::Canceled,
3997 "Refunded" => TransactionStatus::Canceled,
3998 "Refund Failed" => TransactionStatus::Failed,
3999 _ => TransactionStatus::Pending,
4000 }
4001 } else {
4002 TransactionStatus::Pending
4003 }
4004}
4005
4006pub fn parse_transaction(
4054 data: &Value,
4055 transaction_type: ccxt_core::types::TransactionType,
4056) -> Result<ccxt_core::types::Transaction> {
4057 use ccxt_core::types::{Transaction, TransactionFee, TransactionStatus, TransactionType};
4058
4059 let is_deposit = matches!(transaction_type, TransactionType::Deposit);
4060
4061 let id = if is_deposit {
4063 data["id"]
4064 .as_str()
4065 .or_else(|| data["orderNo"].as_str())
4066 .unwrap_or("")
4067 .to_string()
4068 } else {
4069 data["id"]
4070 .as_str()
4071 .or_else(|| data["withdrawOrderId"].as_str())
4072 .unwrap_or("")
4073 .to_string()
4074 };
4075
4076 let currency = data["coin"]
4078 .as_str()
4079 .or_else(|| data["fiatCurrency"].as_str())
4080 .unwrap_or("")
4081 .to_string();
4082
4083 let amount = data["amount"]
4084 .as_str()
4085 .and_then(|s| Decimal::from_str(s).ok())
4086 .unwrap_or_else(|| Decimal::ZERO);
4087
4088 let fee = if is_deposit {
4089 None
4090 } else {
4091 data["transactionFee"]
4092 .as_str()
4093 .or_else(|| data["totalFee"].as_str())
4094 .and_then(|s| Decimal::from_str(s).ok())
4095 .map(|cost| TransactionFee {
4096 currency: currency.clone(),
4097 cost,
4098 })
4099 };
4100
4101 let timestamp = if is_deposit {
4102 data["insertTime"].as_i64()
4103 } else {
4104 data["createTime"].as_i64().or_else(|| {
4105 data["applyTime"].as_str().and_then(|s| {
4106 chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
4107 .ok()
4108 .map(|dt| dt.and_utc().timestamp_millis())
4109 })
4110 })
4111 };
4112
4113 let datetime = timestamp.and_then(|ts| ccxt_core::time::iso8601(ts).ok());
4114
4115 let network = data["network"].as_str().map(|s| s.to_string());
4116
4117 let address = data["address"]
4118 .as_str()
4119 .or_else(|| data["depositAddress"].as_str())
4120 .map(|s| s.to_string());
4121
4122 let tag = data["addressTag"]
4123 .as_str()
4124 .or_else(|| data["tag"].as_str())
4125 .filter(|s| !s.is_empty())
4126 .map(|s| s.to_string());
4127
4128 let mut txid = data["txId"]
4129 .as_str()
4130 .or_else(|| data["hash"].as_str())
4131 .map(|s| s.to_string());
4132
4133 let transfer_type = data["transferType"].as_i64();
4134 let is_internal = transfer_type == Some(1);
4135
4136 if is_internal {
4137 if let Some(ref tx) = txid {
4138 txid = Some(extract_internal_transfer_id(tx));
4139 }
4140 }
4141
4142 let status = if let Some(status_value) = data.get("status") {
4143 parse_transaction_status_by_type(status_value, is_deposit)
4144 } else {
4145 TransactionStatus::Pending
4146 };
4147
4148 let updated = data["updateTime"].as_i64();
4149
4150 let comment = data["info"]
4151 .as_str()
4152 .or_else(|| data["comment"].as_str())
4153 .map(|s| s.to_string());
4154
4155 Ok(Transaction {
4156 info: Some(data.clone()),
4157 id,
4158 txid,
4159 timestamp,
4160 datetime,
4161 network,
4162 address: address.clone(),
4163 address_to: if is_deposit { address.clone() } else { None },
4164 address_from: if !is_deposit { address } else { None },
4165 tag: tag.clone(),
4166 tag_to: if is_deposit { tag.clone() } else { None },
4167 tag_from: if !is_deposit { tag } else { None },
4168 transaction_type,
4169 amount,
4170 currency,
4171 status,
4172 updated,
4173 internal: Some(is_internal),
4174 comment,
4175 fee,
4176 })
4177}
4178
4179pub fn parse_deposit_address(data: &Value) -> Result<ccxt_core::types::DepositAddress> {
4200 use ccxt_core::types::DepositAddress;
4201
4202 let currency = data["coin"]
4203 .as_str()
4204 .ok_or_else(|| Error::from(ParseError::missing_field("coin")))?
4205 .to_string();
4206
4207 let address = data["address"]
4208 .as_str()
4209 .ok_or_else(|| Error::from(ParseError::missing_field("address")))?
4210 .to_string();
4211
4212 let network = data["network"].as_str().map(|s| s.to_string()).or_else(|| {
4213 data["url"].as_str().and_then(|url| {
4214 if url.contains("btc.com") {
4215 Some("BTC".to_string())
4216 } else if url.contains("etherscan.io") {
4217 Some("ETH".to_string())
4218 } else if url.contains("tronscan.org") {
4219 Some("TRX".to_string())
4220 } else {
4221 None
4222 }
4223 })
4224 });
4225
4226 let tag = data["tag"]
4227 .as_str()
4228 .or_else(|| data["addressTag"].as_str())
4229 .filter(|s| !s.is_empty())
4230 .map(|s| s.to_string());
4231
4232 Ok(DepositAddress {
4233 info: Some(data.clone()),
4234 currency,
4235 network,
4236 address,
4237 tag,
4238 })
4239}
4240pub fn parse_currency(data: &Value) -> Result<ccxt_core::types::Currency> {
4285 use ccxt_core::types::{Currency, CurrencyNetwork, MinMax};
4286
4287 let code = data["coin"]
4288 .as_str()
4289 .ok_or_else(|| Error::from(ParseError::missing_field("coin")))?
4290 .to_string();
4291
4292 let id = code.clone();
4293 let name = data["name"].as_str().map(|s| s.to_string());
4294
4295 let active = data["trading"].as_bool().unwrap_or(true);
4296
4297 let mut networks = HashMap::new();
4298 let mut global_deposit = false;
4299 let mut global_withdraw = false;
4300 let mut global_fee = None;
4301 let mut global_precision = None;
4302 let mut global_limits = MinMax::default();
4303
4304 if let Some(network_list) = data["networkList"].as_array() {
4305 for network_data in network_list {
4306 let network_id = network_data["network"]
4307 .as_str()
4308 .unwrap_or(&code)
4309 .to_string();
4310
4311 let is_default = network_data["isDefault"].as_bool().unwrap_or(false);
4312 let deposit_enable = network_data["depositEnable"].as_bool().unwrap_or(false);
4313 let withdraw_enable = network_data["withdrawEnable"].as_bool().unwrap_or(false);
4314
4315 if is_default {
4316 global_deposit = deposit_enable;
4317 global_withdraw = withdraw_enable;
4318 }
4319
4320 let fee = network_data["withdrawFee"]
4321 .as_str()
4322 .and_then(|s| Decimal::from_str(s).ok());
4323
4324 if is_default && fee.is_some() {
4325 global_fee = fee;
4326 }
4327
4328 let precision = network_data["withdrawIntegerMultiple"]
4329 .as_str()
4330 .and_then(|s| Decimal::from_str(s).ok());
4331
4332 if is_default && precision.is_some() {
4333 global_precision = precision;
4334 }
4335
4336 let withdraw_min = network_data["withdrawMin"]
4337 .as_str()
4338 .and_then(|s| Decimal::from_str(s).ok());
4339
4340 let withdraw_max = network_data["withdrawMax"]
4341 .as_str()
4342 .and_then(|s| Decimal::from_str(s).ok());
4343
4344 let limits = MinMax {
4345 min: withdraw_min,
4346 max: withdraw_max,
4347 };
4348
4349 if is_default {
4350 global_limits = limits.clone();
4351 }
4352
4353 let network = CurrencyNetwork {
4354 network: network_id.clone(),
4355 id: Some(network_id.clone()),
4356 name: network_data["name"].as_str().map(|s| s.to_string()),
4357 active: deposit_enable && withdraw_enable,
4358 deposit: deposit_enable,
4359 withdraw: withdraw_enable,
4360 fee,
4361 precision,
4362 limits,
4363 info: value_to_hashmap(network_data),
4364 };
4365
4366 networks.insert(network_id, network);
4367 }
4368 }
4369
4370 Ok(Currency {
4371 code,
4372 id,
4373 name,
4374 active,
4375 deposit: global_deposit,
4376 withdraw: global_withdraw,
4377 fee: global_fee,
4378 precision: global_precision,
4379 limits: global_limits,
4380 networks,
4381 currency_type: if data["isLegalMoney"].as_bool().unwrap_or(false) {
4382 Some("fiat".to_string())
4383 } else {
4384 Some("crypto".to_string())
4385 },
4386 info: value_to_hashmap(data),
4387 })
4388}
4389
4390pub fn parse_currencies(data: &Value) -> Result<Vec<ccxt_core::types::Currency>> {
4400 if let Some(array) = data.as_array() {
4401 array.iter().map(parse_currency).collect()
4402 } else {
4403 Ok(vec![parse_currency(data)?])
4404 }
4405}
4406
4407pub fn parse_stats_24hr(data: &Value) -> Result<ccxt_core::types::Stats24hr> {
4444 use ccxt_core::types::Stats24hr;
4445
4446 let symbol = data["symbol"]
4447 .as_str()
4448 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
4449 .to_string();
4450
4451 let price_change = data["priceChange"]
4452 .as_str()
4453 .and_then(|s| Decimal::from_str(s).ok());
4454
4455 let price_change_percent = data["priceChangePercent"]
4456 .as_str()
4457 .and_then(|s| Decimal::from_str(s).ok());
4458
4459 let weighted_avg_price = data["weightedAvgPrice"]
4460 .as_str()
4461 .and_then(|s| Decimal::from_str(s).ok());
4462
4463 let prev_close_price = data["prevClosePrice"]
4464 .as_str()
4465 .and_then(|s| Decimal::from_str(s).ok());
4466
4467 let last_price = data["lastPrice"]
4468 .as_str()
4469 .and_then(|s| Decimal::from_str(s).ok());
4470
4471 let last_qty = data["lastQty"]
4472 .as_str()
4473 .and_then(|s| Decimal::from_str(s).ok());
4474
4475 let bid_price = data["bidPrice"]
4476 .as_str()
4477 .and_then(|s| Decimal::from_str(s).ok());
4478
4479 let bid_qty = data["bidQty"]
4480 .as_str()
4481 .and_then(|s| Decimal::from_str(s).ok());
4482
4483 let ask_price = data["askPrice"]
4484 .as_str()
4485 .and_then(|s| Decimal::from_str(s).ok());
4486
4487 let ask_qty = data["askQty"]
4488 .as_str()
4489 .and_then(|s| Decimal::from_str(s).ok());
4490
4491 let open_price = data["openPrice"]
4493 .as_str()
4494 .and_then(|s| Decimal::from_str(s).ok());
4495
4496 let high_price = data["highPrice"]
4497 .as_str()
4498 .and_then(|s| Decimal::from_str(s).ok());
4499
4500 let low_price = data["lowPrice"]
4501 .as_str()
4502 .and_then(|s| Decimal::from_str(s).ok());
4503
4504 let volume = data["volume"]
4506 .as_str()
4507 .and_then(|s| Decimal::from_str(s).ok());
4508
4509 let quote_volume = data["quoteVolume"]
4510 .as_str()
4511 .and_then(|s| Decimal::from_str(s).ok());
4512
4513 let open_time = data["openTime"].as_i64();
4514 let close_time = data["closeTime"].as_i64();
4515 let first_id = data["firstId"].as_i64();
4516 let last_id = data["lastId"].as_i64();
4517 let count = data["count"].as_i64();
4518
4519 Ok(Stats24hr {
4520 symbol,
4521 price_change,
4522 price_change_percent,
4523 weighted_avg_price,
4524 prev_close_price,
4525 last_price,
4526 last_qty,
4527 bid_price,
4528 bid_qty,
4529 ask_price,
4530 ask_qty,
4531 open_price,
4532 high_price,
4533 low_price,
4534 volume,
4535 quote_volume,
4536 open_time,
4537 close_time,
4538 first_id,
4539 last_id,
4540 count,
4541 info: value_to_hashmap(data),
4542 })
4543}
4544
4545pub fn parse_agg_trade(data: &Value, symbol: Option<String>) -> Result<ccxt_core::types::AggTrade> {
4570 use ccxt_core::types::AggTrade;
4571
4572 let agg_id = data["a"]
4573 .as_i64()
4574 .ok_or_else(|| Error::from(ParseError::missing_field("a")))?;
4575
4576 let price = data["p"]
4577 .as_str()
4578 .and_then(|s| Decimal::from_str(s).ok())
4579 .ok_or_else(|| Error::from(ParseError::missing_field("p")))?;
4580
4581 let quantity = data["q"]
4582 .as_str()
4583 .and_then(|s| Decimal::from_str(s).ok())
4584 .ok_or_else(|| Error::from(ParseError::missing_field("q")))?;
4585
4586 let first_trade_id = data["f"]
4587 .as_i64()
4588 .ok_or_else(|| Error::from(ParseError::missing_field("f")))?;
4589
4590 let last_trade_id = data["l"]
4591 .as_i64()
4592 .ok_or_else(|| Error::from(ParseError::missing_field("l")))?;
4593
4594 let timestamp = data["T"]
4595 .as_i64()
4596 .ok_or_else(|| Error::from(ParseError::missing_field("T")))?;
4597
4598 let is_buyer_maker = data["m"].as_bool().unwrap_or(false);
4599 let is_best_match = data["M"].as_bool();
4600
4601 Ok(AggTrade {
4602 agg_id,
4603 price,
4604 quantity,
4605 first_trade_id,
4606 last_trade_id,
4607 timestamp,
4608 is_buyer_maker,
4609 is_best_match,
4610 symbol,
4611 })
4612}
4613
4614pub fn parse_trading_limits(
4650 data: &Value,
4651 _symbol: String,
4652) -> Result<ccxt_core::types::TradingLimits> {
4653 use ccxt_core::types::{MinMax, TradingLimits};
4654
4655 let mut price_limits = MinMax::default();
4656 let mut amount_limits = MinMax::default();
4657 let mut cost_limits = MinMax::default();
4658
4659 if let Some(filters) = data["filters"].as_array() {
4660 for filter in filters {
4661 let filter_type = filter["filterType"].as_str().unwrap_or("");
4662
4663 match filter_type {
4664 "PRICE_FILTER" => {
4665 price_limits.min = filter["minPrice"]
4666 .as_str()
4667 .and_then(|s| Decimal::from_str(s).ok());
4668 price_limits.max = filter["maxPrice"]
4669 .as_str()
4670 .and_then(|s| Decimal::from_str(s).ok());
4671 }
4672 "LOT_SIZE" => {
4673 amount_limits.min = filter["minQty"]
4674 .as_str()
4675 .and_then(|s| Decimal::from_str(s).ok());
4676 amount_limits.max = filter["maxQty"]
4677 .as_str()
4678 .and_then(|s| Decimal::from_str(s).ok());
4679 }
4680 "MIN_NOTIONAL" | "NOTIONAL" => {
4681 cost_limits.min = filter["minNotional"]
4682 .as_str()
4683 .and_then(|s| Decimal::from_str(s).ok());
4684 }
4685 _ => {}
4686 }
4687 }
4688 }
4689
4690 Ok(TradingLimits {
4691 min: None,
4692 max: None,
4693 amount: Some(amount_limits),
4694 price: Some(price_limits),
4695 cost: Some(cost_limits),
4696 })
4697}
4698pub fn parse_leverage_tier(data: &Value, market: &Market) -> Result<LeverageTier> {
4709 let tier = data["bracket"]
4710 .as_i64()
4711 .or_else(|| data["tier"].as_i64())
4712 .unwrap_or(0) as i32;
4713
4714 let min_notional = parse_decimal(data, "notionalFloor")
4715 .or_else(|| parse_decimal(data, "minNotional"))
4716 .unwrap_or(Decimal::ZERO);
4717
4718 let max_notional = parse_decimal(data, "notionalCap")
4719 .or_else(|| parse_decimal(data, "maxNotional"))
4720 .unwrap_or(Decimal::MAX);
4721
4722 let maintenance_margin_rate = parse_decimal(data, "maintMarginRatio")
4723 .or_else(|| parse_decimal(data, "maintenanceMarginRate"))
4724 .unwrap_or(Decimal::ZERO);
4725
4726 let max_leverage = data["initialLeverage"]
4727 .as_i64()
4728 .or_else(|| data["maxLeverage"].as_i64())
4729 .unwrap_or(1) as i32;
4730
4731 Ok(LeverageTier {
4732 info: data.clone(),
4733 tier,
4734 symbol: market.symbol.clone(),
4735 currency: market.quote.clone(),
4736 min_notional,
4737 max_notional,
4738 maintenance_margin_rate,
4739 max_leverage,
4740 })
4741}
4742
4743pub fn parse_isolated_borrow_rates(
4753 data: &Value,
4754) -> Result<std::collections::HashMap<String, ccxt_core::types::IsolatedBorrowRate>> {
4755 use ccxt_core::types::IsolatedBorrowRate;
4756 use std::collections::HashMap;
4757
4758 let mut rates_map = HashMap::new();
4759
4760 if let Some(array) = data.as_array() {
4761 for item in array {
4762 let symbol = item["symbol"].as_str().unwrap_or("");
4763 let base = item["base"].as_str().unwrap_or("");
4764 let quote = item["quote"].as_str().unwrap_or("");
4765
4766 let base_rate = item["dailyInterestRate"]
4767 .as_str()
4768 .and_then(|s| s.parse::<f64>().ok())
4769 .unwrap_or(0.0);
4770
4771 let quote_rate = item["quoteDailyInterestRate"]
4772 .as_str()
4773 .and_then(|s| s.parse::<f64>().ok())
4774 .or_else(|| {
4775 item["dailyInterestRate"]
4776 .as_str()
4777 .and_then(|s| s.parse::<f64>().ok())
4778 })
4779 .unwrap_or(0.0);
4780
4781 let timestamp = item["timestamp"].as_i64().or_else(|| item["time"].as_i64());
4782
4783 let datetime = timestamp.and_then(|ts| {
4784 chrono::DateTime::from_timestamp_millis(ts).map(|dt| dt.to_rfc3339())
4785 });
4786
4787 let isolated_rate = IsolatedBorrowRate {
4788 symbol: symbol.to_string(),
4789 base: base.to_string(),
4790 base_rate,
4791 quote: quote.to_string(),
4792 quote_rate,
4793 period: 86400000, timestamp,
4795 datetime,
4796 info: item.clone(),
4797 };
4798
4799 rates_map.insert(symbol.to_string(), isolated_rate);
4800 }
4801 }
4802
4803 Ok(rates_map)
4804}
4805
4806pub fn parse_borrow_interests(data: &Value) -> Result<Vec<BorrowInterest>> {
4816 let mut interests = Vec::new();
4817
4818 if let Some(array) = data.as_array() {
4819 for item in array {
4820 match parse_borrow_interest(item) {
4821 Ok(interest) => interests.push(interest),
4822 Err(e) => {
4823 eprintln!("Failed to parse borrow interest: {}", e);
4824 }
4825 }
4826 }
4827 }
4828
4829 Ok(interests)
4830}
4831
4832pub fn parse_borrow_rate_history(data: &Value, currency: &str) -> Result<BorrowRateHistory> {
4843 let timestamp = data["timestamp"]
4844 .as_i64()
4845 .or_else(|| data["time"].as_i64())
4846 .unwrap_or(0);
4847
4848 let rate = data["hourlyInterestRate"]
4849 .as_str()
4850 .or_else(|| data["dailyInterestRate"].as_str())
4851 .or_else(|| data["rate"].as_str())
4852 .and_then(|s| s.parse::<f64>().ok())
4853 .or_else(|| data["hourlyInterestRate"].as_f64())
4854 .or_else(|| data["dailyInterestRate"].as_f64())
4855 .or_else(|| data["rate"].as_f64())
4856 .unwrap_or(0.0);
4857
4858 let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
4859 .map(|dt| dt.to_rfc3339())
4860 .unwrap_or_default();
4861
4862 let symbol = data["symbol"].as_str().map(|s| s.to_string());
4863 let vip_level = data["vipLevel"].as_i64().map(|v| v as i32);
4864
4865 Ok(BorrowRateHistory {
4866 currency: currency.to_string(),
4867 symbol,
4868 rate,
4869 timestamp,
4870 datetime,
4871 vip_level,
4872 info: data.clone(),
4873 })
4874}
4875
4876pub fn parse_ledger_entry(data: &Value) -> Result<LedgerEntry> {
4886 let id = data["tranId"]
4887 .as_i64()
4888 .or_else(|| data["id"].as_i64())
4889 .map(|v| v.to_string())
4890 .or_else(|| data["tranId"].as_str().map(|s| s.to_string()))
4891 .or_else(|| data["id"].as_str().map(|s| s.to_string()))
4892 .unwrap_or_default();
4893
4894 let currency = data["asset"]
4895 .as_str()
4896 .or_else(|| data["currency"].as_str())
4897 .unwrap_or("")
4898 .to_string();
4899
4900 let amount = data["amount"]
4901 .as_str()
4902 .and_then(|s| s.parse::<f64>().ok())
4903 .or_else(|| data["amount"].as_f64())
4904 .or_else(|| data["qty"].as_str().and_then(|s| s.parse::<f64>().ok()))
4905 .or_else(|| data["qty"].as_f64())
4906 .unwrap_or(0.0);
4907
4908 let timestamp = data["timestamp"]
4909 .as_i64()
4910 .or_else(|| data["time"].as_i64())
4911 .unwrap_or(0);
4912
4913 let type_str = data["type"].as_str().unwrap_or("");
4914 let (direction, entry_type) = match type_str {
4915 "DEPOSIT" => (LedgerDirection::In, LedgerEntryType::Deposit),
4916 "WITHDRAW" => (LedgerDirection::Out, LedgerEntryType::Withdrawal),
4917 "BUY" | "SELL" => (
4918 if amount >= 0.0 {
4919 LedgerDirection::In
4920 } else {
4921 LedgerDirection::Out
4922 },
4923 LedgerEntryType::Trade,
4924 ),
4925 "FEE" => (LedgerDirection::Out, LedgerEntryType::Fee),
4926 "REBATE" => (LedgerDirection::In, LedgerEntryType::Rebate),
4927 "TRANSFER" => (
4928 if amount >= 0.0 {
4929 LedgerDirection::In
4930 } else {
4931 LedgerDirection::Out
4932 },
4933 LedgerEntryType::Transfer,
4934 ),
4935 _ => (
4936 if amount >= 0.0 {
4937 LedgerDirection::In
4938 } else {
4939 LedgerDirection::Out
4940 },
4941 LedgerEntryType::Trade,
4942 ),
4943 };
4944
4945 let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
4946 .map(|dt| dt.to_rfc3339())
4947 .unwrap_or_default();
4948
4949 Ok(LedgerEntry {
4950 id,
4951 currency,
4952 account: None,
4953 reference_account: None,
4954 reference_id: None,
4955 type_: entry_type,
4956 direction,
4957 amount: amount.abs(),
4958 timestamp,
4959 datetime,
4960 before: None,
4961 after: None,
4962 status: None,
4963 fee: None,
4964 info: data.clone(),
4965 })
4966}
4967
4968#[cfg(test)]
4969mod transaction_tests {
4970 use super::*;
4971 use ccxt_core::types::{TransactionStatus, TransactionType};
4972 use serde_json::json;
4973
4974 #[test]
4975 fn test_is_fiat_currency() {
4976 assert!(is_fiat_currency("USD"));
4977 assert!(is_fiat_currency("eur"));
4978 assert!(is_fiat_currency("CNY"));
4979 assert!(!is_fiat_currency("BTC"));
4980 assert!(!is_fiat_currency("ETH"));
4981 }
4982
4983 #[test]
4984 fn test_extract_internal_transfer_id() {
4985 assert_eq!(
4986 extract_internal_transfer_id("Internal transfer 123456"),
4987 "123456"
4988 );
4989 assert_eq!(
4990 extract_internal_transfer_id("normal_hash_abc"),
4991 "normal_hash_abc"
4992 );
4993 }
4994
4995 #[test]
4996 fn test_parse_transaction_status_deposit() {
4997 assert_eq!(
4998 parse_transaction_status_by_type(&json!(0), true),
4999 TransactionStatus::Pending
5000 );
5001 assert_eq!(
5002 parse_transaction_status_by_type(&json!(1), true),
5003 TransactionStatus::Ok
5004 );
5005 assert_eq!(
5006 parse_transaction_status_by_type(&json!(6), true),
5007 TransactionStatus::Ok
5008 );
5009 assert_eq!(
5010 parse_transaction_status_by_type(&json!("Processing"), true),
5011 TransactionStatus::Pending
5012 );
5013 assert_eq!(
5014 parse_transaction_status_by_type(&json!("Successful"), true),
5015 TransactionStatus::Ok
5016 );
5017 }
5018
5019 #[test]
5020 fn test_parse_transaction_status_withdrawal() {
5021 assert_eq!(
5022 parse_transaction_status_by_type(&json!(0), false),
5023 TransactionStatus::Pending
5024 );
5025 assert_eq!(
5026 parse_transaction_status_by_type(&json!(1), false),
5027 TransactionStatus::Canceled
5028 );
5029 assert_eq!(
5030 parse_transaction_status_by_type(&json!(6), false),
5031 TransactionStatus::Ok
5032 );
5033 }
5034
5035 #[test]
5036 fn test_parse_deposit_transaction() {
5037 let data = json!({
5038 "id": "deposit123",
5039 "amount": "0.5",
5040 "coin": "BTC",
5041 "network": "BTC",
5042 "status": 1,
5043 "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
5044 "addressTag": "",
5045 "txId": "hash123abc",
5046 "insertTime": 1609459200000i64,
5047 "transferType": 0
5048 });
5049
5050 let tx = parse_transaction(&data, TransactionType::Deposit).unwrap();
5051 assert_eq!(tx.id, "deposit123");
5052 assert_eq!(tx.currency, "BTC");
5053 assert_eq!(tx.amount, Decimal::from_str("0.5").unwrap());
5054 assert_eq!(tx.status, TransactionStatus::Ok);
5055 assert_eq!(tx.txid, Some("hash123abc".to_string()));
5056 assert_eq!(tx.internal, Some(false));
5057 assert!(tx.is_deposit());
5058 }
5059
5060 #[test]
5061 fn test_parse_withdrawal_transaction() {
5062 let data = json!({
5063 "id": "withdrawal456",
5064 "amount": "0.3",
5065 "transactionFee": "0.0005",
5066 "coin": "BTC",
5067 "status": 6,
5068 "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
5069 "txId": "hash456def",
5070 "applyTime": "2021-01-01 00:00:00",
5071 "network": "BTC",
5072 "transferType": 0
5073 });
5074
5075 let tx = parse_transaction(&data, TransactionType::Withdrawal).unwrap();
5076 assert_eq!(tx.id, "withdrawal456");
5077 assert_eq!(tx.currency, "BTC");
5078 assert_eq!(tx.amount, Decimal::from_str("0.3").unwrap());
5079 assert_eq!(tx.status, TransactionStatus::Ok);
5080 assert!(tx.fee.is_some());
5081 assert_eq!(
5082 tx.fee.as_ref().unwrap().cost,
5083 Decimal::from_str("0.0005").unwrap()
5084 );
5085 assert!(tx.is_withdrawal());
5086 }
5087
5088 #[test]
5089 fn test_parse_internal_transfer() {
5090 let data = json!({
5091 "id": "internal789",
5092 "amount": "1.0",
5093 "coin": "USDT",
5094 "status": 1,
5095 "txId": "Internal transfer 789xyz",
5096 "insertTime": 1609459200000i64,
5097 "transferType": 1
5098 });
5099
5100 let tx = parse_transaction(&data, TransactionType::Deposit).unwrap();
5101 assert_eq!(tx.internal, Some(true));
5102 assert_eq!(tx.txid, Some("789xyz".to_string()));
5103 }
5104
5105 #[test]
5106 fn test_parse_deposit_address() {
5107 let data = json!({
5108 "coin": "BTC",
5109 "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
5110 "tag": "",
5111 "network": "BTC",
5112 "url": "https://btc.com/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
5113 });
5114
5115 let addr = parse_deposit_address(&data).unwrap();
5116 assert_eq!(addr.currency, "BTC");
5117 assert_eq!(addr.address, "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa");
5118 assert_eq!(addr.network, Some("BTC".to_string()));
5119 assert_eq!(addr.tag, None);
5120 }
5121
5122 #[test]
5123 fn test_parse_deposit_address_with_tag() {
5124 let data = json!({
5125 "coin": "XRP",
5126 "address": "rLHzPsX6oXkzU9rKmLwCdxoEFdLQsSz6Xg",
5127 "tag": "123456",
5128 "network": "XRP"
5129 });
5130
5131 let addr = parse_deposit_address(&data).unwrap();
5132 assert_eq!(addr.currency, "XRP");
5133 assert_eq!(addr.tag, Some("123456".to_string()));
5134 }
5135}
5136
5137#[cfg(test)]
5142mod ws_parser_tests {
5143 use super::*;
5144 use serde_json::json;
5145
5146 #[test]
5147 fn test_parse_ws_ticker_24hr() {
5148 let data = json!({
5149 "e": "24hrTicker",
5150 "E": 1609459200000i64,
5151 "s": "BTCUSDT",
5152 "p": "1000.00",
5153 "P": "2.04",
5154 "c": "50000.00",
5155 "o": "49000.00",
5156 "h": "51000.00",
5157 "l": "48500.00",
5158 "v": "1000.5",
5159 "q": "50000000.0",
5160 "b": "49999.00",
5161 "B": "1.5",
5162 "a": "50001.00",
5163 "A": "2.0"
5164 });
5165
5166 let ticker = parse_ws_ticker(&data, None).unwrap();
5167 assert_eq!(ticker.symbol, "BTCUSDT");
5168 assert_eq!(
5169 ticker.last,
5170 Some(Price::new(Decimal::from_str_radix("50000.00", 10).unwrap()))
5171 );
5172 assert_eq!(
5173 ticker.open,
5174 Some(Price::new(Decimal::from_str_radix("49000.00", 10).unwrap()))
5175 );
5176 assert_eq!(
5177 ticker.high,
5178 Some(Price::new(Decimal::from_str_radix("51000.00", 10).unwrap()))
5179 );
5180 assert_eq!(
5181 ticker.low,
5182 Some(Price::new(Decimal::from_str_radix("48500.00", 10).unwrap()))
5183 );
5184 assert_eq!(
5185 ticker.bid,
5186 Some(Price::new(Decimal::from_str_radix("49999.00", 10).unwrap()))
5187 );
5188 assert_eq!(
5189 ticker.ask,
5190 Some(Price::new(Decimal::from_str_radix("50001.00", 10).unwrap()))
5191 );
5192 assert_eq!(ticker.timestamp, 1609459200000);
5193 }
5194
5195 #[test]
5196 fn test_parse_ws_ticker_mark_price() {
5197 let data = json!({
5198 "e": "markPriceUpdate",
5199 "E": 1609459200000i64,
5200 "s": "BTCUSDT",
5201 "p": "50250.50",
5202 "i": "50000.00",
5203 "r": "0.00010000",
5204 "T": 1609459300000i64
5205 });
5206
5207 let ticker = parse_ws_ticker(&data, None).unwrap();
5208 assert_eq!(ticker.symbol, "BTCUSDT");
5209 assert_eq!(
5210 ticker.last,
5211 Some(Price::new(Decimal::from_str_radix("50250.50", 10).unwrap()))
5212 );
5213 assert_eq!(ticker.timestamp, 1609459200000);
5214 }
5215
5216 #[test]
5217 fn test_parse_ws_ticker_book_ticker() {
5218 let data = json!({
5219 "s": "BTCUSDT",
5220 "b": "49999.00",
5221 "B": "1.5",
5222 "a": "50001.00",
5223 "A": "2.0",
5224 "E": 1609459200000i64
5225 });
5226
5227 let ticker = parse_ws_ticker(&data, None).unwrap();
5228 assert_eq!(ticker.symbol, "BTCUSDT");
5229 assert_eq!(
5230 ticker.bid,
5231 Some(Price::new(Decimal::from_str_radix("49999.00", 10).unwrap()))
5232 );
5233 assert_eq!(
5234 ticker.ask,
5235 Some(Price::new(Decimal::from_str_radix("50001.00", 10).unwrap()))
5236 );
5237 assert_eq!(ticker.timestamp, 1609459200000);
5238 }
5239
5240 #[test]
5241 fn test_parse_ws_trade() {
5242 let data = json!({
5243 "e": "trade",
5244 "E": 1609459200000i64,
5245 "s": "BTCUSDT",
5246 "t": 12345,
5247 "p": "50000.00",
5248 "q": "0.5",
5249 "T": 1609459200000i64,
5250 "m": false
5251 });
5252
5253 let trade = parse_ws_trade(&data, None).unwrap();
5254 assert_eq!(trade.id, Some("12345".to_string()));
5255 assert_eq!(trade.symbol, "BTCUSDT");
5256 assert_eq!(
5257 trade.price,
5258 Price::new(Decimal::from_str_radix("50000.00", 10).unwrap())
5259 );
5260 assert_eq!(
5261 trade.amount,
5262 Amount::new(Decimal::from_str_radix("0.5", 10).unwrap())
5263 );
5264 assert_eq!(trade.timestamp, 1609459200000);
5265 assert_eq!(trade.side, OrderSide::Buy); }
5267
5268 #[test]
5269 fn test_parse_ws_trade_agg() {
5270 let data = json!({
5271 "e": "aggTrade",
5272 "E": 1609459200000i64,
5273 "s": "BTCUSDT",
5274 "a": 67890,
5275 "p": "50000.00",
5276 "q": "0.5",
5277 "T": 1609459200000i64,
5278 "m": true
5279 });
5280
5281 let trade = parse_ws_trade(&data, None).unwrap();
5282 assert_eq!(trade.id, Some("67890".to_string()));
5283 assert_eq!(trade.symbol, "BTCUSDT");
5284 assert_eq!(trade.side, OrderSide::Sell); }
5286
5287 #[test]
5288 fn test_parse_ws_orderbook() {
5289 let data = json!({
5290 "e": "depthUpdate",
5291 "E": 1609459200000i64,
5292 "s": "BTCUSDT",
5293 "U": 157,
5294 "u": 160,
5295 "b": [
5296 ["49999.00", "1.5"],
5297 ["49998.00", "2.0"]
5298 ],
5299 "a": [
5300 ["50001.00", "2.0"],
5301 ["50002.00", "1.5"]
5302 ]
5303 });
5304
5305 let orderbook = parse_ws_orderbook(&data, "BTCUSDT".to_string()).unwrap();
5306 assert_eq!(orderbook.symbol, "BTCUSDT");
5307 assert_eq!(orderbook.bids.len(), 2);
5308 assert_eq!(orderbook.asks.len(), 2);
5309 assert_eq!(
5310 orderbook.bids[0].price,
5311 Price::new(Decimal::from_str_radix("49999.00", 10).unwrap())
5312 );
5313 assert_eq!(
5314 orderbook.bids[0].amount,
5315 Amount::new(Decimal::from_str_radix("1.5", 10).unwrap())
5316 );
5317 assert_eq!(
5318 orderbook.asks[0].price,
5319 Price::new(Decimal::from_str_radix("50001.00", 10).unwrap())
5320 );
5321 assert_eq!(
5322 orderbook.asks[0].amount,
5323 Amount::new(Decimal::from_str_radix("2.0", 10).unwrap())
5324 );
5325 assert_eq!(orderbook.timestamp, 1609459200000);
5326 }
5327
5328 #[test]
5329 fn test_parse_ws_ohlcv() {
5330 let data = json!({
5331 "e": "kline",
5332 "E": 1609459200000i64,
5333 "s": "BTCUSDT",
5334 "k": {
5335 "t": 1609459200000i64,
5336 "o": "49000.00",
5337 "h": "51000.00",
5338 "l": "48500.00",
5339 "c": "50000.00",
5340 "v": "1000.5"
5341 }
5342 });
5343
5344 let ohlcv = parse_ws_ohlcv(&data).unwrap();
5345 assert_eq!(ohlcv.timestamp, 1609459200000);
5346 assert_eq!(ohlcv.open, 49000.00);
5347 assert_eq!(ohlcv.high, 51000.00);
5348 assert_eq!(ohlcv.low, 48500.00);
5349 assert_eq!(ohlcv.close, 50000.00);
5350 assert_eq!(ohlcv.volume, 1000.5);
5351 }
5352
5353 #[test]
5354 fn test_parse_ws_bid_ask() {
5355 let data = json!({
5356 "s": "BTCUSDT",
5357 "b": "49999.00",
5358 "B": "1.5",
5359 "a": "50001.00",
5360 "A": "2.0",
5361 "E": 1609459200000i64
5362 });
5363
5364 let bid_ask = parse_ws_bid_ask(&data).unwrap();
5365 assert_eq!(bid_ask.symbol, "BTCUSDT");
5366 assert_eq!(bid_ask.bid_price, 49999.00);
5367 assert_eq!(bid_ask.bid_quantity, 1.5);
5368 assert_eq!(bid_ask.ask_price, 50001.00);
5369 assert_eq!(bid_ask.ask_quantity, 2.0);
5370 assert_eq!(bid_ask.timestamp, 1609459200000);
5371
5372 let spread = bid_ask.spread();
5374 assert_eq!(spread, 2.0);
5375
5376 let mid_price = bid_ask.mid_price();
5377 assert_eq!(mid_price, 50000.0);
5378 }
5379 #[test]
5380 fn test_parse_ws_mark_price() {
5381 let data = json!({
5382 "e": "markPriceUpdate",
5383 "E": 1609459200000i64,
5384 "s": "BTCUSDT",
5385 "p": "50250.50",
5386 "i": "50000.00",
5387 "P": "50500.00",
5388 "r": "0.00010000",
5389 "T": 1609459300000i64
5390 });
5391
5392 let mark_price = parse_ws_mark_price(&data).unwrap();
5393 assert_eq!(mark_price.symbol, "BTCUSDT");
5394 assert_eq!(mark_price.mark_price, 50250.50);
5395 assert_eq!(mark_price.index_price, Some(50000.00));
5396 assert_eq!(mark_price.estimated_settle_price, Some(50500.00));
5397 assert_eq!(mark_price.last_funding_rate, Some(0.0001));
5398 assert_eq!(mark_price.next_funding_time, Some(1609459300000));
5399 assert_eq!(mark_price.timestamp, 1609459200000);
5400
5401 let basis = mark_price.basis();
5403 assert_eq!(basis, Some(250.50));
5404
5405 let funding_rate_pct = mark_price.funding_rate_percent();
5406 assert_eq!(funding_rate_pct, Some(0.01));
5407 }
5408}