ccxt_exchanges/binance/
parser.rs

1//! Binance data parser module.
2//!
3//! Converts Binance API response data into standardized CCXT format structures.
4
5use ccxt_core::{
6    Result,
7    error::{Error, ParseError},
8    types::{
9        AccountConfig, Balance, BalanceEntry, BidAsk, BorrowInterest, BorrowRate,
10        BorrowRateHistory, CommissionRate, DepositWithdrawFee, FeeFundingRate,
11        FeeFundingRateHistory, FeeTradingFee, FundingFee, FundingHistory, IndexPrice,
12        LedgerDirection, LedgerEntry, LedgerEntryType, Leverage, LeverageTier, Liquidation,
13        MarginAdjustment, MarginLoan, MarginType, MarkPrice, Market, MaxBorrowable, MaxLeverage,
14        MaxTransferable, NextFundingRate, OHLCV, OcoOrder, OcoOrderInfo, OpenInterest,
15        OpenInterestHistory, Order, OrderBook, OrderBookDelta, OrderBookEntry, OrderReport,
16        OrderSide, OrderStatus, OrderType, Position, PremiumIndex, TakerOrMaker, Ticker,
17        TimeInForce, Trade, Transfer,
18        financial::{Amount, Cost, Price},
19    },
20};
21use rust_decimal::Decimal;
22use rust_decimal::prelude::{FromPrimitive, FromStr, ToPrimitive};
23use serde_json::Value;
24use std::collections::HashMap;
25
26// ============================================================================
27// Helper Functions - Type Conversion
28// ============================================================================
29
30/// Parse an f64 value from JSON (supports both string and number formats).
31fn parse_f64(data: &Value, key: &str) -> Option<f64> {
32    data.get(key).and_then(|v| {
33        v.as_f64()
34            .or_else(|| v.as_str().and_then(|s| s.parse::<f64>().ok()))
35    })
36}
37
38/// Parse a `Decimal` value from JSON (supports both string and number formats).
39/// Prioritizes string parsing for maximum precision, falls back to f64 only when necessary.
40fn parse_decimal(data: &Value, key: &str) -> Option<Decimal> {
41    data.get(key).and_then(|v| {
42        // Prioritize string parsing for maximum precision
43        if let Some(s) = v.as_str() {
44            Decimal::from_str(s).ok()
45        } else if let Some(num) = v.as_f64() {
46            // Fallback to f64 only when value is a JSON number
47            Decimal::from_f64(num)
48        } else {
49            None
50        }
51    })
52}
53
54/// Parse a `Decimal` value from JSON, trying multiple keys in order.
55/// Useful when different API responses use different field names for the same value.
56fn parse_decimal_multi(data: &Value, keys: &[&str]) -> Option<Decimal> {
57    for key in keys {
58        if let Some(decimal) = parse_decimal(data, key) {
59            return Some(decimal);
60        }
61    }
62    None
63}
64
65/// Convert a JSON `Value` into a `HashMap<String, Value>`.
66fn value_to_hashmap(data: &Value) -> HashMap<String, Value> {
67    data.as_object()
68        .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
69        .unwrap_or_default()
70}
71
72/// Parse an array of orderbook entries from JSON.
73#[allow(dead_code)]
74fn parse_order_book_entries(data: &Value) -> Vec<OrderBookEntry> {
75    data.as_array()
76        .map(|arr| {
77            arr.iter()
78                .filter_map(|item| {
79                    let price = if let Some(arr) = item.as_array() {
80                        arr.get(0)
81                            .and_then(|v| v.as_str())
82                            .and_then(|s| Decimal::from_str(s).ok())
83                    } else {
84                        None
85                    }?;
86
87                    let amount = if let Some(arr) = item.as_array() {
88                        arr.get(1)
89                            .and_then(|v| v.as_str())
90                            .and_then(|s| Decimal::from_str(s).ok())
91                    } else {
92                        None
93                    }?;
94
95                    Some(OrderBookEntry {
96                        price: Price::new(price),
97                        amount: Amount::new(amount),
98                    })
99                })
100                .collect()
101        })
102        .unwrap_or_default()
103}
104
105// ============================================================================
106// Parser Functions
107// ============================================================================
108
109/// Parse market data from Binance exchange info.
110///
111/// # Arguments
112///
113/// * `data` - Binance market data JSON object
114///
115/// # Returns
116///
117/// Returns a CCXT [`Market`] structure.
118///
119/// # Errors
120///
121/// Returns an error if required fields are missing or invalid.
122pub fn parse_market(data: &Value) -> Result<Market> {
123    use ccxt_core::types::{MarketLimits, MarketPrecision, MarketType, MinMax};
124
125    let symbol = data["symbol"]
126        .as_str()
127        .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
128        .to_string();
129
130    let base_asset = data["baseAsset"]
131        .as_str()
132        .ok_or_else(|| Error::from(ParseError::missing_field("baseAsset")))?
133        .to_string();
134
135    let quote_asset = data["quoteAsset"]
136        .as_str()
137        .ok_or_else(|| Error::from(ParseError::missing_field("quoteAsset")))?
138        .to_string();
139
140    let status = data["status"]
141        .as_str()
142        .ok_or_else(|| Error::from(ParseError::missing_field("status")))?;
143
144    let active = Some(status == "TRADING");
145
146    // Check if margin trading is supported
147    let margin = data["isMarginTradingAllowed"].as_bool().unwrap_or(false);
148
149    // Parse price and amount precision from filters
150    let mut price_precision: Option<Decimal> = None;
151    let mut amount_precision: Option<Decimal> = None;
152    let mut min_amount: Option<Decimal> = None;
153    let mut max_amount: Option<Decimal> = None;
154    let mut min_cost: Option<Decimal> = None;
155    let mut min_price: Option<Decimal> = None;
156    let mut max_price: Option<Decimal> = None;
157
158    if let Some(filters) = data["filters"].as_array() {
159        for filter in filters {
160            let filter_type = filter["filterType"].as_str().unwrap_or("");
161
162            match filter_type {
163                "PRICE_FILTER" => {
164                    if let Some(tick_size) = filter["tickSize"].as_str() {
165                        if let Ok(dec) = Decimal::from_str(tick_size) {
166                            price_precision = Some(dec);
167                        }
168                    }
169                    if let Some(min) = filter["minPrice"].as_str() {
170                        min_price = Decimal::from_str(min).ok();
171                    }
172                    if let Some(max) = filter["maxPrice"].as_str() {
173                        max_price = Decimal::from_str(max).ok();
174                    }
175                }
176                "LOT_SIZE" => {
177                    if let Some(step_size) = filter["stepSize"].as_str() {
178                        if let Ok(dec) = Decimal::from_str(step_size) {
179                            amount_precision = Some(dec);
180                        }
181                    }
182                    if let Some(min) = filter["minQty"].as_str() {
183                        min_amount = Decimal::from_str(min).ok();
184                    }
185                    if let Some(max) = filter["maxQty"].as_str() {
186                        max_amount = Decimal::from_str(max).ok();
187                    }
188                }
189                "MIN_NOTIONAL" | "NOTIONAL" => {
190                    if let Some(min) = filter["minNotional"].as_str() {
191                        min_cost = Decimal::from_str(min).ok();
192                    }
193                }
194                _ => {}
195            }
196        }
197    }
198
199    // Create unified symbol
200    let unified_symbol = format!("{}/{}", base_asset, quote_asset);
201    // Parse the symbol to get structured representation
202    let parsed_symbol = ccxt_core::symbol::SymbolParser::parse(&unified_symbol).ok();
203
204    Ok(Market {
205        id: symbol.clone(),
206        symbol: unified_symbol,
207        parsed_symbol,
208        base: base_asset.clone(),
209        quote: quote_asset.clone(),
210        settle: None,
211        base_id: Some(base_asset),
212        quote_id: Some(quote_asset),
213        settle_id: None,
214        market_type: MarketType::Spot,
215        active: active.unwrap_or(true),
216        margin,
217        contract: Some(false),
218        linear: None,
219        inverse: None,
220        // Default fee rate of 0.1% - using from_str which is infallible for valid decimal strings
221        taker: Decimal::from_str("0.001").ok(),
222        maker: Decimal::from_str("0.001").ok(),
223        contract_size: None,
224        expiry: None,
225        expiry_datetime: None,
226        strike: None,
227        option_type: None,
228        percentage: Some(true),
229        tier_based: Some(false),
230        fee_side: Some("quote".to_string()),
231        precision: MarketPrecision {
232            price: price_precision,
233            amount: amount_precision,
234            base: None,
235            quote: None,
236        },
237        limits: MarketLimits {
238            amount: Some(MinMax {
239                min: min_amount,
240                max: max_amount,
241            }),
242            price: Some(MinMax {
243                min: min_price,
244                max: max_price,
245            }),
246            cost: Some(MinMax {
247                min: min_cost,
248                max: None,
249            }),
250            leverage: None,
251        },
252        info: value_to_hashmap(data),
253    })
254}
255
256/// Parse ticker data from Binance 24hr ticker response.
257///
258/// # Arguments
259///
260/// * `data` - Binance ticker data JSON object
261/// * `market` - Optional market information for symbol resolution
262///
263/// # Returns
264///
265/// Returns a CCXT [`Ticker`] structure.
266///
267/// # Errors
268///
269/// Returns an error if the symbol field is missing when market is not provided.
270pub fn parse_ticker(data: &Value, market: Option<&Market>) -> Result<Ticker> {
271    let symbol = if let Some(m) = market {
272        m.symbol.clone()
273    } else {
274        data["symbol"]
275            .as_str()
276            .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
277            .to_string()
278    };
279
280    let timestamp = data["closeTime"].as_i64();
281
282    Ok(Ticker {
283        symbol,
284        timestamp: timestamp.unwrap_or(0),
285        datetime: timestamp.map(|t| {
286            chrono::DateTime::from_timestamp(t / 1000, 0)
287                .map(|dt| dt.to_rfc3339())
288                .unwrap_or_default()
289        }),
290        high: parse_decimal(data, "highPrice").map(Price::new),
291        low: parse_decimal(data, "lowPrice").map(Price::new),
292        bid: parse_decimal(data, "bidPrice").map(Price::new),
293        bid_volume: parse_decimal(data, "bidQty").map(Amount::new),
294        ask: parse_decimal(data, "askPrice").map(Price::new),
295        ask_volume: parse_decimal(data, "askQty").map(Amount::new),
296        vwap: parse_decimal(data, "weightedAvgPrice").map(Price::new),
297        open: parse_decimal(data, "openPrice").map(Price::new),
298        close: parse_decimal(data, "lastPrice").map(Price::new),
299        last: parse_decimal(data, "lastPrice").map(Price::new),
300        previous_close: parse_decimal(data, "prevClosePrice").map(Price::new),
301        change: parse_decimal(data, "priceChange").map(Price::new),
302        percentage: parse_decimal(data, "priceChangePercent"),
303        average: None,
304        base_volume: parse_decimal(data, "volume").map(Amount::new),
305        quote_volume: parse_decimal(data, "quoteVolume").map(Amount::new),
306        info: value_to_hashmap(data),
307    })
308}
309
310/// Parse trade data from Binance trade response.
311///
312/// # Arguments
313///
314/// * `data` - Binance trade data JSON object
315/// * `market` - Optional market information for symbol resolution
316///
317/// # Returns
318///
319/// Returns a CCXT [`Trade`] structure.
320///
321/// # Errors
322///
323/// Returns an error if the symbol field is missing when market is not provided.
324pub fn parse_trade(data: &Value, market: Option<&Market>) -> Result<Trade> {
325    let symbol = if let Some(m) = market {
326        m.symbol.clone()
327    } else {
328        data["symbol"]
329            .as_str()
330            .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
331            .to_string()
332    };
333
334    let id = data["id"]
335        .as_u64()
336        .or_else(|| data["a"].as_u64())
337        .map(|i| i.to_string());
338
339    let timestamp = data["time"].as_i64().or_else(|| data["T"].as_i64());
340
341    let side = if data["isBuyerMaker"].as_bool().unwrap_or(false) {
342        OrderSide::Sell
343    } else if data["m"].as_bool().unwrap_or(false) {
344        OrderSide::Sell
345    } else {
346        OrderSide::Buy
347    };
348
349    let price = parse_decimal_multi(data, &["price", "p"]);
350    let amount = parse_decimal_multi(data, &["qty", "q"]);
351
352    let cost = match (price, amount) {
353        (Some(p), Some(a)) => Some(p * a),
354        _ => None,
355    };
356
357    Ok(Trade {
358        id,
359        order: data["orderId"]
360            .as_u64()
361            .or_else(|| data["orderid"].as_u64())
362            .map(|i| i.to_string()),
363        timestamp: timestamp.unwrap_or(0),
364        datetime: timestamp.map(|t| {
365            chrono::DateTime::from_timestamp(t / 1000, 0)
366                .map(|dt| dt.to_rfc3339())
367                .unwrap_or_default()
368        }),
369        symbol,
370        trade_type: None,
371        side,
372        taker_or_maker: if data["isBuyerMaker"].as_bool().unwrap_or(false) {
373            Some(TakerOrMaker::Maker)
374        } else {
375            Some(TakerOrMaker::Taker)
376        },
377        price: Price::new(price.unwrap_or(Decimal::ZERO)),
378        amount: Amount::new(amount.unwrap_or(Decimal::ZERO)),
379        cost: cost.map(Cost::new),
380        fee: None,
381        info: value_to_hashmap(data),
382    })
383}
384
385/// Parse order data from Binance order response.
386///
387/// # Arguments
388///
389/// * `data` - Binance order data JSON object
390/// * `market` - Optional market information for symbol resolution
391///
392/// # Returns
393///
394/// Returns a CCXT [`Order`] structure.
395///
396/// # Errors
397///
398/// Returns an error if required fields (symbol, orderId, status, side, amount) are missing or invalid.
399pub fn parse_order(data: &Value, market: Option<&Market>) -> Result<Order> {
400    let symbol = if let Some(m) = market {
401        m.symbol.clone()
402    } else {
403        data["symbol"]
404            .as_str()
405            .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
406            .to_string()
407    };
408
409    let id = data["orderId"]
410        .as_u64()
411        .ok_or_else(|| Error::from(ParseError::missing_field("orderId")))?
412        .to_string();
413
414    let timestamp = data["time"]
415        .as_i64()
416        .or_else(|| data["transactTime"].as_i64());
417
418    let status_str = data["status"]
419        .as_str()
420        .ok_or_else(|| Error::from(ParseError::missing_field("status")))?;
421
422    let status = match status_str {
423        "NEW" | "PARTIALLY_FILLED" => OrderStatus::Open,
424        "FILLED" => OrderStatus::Closed,
425        "CANCELED" => OrderStatus::Cancelled,
426        "EXPIRED" => OrderStatus::Expired,
427        "REJECTED" => OrderStatus::Rejected,
428        _ => OrderStatus::Open,
429    };
430
431    let side = match data["side"].as_str() {
432        Some("BUY") => OrderSide::Buy,
433        Some("SELL") => OrderSide::Sell,
434        _ => return Err(Error::from(ParseError::invalid_format("data", "side"))),
435    };
436
437    let order_type = match data["type"].as_str() {
438        Some("MARKET") => OrderType::Market,
439        Some("LIMIT") => OrderType::Limit,
440        Some("STOP_LOSS") => OrderType::StopLoss,
441        Some("STOP_LOSS_LIMIT") => OrderType::StopLossLimit,
442        Some("TAKE_PROFIT") => OrderType::TakeProfit,
443        Some("TAKE_PROFIT_LIMIT") => OrderType::TakeProfitLimit,
444        Some("STOP_MARKET") | Some("STOP") => OrderType::StopMarket,
445        Some("TAKE_PROFIT_MARKET") => OrderType::TakeProfitLimit,
446        Some("TRAILING_STOP_MARKET") => OrderType::TrailingStop,
447        Some("LIMIT_MAKER") => OrderType::LimitMaker,
448        _ => OrderType::Limit,
449    };
450
451    let time_in_force = match data["timeInForce"].as_str() {
452        Some("GTC") => Some(TimeInForce::GTC),
453        Some("IOC") => Some(TimeInForce::IOC),
454        Some("FOK") => Some(TimeInForce::FOK),
455        Some("GTX") => Some(TimeInForce::PO),
456        _ => None,
457    };
458
459    let price = parse_decimal(data, "price");
460    let amount = parse_decimal(data, "origQty");
461    let filled = parse_decimal(data, "executedQty");
462    let remaining = match (&amount, &filled) {
463        (Some(a), Some(f)) => Some(*a - *f),
464        _ => None,
465    };
466
467    let cost = parse_decimal(data, "cummulativeQuoteQty");
468
469    let average = match (&cost, &filled) {
470        (Some(c), Some(f)) if !f.is_zero() => Some(*c / *f),
471        _ => None,
472    };
473
474    Ok(Order {
475        id,
476        client_order_id: data["clientOrderId"].as_str().map(|s| s.to_string()),
477        timestamp,
478        datetime: timestamp.map(|t| {
479            chrono::DateTime::from_timestamp(t / 1000, 0)
480                .map(|dt| dt.to_rfc3339())
481                .unwrap_or_default()
482        }),
483        last_trade_timestamp: data["updateTime"].as_i64(),
484        status,
485        symbol,
486        order_type,
487        time_in_force: time_in_force.map(|t| t.to_string()),
488        side,
489        price,
490        average,
491        amount: amount.ok_or_else(|| Error::from(ParseError::missing_field("amount")))?,
492        filled,
493        remaining,
494        cost,
495        trades: None,
496        fee: None,
497        post_only: None,
498        reduce_only: data["reduceOnly"].as_bool(),
499        trigger_price: parse_decimal(data, "triggerPrice"),
500        stop_price: parse_decimal(data, "stopPrice"),
501        take_profit_price: parse_decimal(data, "takeProfitPrice"),
502        stop_loss_price: parse_decimal(data, "stopLossPrice"),
503        trailing_delta: parse_decimal(data, "trailingDelta"),
504        trailing_percent: parse_decimal_multi(data, &["trailingPercent", "callbackRate"]),
505        activation_price: parse_decimal_multi(data, &["activationPrice", "activatePrice"]),
506        callback_rate: parse_decimal(data, "callbackRate"),
507        working_type: data["workingType"].as_str().map(|s| s.to_string()),
508        fees: Some(Vec::new()),
509        info: value_to_hashmap(data),
510    })
511}
512/// Parse OCO (One-Cancels-the-Other) order data from Binance.
513///
514/// # Arguments
515///
516/// * `data` - Binance OCO order data JSON object
517///
518/// # Returns
519///
520/// Returns a CCXT [`OcoOrder`] structure.
521///
522/// # Errors
523///
524/// Returns an error if required fields (orderListId, symbol, listStatusType, listOrderStatus, transactionTime) are missing.
525pub fn parse_oco_order(data: &Value) -> Result<OcoOrder> {
526    let order_list_id = data["orderListId"]
527        .as_i64()
528        .ok_or_else(|| Error::from(ParseError::missing_field("orderListId")))?;
529
530    let list_client_order_id = data["listClientOrderId"].as_str().map(|s| s.to_string());
531
532    let symbol = data["symbol"]
533        .as_str()
534        .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
535        .to_string();
536
537    let list_status = data["listStatusType"]
538        .as_str()
539        .ok_or_else(|| Error::from(ParseError::missing_field("listStatusType")))?
540        .to_string();
541
542    let list_order_status = data["listOrderStatus"]
543        .as_str()
544        .ok_or_else(|| Error::from(ParseError::missing_field("listOrderStatus")))?
545        .to_string();
546
547    let transaction_time = data["transactionTime"]
548        .as_i64()
549        .ok_or_else(|| Error::from(ParseError::missing_field("transactionTime")))?;
550
551    let datetime = chrono::DateTime::from_timestamp((transaction_time / 1000) as i64, 0)
552        .map(|dt| dt.to_rfc3339())
553        .unwrap_or_default();
554
555    let mut orders = Vec::new();
556    if let Some(orders_array) = data["orders"].as_array() {
557        for order in orders_array {
558            let order_info = OcoOrderInfo {
559                symbol: order["symbol"].as_str().unwrap_or(&symbol).to_string(),
560                order_id: order["orderId"]
561                    .as_i64()
562                    .ok_or_else(|| Error::from(ParseError::missing_field("orderId")))?,
563                client_order_id: order["clientOrderId"].as_str().map(|s| s.to_string()),
564            };
565            orders.push(order_info);
566        }
567    }
568
569    let order_reports = if let Some(reports_array) = data["orderReports"].as_array() {
570        let mut reports = Vec::new();
571        for report in reports_array {
572            let order_report = OrderReport {
573                symbol: report["symbol"].as_str().unwrap_or(&symbol).to_string(),
574                order_id: report["orderId"]
575                    .as_i64()
576                    .ok_or_else(|| Error::from(ParseError::missing_field("orderId")))?,
577                order_list_id: report["orderListId"].as_i64().unwrap_or(order_list_id),
578                client_order_id: report["clientOrderId"].as_str().map(|s| s.to_string()),
579                transact_time: report["transactTime"].as_i64().unwrap_or(transaction_time),
580                price: report["price"].as_str().unwrap_or("0").to_string(),
581                orig_qty: report["origQty"].as_str().unwrap_or("0").to_string(),
582                executed_qty: report["executedQty"].as_str().unwrap_or("0").to_string(),
583                cummulative_quote_qty: report["cummulativeQuoteQty"]
584                    .as_str()
585                    .unwrap_or("0")
586                    .to_string(),
587                status: report["status"].as_str().unwrap_or("NEW").to_string(),
588                time_in_force: report["timeInForce"].as_str().unwrap_or("GTC").to_string(),
589                type_: report["type"].as_str().unwrap_or("LIMIT").to_string(),
590                side: report["side"].as_str().unwrap_or("SELL").to_string(),
591                stop_price: report["stopPrice"].as_str().map(|s| s.to_string()),
592            };
593            reports.push(order_report);
594        }
595        Some(reports)
596    } else {
597        None
598    };
599
600    Ok(OcoOrder {
601        info: Some(data.clone()),
602        order_list_id,
603        list_client_order_id,
604        symbol,
605        list_status,
606        list_order_status,
607        transaction_time,
608        datetime,
609        orders,
610        order_reports,
611    })
612}
613
614/// Parse balance data from Binance account info.
615///
616/// # Arguments
617///
618/// * `data` - Binance account data JSON object
619///
620/// # Returns
621///
622/// Returns a CCXT [`Balance`] structure with all balances (including zero balances).
623pub fn parse_balance(data: &Value) -> Result<Balance> {
624    let mut balances = HashMap::new();
625
626    if let Some(balances_array) = data["balances"].as_array() {
627        for balance in balances_array {
628            let currency = balance["asset"]
629                .as_str()
630                .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
631                .to_string();
632
633            let free = parse_decimal(balance, "free").unwrap_or(Decimal::ZERO);
634            let locked = parse_decimal(balance, "locked").unwrap_or(Decimal::ZERO);
635            let total = free + locked;
636
637            balances.insert(
638                currency,
639                BalanceEntry {
640                    free,
641                    used: locked,
642                    total,
643                },
644            );
645        }
646    }
647
648    Ok(Balance {
649        balances,
650        info: value_to_hashmap(data),
651    })
652}
653
654/// Parse orderbook data from Binance depth response.
655///
656/// # Arguments
657///
658/// * `data` - Binance orderbook data JSON object
659/// * `symbol` - Trading pair symbol
660///
661/// # Returns
662///
663/// Returns a CCXT [`OrderBook`] structure.
664pub fn parse_orderbook(data: &Value, symbol: String) -> Result<OrderBook> {
665    // Try WebSocket event timestamp fields (T or E), fall back to current time for REST API
666    let timestamp = data["T"]
667        .as_i64()
668        .or_else(|| data["E"].as_i64())
669        .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
670
671    let bids = parse_orderbook_side(&data["bids"])?;
672    let asks = parse_orderbook_side(&data["asks"])?;
673
674    Ok(OrderBook {
675        symbol,
676        timestamp,
677        datetime: Some({
678            chrono::DateTime::from_timestamp_millis(timestamp)
679                .map(|dt| dt.to_rfc3339())
680                .unwrap_or_default()
681        }),
682        nonce: data["lastUpdateId"].as_i64(),
683        bids,
684        asks,
685        buffered_deltas: std::collections::VecDeque::new(),
686        bids_map: std::collections::BTreeMap::new(),
687        asks_map: std::collections::BTreeMap::new(),
688        is_synced: false,
689        needs_resync: false,
690        last_resync_time: 0,
691        info: value_to_hashmap(data),
692    })
693}
694
695/// Parse one side (bids or asks) of orderbook data.
696fn parse_orderbook_side(data: &Value) -> Result<Vec<OrderBookEntry>> {
697    let array = data
698        .as_array()
699        .ok_or_else(|| Error::from(ParseError::invalid_value("data", "orderbook side")))?;
700
701    let mut result = Vec::new();
702
703    for item in array {
704        if let Some(arr) = item.as_array() {
705            if arr.len() >= 2 {
706                let price = arr[0]
707                    .as_str()
708                    .and_then(|s| s.parse::<f64>().ok())
709                    .and_then(Decimal::from_f64_retain)
710                    .ok_or_else(|| Error::from(ParseError::invalid_value("data", "price")))?;
711                let amount = arr[1]
712                    .as_str()
713                    .and_then(|s| s.parse::<f64>().ok())
714                    .and_then(Decimal::from_f64_retain)
715                    .ok_or_else(|| Error::from(ParseError::invalid_value("data", "amount")))?;
716                result.push(OrderBookEntry {
717                    price: Price::new(price),
718                    amount: Amount::new(amount),
719                });
720            }
721        }
722    }
723
724    Ok(result)
725}
726
727/// Parse WebSocket orderbook delta data from Binance diff depth stream.
728///
729/// This function parses incremental orderbook updates for proper synchronization.
730/// Binance sends different fields for spot vs futures:
731/// - Spot: `U` (first update ID), `u` (final update ID)
732/// - Futures: `U`, `u`, and `pu` (previous final update ID)
733///
734/// # Arguments
735///
736/// * `data` - Binance WebSocket orderbook delta JSON object
737/// * `symbol` - Trading pair symbol (ccxt format, e.g., "BTC/USDT")
738///
739/// # Returns
740///
741/// Returns a CCXT [`OrderBookDelta`] structure for synchronization.
742///
743/// # Example WebSocket Message (Spot)
744///
745/// ```json
746/// {
747///   "e": "depthUpdate",
748///   "E": 1672515782136,
749///   "s": "BTCUSDT",
750///   "U": 157,
751///   "u": 160,
752///   "b": [["0.0024", "10"]],
753///   "a": [["0.0026", "100"]]
754/// }
755/// ```
756///
757/// # Example WebSocket Message (Futures)
758///
759/// ```json
760/// {
761///   "e": "depthUpdate",
762///   "E": 1672515782136,
763///   "s": "BTCUSDT",
764///   "U": 157,
765///   "u": 160,
766///   "pu": 156,
767///   "b": [["0.0024", "10"]],
768///   "a": [["0.0026", "100"]]
769/// }
770/// ```
771pub fn parse_ws_orderbook_delta(data: &Value, symbol: String) -> Result<OrderBookDelta> {
772    // Parse update IDs for synchronization
773    // U: First update ID in event
774    let first_update_id = data["U"]
775        .as_i64()
776        .ok_or_else(|| Error::from(ParseError::missing_field("U (first_update_id)")))?;
777
778    // u: Final update ID in event
779    let final_update_id = data["u"]
780        .as_i64()
781        .ok_or_else(|| Error::from(ParseError::missing_field("u (final_update_id)")))?;
782
783    // pu: Previous final update ID (futures only, optional for spot)
784    let prev_final_update_id = data["pu"].as_i64();
785
786    // E: Event time
787    let timestamp = data["E"]
788        .as_i64()
789        .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
790
791    // Parse bids and asks using WebSocket field names (b, a)
792    let bids = parse_orderbook_side_ws(&data["b"])?;
793    let asks = parse_orderbook_side_ws(&data["a"])?;
794
795    Ok(OrderBookDelta {
796        symbol,
797        first_update_id,
798        final_update_id,
799        prev_final_update_id,
800        timestamp,
801        bids,
802        asks,
803    })
804}
805
806/// Parse one side (bids or asks) of WebSocket orderbook delta data.
807///
808/// WebSocket uses `b` and `a` field names instead of `bids` and `asks`.
809fn parse_orderbook_side_ws(data: &Value) -> Result<Vec<OrderBookEntry>> {
810    // If the field is null or missing, return empty vector
811    let Some(array) = data.as_array() else {
812        return Ok(Vec::new());
813    };
814
815    let mut result = Vec::with_capacity(array.len());
816
817    for item in array {
818        if let Some(arr) = item.as_array() {
819            if arr.len() >= 2 {
820                let price = arr[0]
821                    .as_str()
822                    .and_then(|s| s.parse::<f64>().ok())
823                    .and_then(Decimal::from_f64_retain)
824                    .ok_or_else(|| Error::from(ParseError::invalid_value("data", "price")))?;
825                let amount = arr[1]
826                    .as_str()
827                    .and_then(|s| s.parse::<f64>().ok())
828                    .and_then(Decimal::from_f64_retain)
829                    .ok_or_else(|| Error::from(ParseError::invalid_value("data", "amount")))?;
830                result.push(OrderBookEntry {
831                    price: Price::new(price),
832                    amount: Amount::new(amount),
833                });
834            }
835        }
836    }
837
838    Ok(result)
839}
840
841// ============================================================================
842// Futures-Specific Parser Functions
843// ============================================================================
844
845/// Parse funding rate data from Binance futures API.
846///
847/// # Arguments
848///
849/// * `data` - Binance funding rate data JSON object
850/// * `market` - Optional market information for symbol resolution
851///
852/// # Returns
853///
854/// Returns a CCXT [`FeeFundingRate`] structure with `Decimal` precision.
855pub fn parse_funding_rate(data: &Value, market: Option<&Market>) -> Result<FeeFundingRate> {
856    let symbol = if let Some(m) = market {
857        m.symbol.clone()
858    } else {
859        data["symbol"]
860            .as_str()
861            .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
862            .to_string()
863    };
864
865    let funding_rate =
866        parse_decimal(data, "lastFundingRate").or_else(|| parse_decimal(data, "fundingRate"));
867
868    let mark_price = parse_decimal(data, "markPrice");
869    let index_price = parse_decimal(data, "indexPrice");
870    let interest_rate = parse_decimal(data, "interestRate");
871
872    let next_funding_time = data["nextFundingTime"].as_i64();
873    let funding_timestamp = next_funding_time.or_else(|| data["fundingTime"].as_i64());
874
875    Ok(FeeFundingRate {
876        info: data.clone(),
877        symbol,
878        mark_price,
879        index_price,
880        interest_rate,
881        estimated_settle_price: None,
882        funding_rate,
883        funding_timestamp,
884        funding_datetime: funding_timestamp.map(|t| {
885            chrono::DateTime::from_timestamp(t / 1000, 0)
886                .map(|dt| dt.to_rfc3339())
887                .unwrap_or_default()
888        }),
889        next_funding_rate: None,
890        next_funding_timestamp: None,
891        next_funding_datetime: None,
892        previous_funding_rate: None,
893        previous_funding_timestamp: None,
894        previous_funding_datetime: None,
895        timestamp: None,
896        datetime: None,
897        interval: None,
898    })
899}
900
901/// Parse funding rate history data from Binance futures API.
902///
903/// # Arguments
904///
905/// * `data` - Binance funding rate history data JSON object
906/// * `market` - Optional market information for symbol resolution
907///
908/// # Returns
909///
910/// Returns a CCXT [`FeeFundingRateHistory`] structure with `Decimal` precision.
911pub fn parse_funding_rate_history(
912    data: &Value,
913    market: Option<&Market>,
914) -> Result<FeeFundingRateHistory> {
915    let symbol = if let Some(m) = market {
916        m.symbol.clone()
917    } else {
918        data["symbol"]
919            .as_str()
920            .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
921            .to_string()
922    };
923
924    let funding_rate = parse_decimal(data, "fundingRate");
925    let funding_time = data["fundingTime"].as_i64();
926
927    Ok(FeeFundingRateHistory {
928        info: data.clone(),
929        symbol,
930        funding_rate,
931        funding_timestamp: funding_time,
932        funding_datetime: funding_time.map(|t| {
933            chrono::DateTime::from_timestamp(t / 1000, 0)
934                .map(|dt| dt.to_rfc3339())
935                .unwrap_or_default()
936        }),
937        timestamp: funding_time,
938        datetime: funding_time.map(|t| {
939            chrono::DateTime::from_timestamp(t / 1000, 0)
940                .map(|dt| dt.to_rfc3339())
941                .unwrap_or_default()
942        }),
943    })
944}
945
946/// Parse position data from Binance futures position risk.
947///
948/// # Arguments
949///
950/// * `data` - Binance position data JSON object
951/// * `market` - Optional market information for symbol resolution
952///
953/// # Returns
954///
955/// Returns a CCXT [`Position`] structure.
956pub fn parse_position(data: &Value, market: Option<&Market>) -> Result<Position> {
957    let symbol = if let Some(m) = market {
958        m.symbol.clone()
959    } else {
960        data["symbol"]
961            .as_str()
962            .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
963            .to_string()
964    };
965
966    let position_side = data["positionSide"].as_str().unwrap_or("BOTH");
967
968    let side = match position_side {
969        "LONG" => Some("long".to_string()),
970        "SHORT" => Some("short".to_string()),
971        "BOTH" => {
972            // For dual position mode, determine side from positionAmt sign
973            let position_amt = parse_f64(data, "positionAmt").unwrap_or(0.0);
974            if position_amt > 0.0 {
975                Some("long".to_string())
976            } else if position_amt < 0.0 {
977                Some("short".to_string())
978            } else {
979                None
980            }
981        }
982        _ => None,
983    };
984
985    let contracts = parse_f64(data, "positionAmt").map(|v| v.abs());
986
987    let contract_size = Some(1.0); // Binance futures contract size is 1
988
989    let entry_price = parse_f64(data, "entryPrice");
990    let mark_price = parse_f64(data, "markPrice");
991    let notional = parse_f64(data, "notional").map(|v| v.abs());
992
993    let leverage = parse_f64(data, "leverage");
994
995    let collateral =
996        parse_f64(data, "isolatedWallet").or_else(|| parse_f64(data, "positionInitialMargin"));
997
998    let initial_margin =
999        parse_f64(data, "initialMargin").or_else(|| parse_f64(data, "positionInitialMargin"));
1000
1001    let maintenance_margin =
1002        parse_f64(data, "maintMargin").or_else(|| parse_f64(data, "positionMaintMargin"));
1003
1004    let unrealized_pnl =
1005        parse_f64(data, "unrealizedProfit").or_else(|| parse_f64(data, "unRealizedProfit"));
1006
1007    let liquidation_price = parse_f64(data, "liquidationPrice");
1008
1009    let margin_ratio = parse_f64(data, "marginRatio");
1010
1011    let margin_mode = data["marginType"]
1012        .as_str()
1013        .or_else(|| data["marginMode"].as_str())
1014        .map(|s| s.to_lowercase());
1015
1016    let hedged = position_side != "BOTH";
1017
1018    let percentage = match (unrealized_pnl, collateral) {
1019        (Some(pnl), Some(col)) if col > 0.0 => Some((pnl / col) * 100.0),
1020        _ => None,
1021    };
1022
1023    let initial_margin_percentage = parse_f64(data, "initialMarginPercentage");
1024    let maintenance_margin_percentage = parse_f64(data, "maintMarginPercentage");
1025
1026    let update_time = data["updateTime"].as_i64();
1027
1028    Ok(Position {
1029        info: data.clone(),
1030        id: None,
1031        symbol,
1032        side,
1033        contracts,
1034        contract_size,
1035        entry_price,
1036        mark_price,
1037        notional,
1038        leverage,
1039        collateral,
1040        initial_margin,
1041        initial_margin_percentage,
1042        maintenance_margin,
1043        maintenance_margin_percentage,
1044        unrealized_pnl,
1045        realized_pnl: None, // Binance positionRisk endpoint does not provide realized PnL
1046        liquidation_price,
1047        margin_ratio,
1048        margin_mode,
1049        hedged: Some(hedged),
1050        percentage,
1051        position_side: None,
1052        dual_side_position: None,
1053        timestamp: update_time,
1054        datetime: update_time.map(|t| {
1055            chrono::DateTime::from_timestamp(t / 1000, 0)
1056                .map(|dt| dt.to_rfc3339())
1057                .unwrap_or_default()
1058        }),
1059    })
1060}
1061/// Parse leverage information from Binance futures API.
1062///
1063/// # Arguments
1064///
1065/// * `data` - Binance leverage data JSON object
1066/// * `market` - Optional market information (unused)
1067///
1068/// # Returns
1069///
1070/// Returns a CCXT [`Leverage`] structure.
1071pub fn parse_leverage(data: &Value, _market: Option<&Market>) -> Result<Leverage> {
1072    let market_id = data.get("symbol").and_then(|v| v.as_str()).unwrap_or("");
1073
1074    let margin_mode = if let Some(isolated) = data.get("isolated").and_then(|v| v.as_bool()) {
1075        Some(if isolated {
1076            MarginType::Isolated
1077        } else {
1078            MarginType::Cross
1079        })
1080    } else if let Some(margin_type) = data.get("marginType").and_then(|v| v.as_str()) {
1081        Some(if margin_type == "crossed" {
1082            MarginType::Cross
1083        } else {
1084            MarginType::Isolated
1085        })
1086    } else {
1087        None
1088    };
1089
1090    let side = data
1091        .get("positionSide")
1092        .and_then(|v| v.as_str())
1093        .map(|s| s.to_lowercase());
1094
1095    let leverage_value = data.get("leverage").and_then(|v| v.as_i64());
1096
1097    // 4. 根据持仓方向分配杠杆
1098    let (long_leverage, short_leverage) = match side.as_deref() {
1099        None | Some("both") => (leverage_value, leverage_value),
1100        Some("long") => (leverage_value, None),
1101        Some("short") => (None, leverage_value),
1102        _ => (None, None),
1103    };
1104
1105    Ok(Leverage {
1106        info: data.clone(),
1107        symbol: market_id.to_string(),
1108        margin_mode,
1109        long_leverage,
1110        short_leverage,
1111        timestamp: None,
1112        datetime: None,
1113    })
1114}
1115
1116/// Parse funding fee history data from Binance futures API.
1117///
1118/// # Arguments
1119///
1120/// * `data` - Binance funding fee history data JSON object
1121/// * `market` - Optional market information for symbol resolution
1122///
1123/// # Returns
1124///
1125/// Returns a CCXT [`FundingHistory`] structure.
1126pub fn parse_funding_history(data: &Value, market: Option<&Market>) -> Result<FundingHistory> {
1127    let symbol = if let Some(m) = market {
1128        m.symbol.clone()
1129    } else {
1130        data["symbol"]
1131            .as_str()
1132            .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
1133            .to_string()
1134    };
1135
1136    let id = data["tranId"]
1137        .as_u64()
1138        .or_else(|| data["id"].as_u64())
1139        .map(|i| i.to_string());
1140
1141    let amount = parse_f64(data, "income");
1142    let code = data["asset"].as_str().map(|s| s.to_string());
1143    let timestamp = data["time"].as_i64();
1144
1145    Ok(FundingHistory {
1146        info: data.clone(),
1147        id,
1148        symbol,
1149        code,
1150        amount,
1151        timestamp: timestamp,
1152        datetime: timestamp.map(|t| {
1153            chrono::DateTime::from_timestamp((t / 1000) as i64, 0)
1154                .map(|dt| dt.to_rfc3339())
1155                .unwrap_or_default()
1156        }),
1157    })
1158}
1159
1160/// Parse funding fee data from Binance futures API.
1161///
1162/// # Arguments
1163///
1164/// * `data` - Binance funding fee data JSON object
1165/// * `market` - Optional market information for symbol resolution
1166///
1167/// # Returns
1168///
1169/// Returns a CCXT [`FundingFee`] structure.
1170pub fn parse_funding_fee(data: &Value, market: Option<&Market>) -> Result<FundingFee> {
1171    let symbol = if let Some(m) = market {
1172        m.symbol.clone()
1173    } else {
1174        data["symbol"]
1175            .as_str()
1176            .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
1177            .to_string()
1178    };
1179
1180    let income = parse_f64(data, "income").unwrap_or(0.0);
1181    let asset = data["asset"]
1182        .as_str()
1183        .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
1184        .to_string();
1185
1186    let time = data["time"]
1187        .as_i64()
1188        .ok_or_else(|| Error::from(ParseError::missing_field("time")))?;
1189
1190    let funding_rate = parse_f64(data, "fundingRate");
1191    let mark_price = parse_f64(data, "markPrice");
1192
1193    let datetime = Some(
1194        chrono::DateTime::from_timestamp((time / 1000) as i64, 0)
1195            .map(|dt| dt.to_rfc3339())
1196            .unwrap_or_default(),
1197    );
1198
1199    Ok(FundingFee {
1200        info: data.clone(),
1201        symbol,
1202        income,
1203        asset,
1204        time,
1205        datetime,
1206        funding_rate,
1207        mark_price,
1208    })
1209}
1210
1211/// Parse next funding rate data from Binance premium index.
1212///
1213/// # Arguments
1214///
1215/// * `data` - Binance premium index data JSON object
1216/// * `market` - Market information for symbol resolution
1217///
1218/// # Returns
1219///
1220/// Returns a CCXT [`NextFundingRate`] structure.
1221pub fn parse_next_funding_rate(data: &Value, market: &Market) -> Result<NextFundingRate> {
1222    let symbol = market.symbol.clone();
1223
1224    let mark_price = parse_f64(data, "markPrice")
1225        .ok_or_else(|| Error::from(ParseError::missing_field("markPrice")))?;
1226
1227    let index_price = parse_f64(data, "indexPrice");
1228
1229    let current_funding_rate = parse_f64(data, "lastFundingRate").unwrap_or(0.0);
1230
1231    let next_funding_rate = parse_f64(data, "interestRate")
1232        .or_else(|| parse_f64(data, "estimatedSettlePrice"))
1233        .unwrap_or(current_funding_rate);
1234
1235    let next_funding_time = data["nextFundingTime"]
1236        .as_i64()
1237        .ok_or_else(|| Error::from(ParseError::missing_field("nextFundingTime")))?;
1238
1239    let next_funding_datetime = Some(
1240        chrono::DateTime::from_timestamp((next_funding_time / 1000) as i64, 0)
1241            .map(|dt| dt.to_rfc3339())
1242            .unwrap_or_default(),
1243    );
1244
1245    Ok(NextFundingRate {
1246        info: data.clone(),
1247        symbol,
1248        mark_price,
1249        index_price,
1250        current_funding_rate,
1251        next_funding_rate,
1252        next_funding_time,
1253        next_funding_datetime,
1254    })
1255}
1256// ============================================================================
1257// Account Configuration Parser Functions
1258// ============================================================================
1259
1260/// Parse account configuration from Binance futures account info.
1261///
1262/// # Arguments
1263///
1264/// * `data` - Binance account info data JSON object
1265///
1266/// # Returns
1267///
1268/// Returns a CCXT [`AccountConfig`] structure.
1269pub fn parse_account_config(data: &Value) -> Result<AccountConfig> {
1270    let multi_assets_margin = data["multiAssetsMargin"].as_bool().unwrap_or(false);
1271
1272    let fee_tier = data["feeTier"].as_i64().unwrap_or(0) as i32;
1273
1274    let can_trade = data["canTrade"].as_bool().unwrap_or(true);
1275
1276    let can_deposit = data["canDeposit"].as_bool().unwrap_or(true);
1277
1278    let can_withdraw = data["canWithdraw"].as_bool().unwrap_or(true);
1279
1280    let update_time = data["updateTime"].as_i64().unwrap_or(0);
1281
1282    Ok(AccountConfig {
1283        info: Some(data.clone()),
1284        multi_assets_margin,
1285        fee_tier,
1286        can_trade,
1287        can_deposit,
1288        can_withdraw,
1289        update_time,
1290    })
1291}
1292
1293/// Parse commission rate information from Binance.
1294///
1295/// # Arguments
1296///
1297/// * `data` - Binance commission rate data JSON object
1298/// * `market` - Market information for symbol resolution
1299///
1300/// # Returns
1301///
1302/// Returns a CCXT [`CommissionRate`] structure.
1303pub fn parse_commission_rate(data: &Value, market: &Market) -> Result<CommissionRate> {
1304    let maker_commission_rate = data["makerCommissionRate"]
1305        .as_str()
1306        .and_then(|s| s.parse::<f64>().ok())
1307        .unwrap_or(0.0);
1308
1309    let taker_commission_rate = data["takerCommissionRate"]
1310        .as_str()
1311        .and_then(|s| s.parse::<f64>().ok())
1312        .unwrap_or(0.0);
1313
1314    Ok(CommissionRate {
1315        info: Some(data.clone()),
1316        symbol: market.symbol.clone(),
1317        maker_commission_rate,
1318        taker_commission_rate,
1319    })
1320}
1321
1322// ============================================================================
1323// Risk and Limits Query Parser Functions
1324// ============================================================================
1325
1326/// Parse open interest data from Binance futures API.
1327///
1328/// # Arguments
1329///
1330/// * `data` - Binance open interest data JSON object
1331/// * `market` - Market information for symbol resolution
1332///
1333/// # Returns
1334///
1335/// Returns a CCXT [`OpenInterest`] structure.
1336///
1337/// # Binance Response Example
1338///
1339/// ```json
1340/// {
1341///   "openInterest": "10659.509",
1342///   "symbol": "BTCUSDT",
1343///   "time": 1589437530011
1344/// }
1345/// ```
1346pub fn parse_open_interest(data: &Value, market: &Market) -> Result<OpenInterest> {
1347    let open_interest = data["openInterest"]
1348        .as_str()
1349        .and_then(|s| s.parse::<f64>().ok())
1350        .or_else(|| data["openInterest"].as_f64())
1351        .unwrap_or(0.0);
1352
1353    let timestamp = data["time"]
1354        .as_i64()
1355        .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1356
1357    let contract_size = market
1358        .contract_size
1359        .unwrap_or_else(|| rust_decimal::Decimal::from(1))
1360        .to_f64()
1361        .unwrap_or(1.0);
1362    let open_interest_value = open_interest * contract_size;
1363
1364    Ok(OpenInterest {
1365        info: Some(data.clone()),
1366        symbol: market.symbol.clone(),
1367        open_interest,
1368        open_interest_value,
1369        timestamp,
1370    })
1371}
1372
1373/// Parse open interest history data from Binance futures API.
1374///
1375/// # Arguments
1376///
1377/// * `data` - Binance open interest history data JSON array
1378/// * `market` - Market information for symbol resolution
1379///
1380/// # Returns
1381///
1382/// Returns a vector of [`OpenInterestHistory`] structures.
1383///
1384/// # Binance Response Example
1385///
1386/// ```json
1387/// [
1388///   {
1389///     "symbol": "BTCUSDT",
1390///     "sumOpenInterest": "10659.509",
1391///     "sumOpenInterestValue": "106595090.00",
1392///     "timestamp": 1589437530011
1393///   }
1394/// ]
1395/// ```
1396pub fn parse_open_interest_history(
1397    data: &Value,
1398    market: &Market,
1399) -> Result<Vec<OpenInterestHistory>> {
1400    let array = data.as_array().ok_or_else(|| {
1401        Error::from(ParseError::invalid_value(
1402            "data",
1403            "Expected array for open interest history",
1404        ))
1405    })?;
1406
1407    let mut result = Vec::new();
1408
1409    for item in array {
1410        let sum_open_interest = item["sumOpenInterest"]
1411            .as_str()
1412            .and_then(|s| s.parse::<f64>().ok())
1413            .or_else(|| item["sumOpenInterest"].as_f64())
1414            .unwrap_or(0.0);
1415
1416        let sum_open_interest_value = item["sumOpenInterestValue"]
1417            .as_str()
1418            .and_then(|s| s.parse::<f64>().ok())
1419            .or_else(|| item["sumOpenInterestValue"].as_f64())
1420            .unwrap_or(0.0);
1421
1422        let timestamp = item["timestamp"]
1423            .as_i64()
1424            .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1425
1426        result.push(OpenInterestHistory {
1427            info: Some(item.clone()),
1428            symbol: market.symbol.clone(),
1429            sum_open_interest,
1430            sum_open_interest_value,
1431            timestamp,
1432        });
1433    }
1434
1435    Ok(result)
1436}
1437
1438/// Parse maximum leverage data from Binance leverage brackets.
1439///
1440/// # Arguments
1441///
1442/// * `data` - Binance leverage bracket data JSON (array or object)
1443/// * `market` - Market information for symbol matching
1444///
1445/// # Returns
1446///
1447/// Returns a CCXT [`MaxLeverage`] structure.
1448///
1449/// # Binance Response Example
1450///
1451/// ```json
1452/// [
1453///   {
1454///     "symbol": "BTCUSDT",
1455///     "brackets": [
1456///       {
1457///         "bracket": 1,
1458///         "initialLeverage": 125,
1459///         "notionalCap": 50000,
1460///         "notionalFloor": 0,
1461///         "maintMarginRatio": 0.004
1462///       }
1463///     ]
1464///   }
1465/// ]
1466/// ```
1467pub fn parse_max_leverage(data: &Value, market: &Market) -> Result<MaxLeverage> {
1468    let target_data = if let Some(array) = data.as_array() {
1469        array
1470            .iter()
1471            .find(|item| item["symbol"].as_str().unwrap_or("") == market.id)
1472            .ok_or_else(|| {
1473                Error::from(ParseError::invalid_value(
1474                    "symbol",
1475                    format!("Symbol {} not found in leverage brackets", market.id),
1476                ))
1477            })?
1478    } else {
1479        data
1480    };
1481
1482    let brackets = target_data["brackets"]
1483        .as_array()
1484        .ok_or_else(|| Error::from(ParseError::missing_field("brackets")))?;
1485
1486    if brackets.is_empty() {
1487        return Err(Error::from(ParseError::invalid_value(
1488            "data",
1489            "Empty brackets array",
1490        )));
1491    }
1492
1493    let first_bracket = &brackets[0];
1494    let max_leverage = first_bracket["initialLeverage"].as_i64().unwrap_or(1) as i32;
1495
1496    let notional = first_bracket["notionalCap"]
1497        .as_f64()
1498        .or_else(|| {
1499            first_bracket["notionalCap"]
1500                .as_str()
1501                .and_then(|s| s.parse::<f64>().ok())
1502        })
1503        .unwrap_or(0.0);
1504
1505    Ok(MaxLeverage {
1506        info: Some(data.clone()),
1507        symbol: market.symbol.clone(),
1508        max_leverage,
1509        notional,
1510    })
1511}
1512
1513// ============================================================================
1514// Futures Market Data Parser Functions
1515// ============================================================================
1516
1517/// Parse index price data from Binance futures API.
1518///
1519/// # Arguments
1520///
1521/// * `data` - Binance index price data JSON object
1522/// * `market` - Market information for symbol resolution
1523///
1524/// # Returns
1525///
1526/// Returns a CCXT [`IndexPrice`] structure.
1527pub fn parse_index_price(data: &Value, market: &Market) -> Result<IndexPrice> {
1528    let index_price = data["indexPrice"]
1529        .as_f64()
1530        .or_else(|| {
1531            data["indexPrice"]
1532                .as_str()
1533                .and_then(|s| s.parse::<f64>().ok())
1534        })
1535        .ok_or_else(|| Error::from(ParseError::missing_field("indexPrice")))?;
1536
1537    let timestamp = data["time"]
1538        .as_i64()
1539        .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1540
1541    Ok(IndexPrice {
1542        info: Some(data.clone()),
1543        symbol: market.symbol.clone(),
1544        index_price,
1545        timestamp,
1546    })
1547}
1548
1549/// Parse premium index data from Binance futures API.
1550///
1551/// # Arguments
1552///
1553/// * `data` - Binance premium index data JSON object
1554/// * `market` - Market information for symbol resolution
1555///
1556/// # Returns
1557///
1558/// Returns a CCXT [`PremiumIndex`] structure.
1559pub fn parse_premium_index(data: &Value, market: &Market) -> Result<PremiumIndex> {
1560    let mark_price = data["markPrice"]
1561        .as_f64()
1562        .or_else(|| {
1563            data["markPrice"]
1564                .as_str()
1565                .and_then(|s| s.parse::<f64>().ok())
1566        })
1567        .ok_or_else(|| Error::from(ParseError::missing_field("markPrice")))?;
1568
1569    let index_price = data["indexPrice"]
1570        .as_f64()
1571        .or_else(|| {
1572            data["indexPrice"]
1573                .as_str()
1574                .and_then(|s| s.parse::<f64>().ok())
1575        })
1576        .ok_or_else(|| Error::from(ParseError::missing_field("indexPrice")))?;
1577
1578    let estimated_settle_price = data["estimatedSettlePrice"]
1579        .as_f64()
1580        .or_else(|| {
1581            data["estimatedSettlePrice"]
1582                .as_str()
1583                .and_then(|s| s.parse::<f64>().ok())
1584        })
1585        .unwrap_or(0.0);
1586
1587    let last_funding_rate = data["lastFundingRate"]
1588        .as_f64()
1589        .or_else(|| {
1590            data["lastFundingRate"]
1591                .as_str()
1592                .and_then(|s| s.parse::<f64>().ok())
1593        })
1594        .unwrap_or(0.0);
1595
1596    // 解析下次资金费率时间
1597    let next_funding_time = data["nextFundingTime"].as_i64().unwrap_or(0);
1598
1599    // 解析当前时间
1600    let time = data["time"]
1601        .as_i64()
1602        .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1603
1604    Ok(PremiumIndex {
1605        info: Some(data.clone()),
1606        symbol: market.symbol.clone(),
1607        mark_price,
1608        index_price,
1609        estimated_settle_price,
1610        last_funding_rate,
1611        next_funding_time,
1612        time,
1613    })
1614}
1615
1616/// Parse liquidation order data from Binance futures API.
1617///
1618/// # Arguments
1619///
1620/// * `data` - Binance liquidation order data JSON object
1621/// * `market` - Market information for symbol resolution
1622///
1623/// # Returns
1624///
1625/// Returns a CCXT [`Liquidation`] structure.
1626pub fn parse_liquidation(data: &Value, market: &Market) -> Result<Liquidation> {
1627    let side = data["side"]
1628        .as_str()
1629        .ok_or_else(|| Error::from(ParseError::missing_field("side")))?
1630        .to_string();
1631
1632    let order_type = data["type"].as_str().unwrap_or("LIMIT").to_string();
1633
1634    let time = data["time"]
1635        .as_i64()
1636        .ok_or_else(|| Error::from(ParseError::missing_field("time")))?;
1637
1638    let price = data["price"]
1639        .as_f64()
1640        .or_else(|| data["price"].as_str().and_then(|s| s.parse::<f64>().ok()))
1641        .ok_or_else(|| Error::from(ParseError::missing_field("price")))?;
1642
1643    let quantity = data["origQty"]
1644        .as_f64()
1645        .or_else(|| data["origQty"].as_str().and_then(|s| s.parse::<f64>().ok()))
1646        .ok_or_else(|| Error::from(ParseError::missing_field("origQty")))?;
1647
1648    let average_price = data["averagePrice"]
1649        .as_f64()
1650        .or_else(|| {
1651            data["averagePrice"]
1652                .as_str()
1653                .and_then(|s| s.parse::<f64>().ok())
1654        })
1655        .unwrap_or(price);
1656
1657    Ok(Liquidation {
1658        info: Some(data.clone()),
1659        symbol: market.symbol.clone(),
1660        side,
1661        order_type,
1662        time,
1663        price,
1664        quantity,
1665        average_price,
1666    })
1667}
1668
1669// ============================================================================
1670// Margin Trading Parser Functions
1671// ============================================================================
1672
1673/// Parse borrow interest rate data from Binance margin API.
1674///
1675/// # Arguments
1676///
1677/// * `data` - Binance borrow rate data JSON object
1678/// * `currency` - Currency code (e.g., "USDT", "BTC")
1679/// * `symbol` - Trading pair symbol (required for isolated margin only)
1680///
1681/// # Returns
1682///
1683/// Returns a CCXT [`BorrowRate`] structure.
1684pub fn parse_borrow_rate(data: &Value, currency: &str, symbol: Option<&str>) -> Result<BorrowRate> {
1685    let rate = if let Some(rate_str) = data["dailyInterestRate"].as_str() {
1686        rate_str.parse::<f64>().unwrap_or(0.0)
1687    } else {
1688        data["dailyInterestRate"].as_f64().unwrap_or(0.0)
1689    };
1690
1691    let timestamp = data["timestamp"]
1692        .as_i64()
1693        .or_else(|| {
1694            data["vipLevel"]
1695                .as_i64()
1696                .map(|_| chrono::Utc::now().timestamp_millis())
1697        })
1698        .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1699
1700    if let Some(sym) = symbol {
1701        Ok(BorrowRate::new_isolated(
1702            currency.to_string(),
1703            sym.to_string(),
1704            rate,
1705            timestamp,
1706            data.clone(),
1707        ))
1708    } else {
1709        Ok(BorrowRate::new_cross(
1710            currency.to_string(),
1711            rate,
1712            timestamp,
1713            data.clone(),
1714        ))
1715    }
1716}
1717
1718/// Parse margin loan record data from Binance margin API.
1719///
1720/// # Arguments
1721///
1722/// * `data` - Binance margin loan record data JSON object
1723///
1724/// # Returns
1725///
1726/// Returns a CCXT [`MarginLoan`] structure.
1727pub fn parse_margin_loan(data: &Value) -> Result<MarginLoan> {
1728    let id = data["tranId"]
1729        .as_i64()
1730        .or_else(|| data["txId"].as_i64())
1731        .map(|id| id.to_string())
1732        .unwrap_or_default();
1733
1734    let currency = data["asset"]
1735        .as_str()
1736        .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
1737        .to_string();
1738
1739    let symbol = data["symbol"].as_str().map(|s| s.to_string());
1740
1741    let amount = if let Some(amount_str) = data["amount"].as_str() {
1742        amount_str.parse::<f64>().unwrap_or(0.0)
1743    } else {
1744        data["amount"].as_f64().unwrap_or(0.0)
1745    };
1746
1747    let timestamp = data["timestamp"]
1748        .as_i64()
1749        .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1750
1751    let status = data["status"].as_str().unwrap_or("CONFIRMED").to_string();
1752
1753    Ok(MarginLoan::new(
1754        id,
1755        currency,
1756        symbol,
1757        amount,
1758        timestamp,
1759        status,
1760        data.clone(),
1761    ))
1762}
1763
1764/// Parse borrow interest accrual data from Binance margin API.
1765///
1766/// # Arguments
1767///
1768/// * `data` - Binance borrow interest data JSON object
1769///
1770/// # Returns
1771///
1772/// Returns a CCXT [`BorrowInterest`] structure.
1773pub fn parse_borrow_interest(data: &Value) -> Result<BorrowInterest> {
1774    let id = data["txId"]
1775        .as_i64()
1776        .or_else(|| {
1777            data["isolatedSymbol"]
1778                .as_str()
1779                .map(|_| chrono::Utc::now().timestamp_millis())
1780        })
1781        .map(|id| id.to_string())
1782        .unwrap_or_default();
1783
1784    let currency = data["asset"]
1785        .as_str()
1786        .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
1787        .to_string();
1788
1789    let symbol = data["isolatedSymbol"].as_str().map(|s| s.to_string());
1790
1791    let interest = if let Some(interest_str) = data["interest"].as_str() {
1792        interest_str.parse::<f64>().unwrap_or(0.0)
1793    } else {
1794        data["interest"].as_f64().unwrap_or(0.0)
1795    };
1796
1797    let interest_rate = if let Some(rate_str) = data["interestRate"].as_str() {
1798        rate_str.parse::<f64>().unwrap_or(0.0)
1799    } else {
1800        data["interestRate"].as_f64().unwrap_or(0.0)
1801    };
1802
1803    let principal = if let Some(principal_str) = data["principal"].as_str() {
1804        principal_str.parse::<f64>().unwrap_or(0.0)
1805    } else {
1806        data["principal"].as_f64().unwrap_or(0.0)
1807    };
1808
1809    let timestamp = data["interestAccuredTime"]
1810        .as_i64()
1811        .or_else(|| data["timestamp"].as_i64())
1812        .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1813
1814    Ok(BorrowInterest::new(
1815        id,
1816        currency,
1817        symbol,
1818        interest,
1819        interest_rate,
1820        principal,
1821        timestamp,
1822        data.clone(),
1823    ))
1824}
1825
1826/// Parse margin adjustment (transfer) data from Binance margin API.
1827///
1828/// # Arguments
1829///
1830/// * `data` - Binance margin adjustment data JSON object
1831///
1832/// # Returns
1833///
1834/// Returns a CCXT [`MarginAdjustment`] structure.
1835pub fn parse_margin_adjustment(data: &Value) -> Result<MarginAdjustment> {
1836    let id = data["tranId"]
1837        .as_i64()
1838        .or_else(|| data["txId"].as_i64())
1839        .map(|id| id.to_string())
1840        .unwrap_or_default();
1841
1842    let symbol = data["symbol"].as_str().map(|s| s.to_string());
1843
1844    let currency = data["asset"]
1845        .as_str()
1846        .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
1847        .to_string();
1848
1849    let amount = if let Some(amount_str) = data["amount"].as_str() {
1850        amount_str.parse::<f64>().unwrap_or(0.0)
1851    } else {
1852        data["amount"].as_f64().unwrap_or(0.0)
1853    };
1854
1855    let transfer_type = data["type"]
1856        .as_str()
1857        .or_else(|| data["transFrom"].as_str())
1858        .map(|t| {
1859            if t.contains("MAIN") || t.eq("1") || t.eq("ROLL_IN") {
1860                "IN"
1861            } else {
1862                "OUT"
1863            }
1864        })
1865        .unwrap_or("IN")
1866        .to_string();
1867
1868    let timestamp = data["timestamp"]
1869        .as_i64()
1870        .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1871
1872    let status = data["status"].as_str().unwrap_or("SUCCESS").to_string();
1873
1874    Ok(MarginAdjustment::new(
1875        id,
1876        symbol,
1877        currency,
1878        amount,
1879        transfer_type,
1880        timestamp,
1881        status,
1882        data.clone(),
1883    ))
1884}
1885// ============================================================================
1886// Account Management Parser Functions
1887// ============================================================================
1888
1889/// Parse futures transfer type code from Binance API.
1890/// Converts Binance futures transfer API `type` parameter (1-4) to
1891/// source and destination account names.
1892///
1893/// # Arguments
1894///
1895/// * `transfer_type` - Transfer type code:
1896///   - 1: Spot account → USDT-M futures account
1897///   - 2: USDT-M futures account → Spot account
1898///   - 3: Spot account → COIN-M futures account
1899///   - 4: COIN-M futures account → Spot account
1900///
1901/// # Returns
1902///
1903/// Returns a tuple of `(from_account, to_account)`.
1904///
1905/// # Example
1906///
1907/// ```
1908/// use ccxt_exchanges::binance::parser::parse_futures_transfer_type;
1909/// let (from, to) = parse_futures_transfer_type(1).unwrap();
1910/// assert_eq!(from, "spot");
1911/// assert_eq!(to, "future");
1912/// ```
1913pub fn parse_futures_transfer_type(transfer_type: i32) -> Result<(&'static str, &'static str)> {
1914    match transfer_type {
1915        1 => Ok(("spot", "future")),
1916        2 => Ok(("future", "spot")),
1917        3 => Ok(("spot", "delivery")),
1918        4 => Ok(("delivery", "spot")),
1919        _ => Err(Error::invalid_request(format!(
1920            "Invalid futures transfer type: {}. Must be between 1 and 4",
1921            transfer_type
1922        ))),
1923    }
1924}
1925
1926/// Parse transfer record data from Binance universal transfer API.
1927///
1928/// # Arguments
1929///
1930/// * `data` - Binance transfer record data JSON object
1931///
1932/// # Returns
1933///
1934/// Returns a CCXT [`Transfer`] structure.
1935///
1936/// # Binance Response Format
1937///
1938/// ```json
1939/// // transfer response (POST /sapi/v1/asset/transfer)
1940/// {
1941///   "tranId": 13526853623
1942/// }
1943///
1944/// // fetchTransfers response (GET /sapi/v1/asset/transfer)
1945/// {
1946///   "timestamp": 1614640878000,
1947///   "asset": "USDT",
1948///   "amount": "25",
1949///   "type": "MAIN_UMFUTURE",
1950///   "status": "CONFIRMED",
1951///   "tranId": 43000126248
1952/// }
1953/// ```
1954pub fn parse_transfer(data: &Value) -> Result<Transfer> {
1955    let id = data["tranId"]
1956        .as_i64()
1957        .or_else(|| data["txId"].as_i64())
1958        .or_else(|| data["transactionId"].as_i64())
1959        .map(|id| id.to_string());
1960
1961    let timestamp = data["timestamp"]
1962        .as_i64()
1963        .or_else(|| data["transactionTime"].as_i64())
1964        .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1965
1966    let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
1967        .map(|dt| dt.to_rfc3339())
1968        .unwrap_or_default();
1969
1970    let currency = data["asset"]
1971        .as_str()
1972        .or_else(|| data["currency"].as_str())
1973        .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
1974        .to_string();
1975
1976    let amount = if let Some(amount_str) = data["amount"].as_str() {
1977        amount_str.parse::<f64>().unwrap_or(0.0)
1978    } else {
1979        data["amount"].as_f64().unwrap_or(0.0)
1980    };
1981
1982    let mut from_account = data["fromAccountType"].as_str().map(|s| s.to_string());
1983
1984    let mut to_account = data["toAccountType"].as_str().map(|s| s.to_string());
1985
1986    // If fromAccountType/toAccountType not present, parse from type field (e.g., "MAIN_UMFUTURE")
1987    if from_account.is_none() || to_account.is_none() {
1988        if let Some(type_str) = data["type"].as_str() {
1989            let parts: Vec<&str> = type_str.split('_').collect();
1990            if parts.len() == 2 {
1991                from_account = Some(parts[0].to_lowercase());
1992                to_account = Some(parts[1].to_lowercase());
1993            }
1994        }
1995    }
1996
1997    let status = data["status"].as_str().unwrap_or("SUCCESS").to_lowercase();
1998
1999    Ok(Transfer {
2000        id,
2001        timestamp: timestamp,
2002        datetime,
2003        currency,
2004        amount,
2005        from_account,
2006        to_account,
2007        status,
2008        info: Some(data.clone()),
2009    })
2010}
2011
2012/// Parse maximum borrowable amount data from Binance margin API.
2013///
2014/// # Arguments
2015///
2016/// * `data` - Binance max borrowable amount data JSON object
2017/// * `currency` - Currency code (e.g., "USDT", "BTC")
2018/// * `symbol` - Trading pair symbol (required for isolated margin)
2019///
2020/// # Returns
2021///
2022/// Returns a CCXT [`MaxBorrowable`] structure.
2023pub fn parse_max_borrowable(
2024    data: &Value,
2025    currency: &str,
2026    symbol: Option<String>,
2027) -> Result<MaxBorrowable> {
2028    let amount = if let Some(amount_str) = data["amount"].as_str() {
2029        amount_str.parse::<f64>().unwrap_or(0.0)
2030    } else {
2031        data["amount"].as_f64().unwrap_or(0.0)
2032    };
2033
2034    let borrow_limit = if let Some(limit_str) = data["borrowLimit"].as_str() {
2035        Some(limit_str.parse::<f64>().unwrap_or(0.0))
2036    } else {
2037        data["borrowLimit"].as_f64()
2038    };
2039
2040    let timestamp = chrono::Utc::now().timestamp_millis();
2041    let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
2042        .map(|dt| dt.to_rfc3339())
2043        .unwrap_or_default();
2044
2045    Ok(MaxBorrowable {
2046        currency: currency.to_string(),
2047        amount,
2048        borrow_limit,
2049        symbol,
2050        timestamp,
2051        datetime,
2052        info: data.clone(),
2053    })
2054}
2055
2056/// Parse maximum transferable amount data from Binance margin API.
2057///
2058/// # Arguments
2059///
2060/// * `data` - Binance max transferable amount data JSON object
2061/// * `currency` - Currency code (e.g., "USDT", "BTC")
2062/// * `symbol` - Trading pair symbol (required for isolated margin)
2063///
2064/// # Returns
2065///
2066/// Returns a CCXT [`MaxTransferable`] structure.
2067pub fn parse_max_transferable(
2068    data: &Value,
2069    currency: &str,
2070    symbol: Option<String>,
2071) -> Result<MaxTransferable> {
2072    let amount = if let Some(amount_str) = data["amount"].as_str() {
2073        amount_str.parse::<f64>().unwrap_or(0.0)
2074    } else {
2075        data["amount"].as_f64().unwrap_or(0.0)
2076    };
2077
2078    let timestamp = chrono::Utc::now().timestamp_millis();
2079    let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
2080        .map(|dt| dt.to_rfc3339())
2081        .unwrap_or_default();
2082
2083    Ok(MaxTransferable {
2084        currency: currency.to_string(),
2085        amount,
2086        symbol,
2087        timestamp,
2088        datetime,
2089        info: data.clone(),
2090    })
2091}
2092
2093/// 解析余额信息(支持多种账户类型)
2094///
2095/// # Arguments
2096///
2097/// * `data` - Binance余额JSON数据
2098/// * `account_type` - 账户类型(spot, margin, isolated, futures等)
2099///
2100/// # Returns
2101///
2102/// # Returns
2103///
2104/// Returns a CCXT [`Balance`] structure.
2105pub fn parse_balance_with_type(data: &Value, account_type: &str) -> Result<Balance> {
2106    let mut balances = HashMap::new();
2107    let _timestamp = chrono::Utc::now().timestamp_millis();
2108
2109    match account_type {
2110        "spot" => {
2111            if let Some(balances_array) = data["balances"].as_array() {
2112                for item in balances_array {
2113                    if let Some(asset) = item["asset"].as_str() {
2114                        let free = if let Some(free_str) = item["free"].as_str() {
2115                            free_str.parse::<f64>().unwrap_or(0.0)
2116                        } else {
2117                            item["free"].as_f64().unwrap_or(0.0)
2118                        };
2119
2120                        let locked = if let Some(locked_str) = item["locked"].as_str() {
2121                            locked_str.parse::<f64>().unwrap_or(0.0)
2122                        } else {
2123                            item["locked"].as_f64().unwrap_or(0.0)
2124                        };
2125
2126                        balances.insert(
2127                            asset.to_string(),
2128                            BalanceEntry {
2129                                free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2130                                used: Decimal::from_f64(locked).unwrap_or(Decimal::ZERO),
2131                                total: Decimal::from_f64(free + locked).unwrap_or(Decimal::ZERO),
2132                            },
2133                        );
2134                    }
2135                }
2136            }
2137        }
2138        "margin" | "cross" => {
2139            if let Some(user_assets) = data["userAssets"].as_array() {
2140                for item in user_assets {
2141                    if let Some(asset) = item["asset"].as_str() {
2142                        let free = if let Some(free_str) = item["free"].as_str() {
2143                            free_str.parse::<f64>().unwrap_or(0.0)
2144                        } else {
2145                            item["free"].as_f64().unwrap_or(0.0)
2146                        };
2147
2148                        let locked = if let Some(locked_str) = item["locked"].as_str() {
2149                            locked_str.parse::<f64>().unwrap_or(0.0)
2150                        } else {
2151                            item["locked"].as_f64().unwrap_or(0.0)
2152                        };
2153
2154                        balances.insert(
2155                            asset.to_string(),
2156                            BalanceEntry {
2157                                free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2158                                used: Decimal::from_f64(locked).unwrap_or(Decimal::ZERO),
2159                                total: Decimal::from_f64(free + locked).unwrap_or(Decimal::ZERO),
2160                            },
2161                        );
2162                    }
2163                }
2164            }
2165        }
2166        "isolated" => {
2167            if let Some(assets) = data["assets"].as_array() {
2168                for item in assets {
2169                    if let Some(base_asset) = item["baseAsset"].as_object() {
2170                        if let Some(asset) = base_asset["asset"].as_str() {
2171                            let free = if let Some(free_str) = base_asset["free"].as_str() {
2172                                free_str.parse::<f64>().unwrap_or(0.0)
2173                            } else {
2174                                base_asset["free"].as_f64().unwrap_or(0.0)
2175                            };
2176
2177                            let locked = if let Some(locked_str) = base_asset["locked"].as_str() {
2178                                locked_str.parse::<f64>().unwrap_or(0.0)
2179                            } else {
2180                                base_asset["locked"].as_f64().unwrap_or(0.0)
2181                            };
2182
2183                            balances.insert(
2184                                asset.to_string(),
2185                                BalanceEntry {
2186                                    free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2187                                    used: Decimal::from_f64(locked).unwrap_or(Decimal::ZERO),
2188                                    total: Decimal::from_f64(free + locked)
2189                                        .unwrap_or(Decimal::ZERO),
2190                                },
2191                            );
2192                        }
2193                    }
2194
2195                    if let Some(quote_asset) = item["quoteAsset"].as_object() {
2196                        if let Some(asset) = quote_asset["asset"].as_str() {
2197                            let free = if let Some(free_str) = quote_asset["free"].as_str() {
2198                                free_str.parse::<f64>().unwrap_or(0.0)
2199                            } else {
2200                                quote_asset["free"].as_f64().unwrap_or(0.0)
2201                            };
2202
2203                            let locked = if let Some(locked_str) = quote_asset["locked"].as_str() {
2204                                locked_str.parse::<f64>().unwrap_or(0.0)
2205                            } else {
2206                                quote_asset["locked"].as_f64().unwrap_or(0.0)
2207                            };
2208
2209                            balances.insert(
2210                                asset.to_string(),
2211                                BalanceEntry {
2212                                    free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2213                                    used: Decimal::from_f64(locked).unwrap_or(Decimal::ZERO),
2214                                    total: Decimal::from_f64(free + locked)
2215                                        .unwrap_or(Decimal::ZERO),
2216                                },
2217                            );
2218                        }
2219                    }
2220                }
2221            }
2222        }
2223        "linear" | "future" => {
2224            // Handle both array response (from /fapi/v2/balance) and object with assets field
2225            let assets = if let Some(arr) = data.as_array() {
2226                arr.clone()
2227            } else if let Some(arr) = data["assets"].as_array() {
2228                arr.clone()
2229            } else {
2230                vec![]
2231            };
2232
2233            for item in &assets {
2234                if let Some(asset) = item["asset"].as_str() {
2235                    let available_balance =
2236                        if let Some(balance_str) = item["availableBalance"].as_str() {
2237                            balance_str.parse::<f64>().unwrap_or(0.0)
2238                        } else {
2239                            item["availableBalance"].as_f64().unwrap_or(0.0)
2240                        };
2241
2242                    let wallet_balance = if let Some(balance_str) = item["walletBalance"].as_str() {
2243                        balance_str.parse::<f64>().unwrap_or(0.0)
2244                    } else {
2245                        item["walletBalance"].as_f64().unwrap_or(0.0)
2246                    };
2247
2248                    // Also check balance field as fallback
2249                    let wallet_balance = if wallet_balance == 0.0 {
2250                        if let Some(balance_str) = item["balance"].as_str() {
2251                            balance_str.parse::<f64>().unwrap_or(0.0)
2252                        } else {
2253                            item["balance"].as_f64().unwrap_or(wallet_balance)
2254                        }
2255                    } else {
2256                        wallet_balance
2257                    };
2258
2259                    let used = wallet_balance - available_balance;
2260
2261                    balances.insert(
2262                        asset.to_string(),
2263                        BalanceEntry {
2264                            free: Decimal::from_f64(available_balance).unwrap_or(Decimal::ZERO),
2265                            used: Decimal::from_f64(used).unwrap_or(Decimal::ZERO),
2266                            total: Decimal::from_f64(wallet_balance).unwrap_or(Decimal::ZERO),
2267                        },
2268                    );
2269                }
2270            }
2271        }
2272        "inverse" | "delivery" => {
2273            // Handle both array response and object with assets field
2274            let assets = if let Some(arr) = data.as_array() {
2275                arr.clone()
2276            } else if let Some(arr) = data["assets"].as_array() {
2277                arr.clone()
2278            } else {
2279                vec![]
2280            };
2281
2282            for item in &assets {
2283                if let Some(asset) = item["asset"].as_str() {
2284                    let available_balance =
2285                        if let Some(balance_str) = item["availableBalance"].as_str() {
2286                            balance_str.parse::<f64>().unwrap_or(0.0)
2287                        } else {
2288                            item["availableBalance"].as_f64().unwrap_or(0.0)
2289                        };
2290
2291                    let wallet_balance = if let Some(balance_str) = item["walletBalance"].as_str() {
2292                        balance_str.parse::<f64>().unwrap_or(0.0)
2293                    } else {
2294                        item["walletBalance"].as_f64().unwrap_or(0.0)
2295                    };
2296
2297                    // Also check balance field as fallback
2298                    let wallet_balance = if wallet_balance == 0.0 {
2299                        if let Some(balance_str) = item["balance"].as_str() {
2300                            balance_str.parse::<f64>().unwrap_or(0.0)
2301                        } else {
2302                            item["balance"].as_f64().unwrap_or(wallet_balance)
2303                        }
2304                    } else {
2305                        wallet_balance
2306                    };
2307
2308                    let used = wallet_balance - available_balance;
2309
2310                    balances.insert(
2311                        asset.to_string(),
2312                        BalanceEntry {
2313                            free: Decimal::from_f64(available_balance).unwrap_or(Decimal::ZERO),
2314                            used: Decimal::from_f64(used).unwrap_or(Decimal::ZERO),
2315                            total: Decimal::from_f64(wallet_balance).unwrap_or(Decimal::ZERO),
2316                        },
2317                    );
2318                }
2319            }
2320        }
2321        "funding" => {
2322            if let Some(assets) = data.as_array() {
2323                for item in assets {
2324                    if let Some(asset) = item["asset"].as_str() {
2325                        let free = if let Some(free_str) = item["free"].as_str() {
2326                            free_str.parse::<f64>().unwrap_or(0.0)
2327                        } else {
2328                            item["free"].as_f64().unwrap_or(0.0)
2329                        };
2330
2331                        let locked = if let Some(locked_str) = item["locked"].as_str() {
2332                            locked_str.parse::<f64>().unwrap_or(0.0)
2333                        } else {
2334                            item["locked"].as_f64().unwrap_or(0.0)
2335                        };
2336
2337                        let total = free + locked;
2338
2339                        balances.insert(
2340                            asset.to_string(),
2341                            BalanceEntry {
2342                                free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2343                                used: Decimal::from_f64(locked).unwrap_or(Decimal::ZERO),
2344                                total: Decimal::from_f64(total).unwrap_or(Decimal::ZERO),
2345                            },
2346                        );
2347                    }
2348                }
2349            }
2350        }
2351        "option" => {
2352            // Options account uses equity and available fields
2353            if let Some(asset) = data["asset"].as_str() {
2354                let equity = if let Some(equity_str) = data["equity"].as_str() {
2355                    equity_str.parse::<f64>().unwrap_or(0.0)
2356                } else {
2357                    data["equity"].as_f64().unwrap_or(0.0)
2358                };
2359
2360                let available = if let Some(available_str) = data["available"].as_str() {
2361                    available_str.parse::<f64>().unwrap_or(0.0)
2362                } else {
2363                    data["available"].as_f64().unwrap_or(0.0)
2364                };
2365
2366                let used = equity - available;
2367
2368                balances.insert(
2369                    asset.to_string(),
2370                    BalanceEntry {
2371                        free: Decimal::from_f64(available).unwrap_or(Decimal::ZERO),
2372                        used: Decimal::from_f64(used).unwrap_or(Decimal::ZERO),
2373                        total: Decimal::from_f64(equity).unwrap_or(Decimal::ZERO),
2374                    },
2375                );
2376            }
2377        }
2378        "portfolio" => {
2379            // Portfolio margin balance - can be array or single object
2380            let assets = if let Some(arr) = data.as_array() {
2381                arr.clone()
2382            } else if data.is_object() {
2383                vec![data.clone()]
2384            } else {
2385                vec![]
2386            };
2387
2388            for item in &assets {
2389                if let Some(asset) = item["asset"].as_str() {
2390                    let total_wallet_balance =
2391                        if let Some(balance_str) = item["totalWalletBalance"].as_str() {
2392                            balance_str.parse::<f64>().unwrap_or(0.0)
2393                        } else {
2394                            item["totalWalletBalance"].as_f64().unwrap_or(0.0)
2395                        };
2396
2397                    let available_balance =
2398                        if let Some(balance_str) = item["availableBalance"].as_str() {
2399                            balance_str.parse::<f64>().unwrap_or(0.0)
2400                        } else {
2401                            item["availableBalance"].as_f64().unwrap_or(0.0)
2402                        };
2403
2404                    // Fallback to crossWalletBalance if availableBalance not present
2405                    let free = if available_balance > 0.0 {
2406                        available_balance
2407                    } else if let Some(cross_str) = item["crossWalletBalance"].as_str() {
2408                        cross_str.parse::<f64>().unwrap_or(0.0)
2409                    } else {
2410                        item["crossWalletBalance"].as_f64().unwrap_or(0.0)
2411                    };
2412
2413                    let used = total_wallet_balance - free;
2414
2415                    balances.insert(
2416                        asset.to_string(),
2417                        BalanceEntry {
2418                            free: Decimal::from_f64(free).unwrap_or(Decimal::ZERO),
2419                            used: Decimal::from_f64(used).unwrap_or(Decimal::ZERO),
2420                            total: Decimal::from_f64(total_wallet_balance).unwrap_or(Decimal::ZERO),
2421                        },
2422                    );
2423                }
2424            }
2425        }
2426        _ => {
2427            return Err(Error::from(ParseError::invalid_value(
2428                "account_type",
2429                format!("Unsupported account type: {}", account_type),
2430            )));
2431        }
2432    }
2433
2434    let mut info_map = HashMap::new();
2435    if let Some(obj) = data.as_object() {
2436        for (k, v) in obj {
2437            info_map.insert(k.clone(), v.clone());
2438        }
2439    }
2440
2441    Ok(Balance {
2442        balances,
2443        info: info_map,
2444    })
2445}
2446
2447// ============================================================================
2448// Market Data Parser Functions
2449// ============================================================================
2450
2451/// Parse bid/ask price data from Binance ticker API.
2452///
2453/// # Arguments
2454///
2455/// * `data` - Binance bid/ask ticker data JSON object
2456///
2457/// # Returns
2458///
2459/// Returns a CCXT [`BidAsk`](ccxt_core::types::BidAsk) structure.
2460pub fn parse_bid_ask(data: &Value) -> Result<ccxt_core::types::BidAsk> {
2461    use ccxt_core::types::BidAsk;
2462
2463    let symbol = data["symbol"]
2464        .as_str()
2465        .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
2466        .to_string();
2467
2468    let formatted_symbol = if symbol.len() >= 6 {
2469        let quote_currencies = ["USDT", "BUSD", "USDC", "BTC", "ETH", "BNB"];
2470        let mut found = false;
2471        let mut formatted = symbol.clone();
2472
2473        for quote in &quote_currencies {
2474            if symbol.ends_with(quote) {
2475                let base = &symbol[..symbol.len() - quote.len()];
2476                formatted = format!("{}/{}", base, quote);
2477                found = true;
2478                break;
2479            }
2480        }
2481
2482        if !found { symbol.clone() } else { formatted }
2483    } else {
2484        symbol.clone()
2485    };
2486
2487    let bid_price = parse_decimal(data, "bidPrice").unwrap_or(Decimal::ZERO);
2488    let bid_quantity = parse_decimal(data, "bidQty").unwrap_or(Decimal::ZERO);
2489    let ask_price = parse_decimal(data, "askPrice").unwrap_or(Decimal::ZERO);
2490    let ask_quantity = parse_decimal(data, "askQty").unwrap_or(Decimal::ZERO);
2491
2492    let timestamp = data["time"]
2493        .as_i64()
2494        .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
2495
2496    Ok(BidAsk {
2497        symbol: formatted_symbol,
2498        bid_price,
2499        bid_quantity,
2500        ask_price,
2501        ask_quantity,
2502        timestamp,
2503    })
2504}
2505
2506/// Parse multiple bid/ask price data entries from Binance ticker API.
2507///
2508/// # Arguments
2509///
2510/// * `data` - Binance bid/ask ticker data (array or single object)
2511///
2512/// # Returns
2513///
2514/// Returns a vector of CCXT [`BidAsk`](ccxt_core::types::BidAsk) structures.
2515pub fn parse_bids_asks(data: &Value) -> Result<Vec<ccxt_core::types::BidAsk>> {
2516    if let Some(array) = data.as_array() {
2517        array.iter().map(|item| parse_bid_ask(item)).collect()
2518    } else {
2519        Ok(vec![parse_bid_ask(data)?])
2520    }
2521}
2522
2523/// Parse latest price data from Binance ticker API.
2524///
2525/// # Arguments
2526///
2527/// * `data` - Binance price ticker data JSON object
2528///
2529/// # Returns
2530///
2531/// Returns a CCXT [`LastPrice`](ccxt_core::types::LastPrice) structure.
2532pub fn parse_last_price(data: &Value) -> Result<ccxt_core::types::LastPrice> {
2533    use ccxt_core::types::LastPrice;
2534
2535    let symbol = data["symbol"]
2536        .as_str()
2537        .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
2538        .to_string();
2539
2540    let formatted_symbol = if symbol.len() >= 6 {
2541        let quote_currencies = ["USDT", "BUSD", "USDC", "BTC", "ETH", "BNB"];
2542        let mut found = false;
2543        let mut formatted = symbol.clone();
2544
2545        for quote in &quote_currencies {
2546            if symbol.ends_with(quote) {
2547                let base = &symbol[..symbol.len() - quote.len()];
2548                formatted = format!("{}/{}", base, quote);
2549                found = true;
2550                break;
2551            }
2552        }
2553
2554        if !found { symbol.clone() } else { formatted }
2555    } else {
2556        symbol.clone()
2557    };
2558
2559    let price = parse_decimal(data, "price").unwrap_or(Decimal::ZERO);
2560
2561    let timestamp = data["time"]
2562        .as_i64()
2563        .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
2564
2565    let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
2566        .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string())
2567        .unwrap_or_default();
2568
2569    Ok(LastPrice {
2570        symbol: formatted_symbol,
2571        price,
2572        timestamp,
2573        datetime,
2574    })
2575}
2576
2577/// Parse multiple latest price data entries from Binance ticker API.
2578///
2579/// # Arguments
2580///
2581/// * `data` - Binance price ticker data (array or single object)
2582///
2583/// # Returns
2584///
2585/// Returns a vector of CCXT [`LastPrice`](ccxt_core::types::LastPrice) structures.
2586pub fn parse_last_prices(data: &Value) -> Result<Vec<ccxt_core::types::LastPrice>> {
2587    if let Some(array) = data.as_array() {
2588        array.iter().map(|item| parse_last_price(item)).collect()
2589    } else {
2590        Ok(vec![parse_last_price(data)?])
2591    }
2592}
2593
2594/// Parse mark price data from Binance futures API.
2595///
2596/// # Arguments
2597///
2598/// * `data` - Binance mark price data JSON object
2599///
2600/// # Returns
2601///
2602/// Returns a CCXT [`MarkPrice`](ccxt_core::types::MarkPrice) structure.
2603pub fn parse_mark_price(data: &Value) -> Result<ccxt_core::types::MarkPrice> {
2604    use ccxt_core::types::MarkPrice;
2605
2606    let symbol = data["symbol"]
2607        .as_str()
2608        .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
2609        .to_string();
2610
2611    let formatted_symbol = if symbol.len() >= 6 {
2612        let quote_currencies = ["USDT", "BUSD", "USDC", "BTC", "ETH", "BNB"];
2613        let mut found = false;
2614        let mut formatted = symbol.clone();
2615
2616        for quote in &quote_currencies {
2617            if symbol.ends_with(quote) {
2618                let base = &symbol[..symbol.len() - quote.len()];
2619                formatted = format!("{}/{}", base, quote);
2620                found = true;
2621                break;
2622            }
2623        }
2624
2625        if !found { symbol.clone() } else { formatted }
2626    } else {
2627        symbol.clone()
2628    };
2629
2630    let mark_price = parse_decimal(data, "markPrice").unwrap_or(Decimal::ZERO);
2631    let index_price = parse_decimal(data, "indexPrice");
2632    let estimated_settle_price = parse_decimal(data, "estimatedSettlePrice");
2633    let last_funding_rate = parse_decimal(data, "lastFundingRate");
2634
2635    let next_funding_time = data["nextFundingTime"].as_i64();
2636
2637    let timestamp = data["time"]
2638        .as_i64()
2639        .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
2640
2641    Ok(MarkPrice {
2642        symbol: formatted_symbol,
2643        mark_price,
2644        index_price,
2645        estimated_settle_price,
2646        last_funding_rate,
2647        next_funding_time,
2648        interest_rate: None,
2649        timestamp,
2650    })
2651}
2652
2653/// Parse multiple mark price data entries from Binance futures API.
2654///
2655/// # Arguments
2656///
2657/// * `data` - Binance mark price data (array or single object)
2658///
2659/// # Returns
2660///
2661/// Returns a vector of CCXT [`MarkPrice`](ccxt_core::types::MarkPrice) structures.
2662pub fn parse_mark_prices(data: &Value) -> Result<Vec<ccxt_core::types::MarkPrice>> {
2663    if let Some(array) = data.as_array() {
2664        array.iter().map(|item| parse_mark_price(item)).collect()
2665    } else {
2666        Ok(vec![parse_mark_price(data)?])
2667    }
2668}
2669
2670/// Parse OHLCV (candlestick/kline) data from Binance market API.
2671///
2672/// # Binance API Response Format
2673///
2674/// ```json
2675/// [
2676///   1499040000000,      // Open time
2677///   "0.01634000",       // Open price
2678///   "0.80000000",       // High price
2679///   "0.01575800",       // Low price
2680///   "0.01577100",       // Close price (current price for incomplete candle)
2681///   "148976.11427815",  // Volume
2682///   1499644799999,      // Close time
2683///   "2434.19055334",    // Quote asset volume
2684///   308,                // Number of trades
2685///   "1756.87402397",    // Taker buy base asset volume
2686///   "28.46694368",      // Taker buy quote asset volume
2687///   "17928899.62484339" // Unused field
2688/// ]
2689/// ```
2690///
2691/// # Returns
2692///
2693/// Returns a CCXT [`OHLCV`](ccxt_core::types::OHLCV) structure.
2694pub fn parse_ohlcv(data: &Value) -> Result<ccxt_core::types::OHLCV> {
2695    use ccxt_core::error::{Error, ParseError};
2696    use ccxt_core::types::OHLCV;
2697
2698    if let Some(array) = data.as_array() {
2699        if array.len() < 6 {
2700            return Err(Error::from(ParseError::invalid_format(
2701                "data",
2702                "OHLCV array length insufficient",
2703            )));
2704        }
2705
2706        let timestamp = array[0]
2707            .as_i64()
2708            .ok_or_else(|| Error::from(ParseError::invalid_format("data", "无效的时间戳")))?;
2709
2710        let open = array[1]
2711            .as_str()
2712            .and_then(|s| s.parse::<f64>().ok())
2713            .or_else(|| array[1].as_f64())
2714            .ok_or_else(|| Error::from(ParseError::invalid_format("data", "Invalid open price")))?;
2715
2716        let high = array[2]
2717            .as_str()
2718            .and_then(|s| s.parse::<f64>().ok())
2719            .or_else(|| array[2].as_f64())
2720            .ok_or_else(|| Error::from(ParseError::invalid_format("data", "Invalid high price")))?;
2721
2722        let low = array[3]
2723            .as_str()
2724            .and_then(|s| s.parse::<f64>().ok())
2725            .or_else(|| array[3].as_f64())
2726            .ok_or_else(|| Error::from(ParseError::invalid_format("data", "Invalid low price")))?;
2727
2728        let close = array[4]
2729            .as_str()
2730            .and_then(|s| s.parse::<f64>().ok())
2731            .or_else(|| array[4].as_f64())
2732            .ok_or_else(|| {
2733                Error::from(ParseError::invalid_format("data", "Invalid close price"))
2734            })?;
2735
2736        let volume = array[5]
2737            .as_str()
2738            .and_then(|s| s.parse::<f64>().ok())
2739            .or_else(|| array[5].as_f64())
2740            .ok_or_else(|| Error::from(ParseError::invalid_format("data", "Invalid volume")))?;
2741
2742        Ok(OHLCV::new(timestamp, open, high, low, close, volume))
2743    } else {
2744        Err(Error::from(ParseError::invalid_format(
2745            "data",
2746            "OHLCV data must be in array format",
2747        )))
2748    }
2749}
2750
2751/// Parse multiple OHLCV (candlestick/kline) data entries from Binance market API.
2752///
2753/// # Returns
2754///
2755/// Returns a vector of CCXT [`OHLCV`](ccxt_core::types::OHLCV) structures.
2756pub fn parse_ohlcvs(data: &Value) -> Result<Vec<ccxt_core::types::OHLCV>> {
2757    use ccxt_core::error::{Error, ParseError};
2758
2759    if let Some(array) = data.as_array() {
2760        array.iter().map(|item| parse_ohlcv(item)).collect()
2761    } else {
2762        Err(Error::from(ParseError::invalid_format(
2763            "data",
2764            "OHLCV data list must be in array format",
2765        )))
2766    }
2767}
2768
2769/// Parse trading fee data from Binance API.
2770///
2771/// # Binance API Response Format
2772///
2773/// ```json
2774/// {
2775///   "symbol": "BTCUSDT",
2776///   "makerCommission": "0.001",
2777///   "takerCommission": "0.001"
2778/// }
2779/// ```
2780///
2781/// # Returns
2782///
2783/// Returns a [`FeeTradingFee`] structure.
2784pub fn parse_trading_fee(data: &Value) -> Result<FeeTradingFee> {
2785    use ccxt_core::error::{Error, ParseError};
2786
2787    let symbol = data["symbol"]
2788        .as_str()
2789        .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
2790        .to_string();
2791
2792    let maker = data["makerCommission"]
2793        .as_str()
2794        .and_then(|s| Decimal::from_str(s).ok())
2795        .or_else(|| data["makerCommission"].as_f64().and_then(Decimal::from_f64))
2796        .ok_or_else(|| {
2797            Error::from(ParseError::invalid_format(
2798                "data",
2799                "Invalid maker commission",
2800            ))
2801        })?;
2802
2803    let taker = data["takerCommission"]
2804        .as_str()
2805        .and_then(|s| Decimal::from_str(s).ok())
2806        .or_else(|| data["takerCommission"].as_f64().and_then(Decimal::from_f64))
2807        .ok_or_else(|| {
2808            Error::from(ParseError::invalid_format(
2809                "data",
2810                "Invalid taker commission",
2811            ))
2812        })?;
2813
2814    Ok(FeeTradingFee::new(symbol, maker, taker))
2815}
2816
2817/// Parse multiple trading fee data entries from Binance API.
2818///
2819/// # Returns
2820///
2821/// Returns a vector of [`FeeTradingFee`] structures.
2822pub fn parse_trading_fees(data: &Value) -> Result<Vec<FeeTradingFee>> {
2823    if let Some(array) = data.as_array() {
2824        array.iter().map(|item| parse_trading_fee(item)).collect()
2825    } else {
2826        Ok(vec![parse_trading_fee(data)?])
2827    }
2828}
2829
2830/// Parse server time data from Binance API.
2831///
2832/// # Binance API Response Format
2833///
2834/// ```json
2835/// {
2836///   "serverTime": 1499827319559
2837/// }
2838/// ```
2839///
2840/// # Returns
2841///
2842/// Returns a CCXT [`ServerTime`](ccxt_core::types::ServerTime) structure.
2843pub fn parse_server_time(data: &Value) -> Result<ccxt_core::types::ServerTime> {
2844    use ccxt_core::error::{Error, ParseError};
2845    use ccxt_core::types::ServerTime;
2846
2847    let server_time = data["serverTime"]
2848        .as_i64()
2849        .ok_or_else(|| Error::from(ParseError::missing_field("serverTime")))?;
2850
2851    Ok(ServerTime::new(server_time))
2852}
2853
2854/// Parse order trade history from Binance myTrades endpoint.
2855///
2856/// # Binance API Response Format
2857///
2858/// ```json
2859/// [
2860///   {
2861///     "id": 28457,
2862///     "orderId": 100234,
2863///     "price": "4.00000100",
2864///     "qty": "12.00000000",
2865///     "commission": "10.10000000",
2866///     "commissionAsset": "BNB",
2867///     "time": 1499865549590,
2868///     "isBuyer": true,
2869///     "isMaker": false,
2870///     "isBestMatch": true
2871///   }
2872/// ]
2873/// ```
2874///
2875/// # Returns
2876///
2877/// Returns a vector of CCXT [`Trade`](ccxt_core::types::Trade) structures.
2878pub fn parse_order_trades(
2879    data: &Value,
2880    market: Option<&Market>,
2881) -> Result<Vec<ccxt_core::types::Trade>> {
2882    if let Some(array) = data.as_array() {
2883        array.iter().map(|item| parse_trade(item, market)).collect()
2884    } else {
2885        Ok(vec![parse_trade(data, market)?])
2886    }
2887}
2888
2889/// Parse edit order response from Binance cancelReplace endpoint.
2890///
2891/// # Binance API Response Format
2892///
2893/// ```json
2894/// {
2895///   "cancelResult": "SUCCESS",
2896///   "newOrderResult": "SUCCESS",
2897///   "cancelResponse": {
2898///     "symbol": "BTCUSDT",
2899///     "orderId": 12345,
2900///     ...
2901///   },
2902///   "newOrderResponse": {
2903///     "symbol": "BTCUSDT",
2904///     "orderId": 12346,
2905///     "status": "NEW",
2906///     ...
2907///   }
2908/// }
2909/// ```
2910///
2911/// # Returns
2912///
2913/// Returns the new [`Order`] structure.
2914pub fn parse_edit_order_result(data: &Value, market: Option<&Market>) -> Result<Order> {
2915    let new_order_data = data.get("newOrderResponse").ok_or_else(|| {
2916        Error::from(ParseError::invalid_format(
2917            "data",
2918            "Missing newOrderResponse field",
2919        ))
2920    })?;
2921
2922    parse_order(new_order_data, market)
2923}
2924
2925// ============================================================================
2926// WebSocket Data Parser Functions
2927// ============================================================================
2928
2929/// Parse WebSocket ticker data from Binance streams.
2930///
2931/// Supports multiple Binance WebSocket ticker formats:
2932/// - `24hrTicker`: Full 24-hour ticker statistics
2933/// - `24hrMiniTicker`: Simplified 24-hour ticker statistics
2934/// - `markPriceUpdate`: Mark price updates
2935/// - `bookTicker`: Best bid/ask prices
2936///
2937/// # Arguments
2938///
2939/// * `data` - WebSocket message JSON data
2940/// * `market` - Optional market information
2941///
2942/// # Returns
2943///
2944/// Returns a CCXT [`Ticker`] structure.
2945///
2946/// # Example WebSocket Messages
2947///
2948/// ```json
2949/// // 24hrTicker
2950/// {
2951///     "e": "24hrTicker",
2952///     "E": 1579485598569,
2953///     "s": "ETHBTC",
2954///     "p": "-0.00004000",
2955///     "P": "-0.209",
2956///     "w": "0.01920495",
2957///     "c": "0.01912500",
2958///     "Q": "0.10400000",
2959///     "b": "0.01912200",
2960///     "B": "4.10400000",
2961///     "a": "0.01912500",
2962///     "A": "0.00100000",
2963///     "o": "0.01916500",
2964///     "h": "0.01956500",
2965///     "l": "0.01887700",
2966///     "v": "173518.11900000",
2967///     "q": "3332.40703994",
2968///     "O": 1579399197842,
2969///     "C": 1579485597842,
2970///     "F": 158251292,
2971///     "L": 158414513,
2972///     "n": 163222
2973/// }
2974///
2975/// // markPriceUpdate
2976/// {
2977///     "e": "markPriceUpdate",
2978///     "E": 1562305380000,
2979///     "s": "BTCUSDT",
2980///     "p": "11794.15000000",
2981///     "i": "11784.62659091",
2982///     "P": "11784.25641265",
2983///     "r": "0.00038167",
2984///     "T": 1562306400000
2985/// }
2986///
2987/// // bookTicker
2988/// {
2989///     "u": 400900217,
2990///     "s": "BNBUSDT",
2991///     "b": "25.35190000",
2992///     "B": "31.21000000",
2993///     "a": "25.36520000",
2994///     "A": "40.66000000"
2995/// }
2996/// ```
2997pub fn parse_ws_ticker(data: &Value, market: Option<&Market>) -> Result<Ticker> {
2998    let market_id = data["s"]
2999        .as_str()
3000        .or_else(|| data["symbol"].as_str())
3001        .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?;
3002
3003    let symbol = if let Some(m) = market {
3004        m.symbol.clone()
3005    } else {
3006        market_id.to_string()
3007    };
3008
3009    let event = data["e"].as_str().unwrap_or("bookTicker");
3010
3011    if event == "markPriceUpdate" {
3012        let timestamp = data["E"].as_i64().unwrap_or(0);
3013        return Ok(Ticker {
3014            symbol,
3015            timestamp,
3016            datetime: Some(
3017                chrono::DateTime::from_timestamp_millis(timestamp)
3018                    .map(|dt| dt.to_rfc3339())
3019                    .unwrap_or_default(),
3020            ),
3021            high: None,
3022            low: None,
3023            bid: None,
3024            bid_volume: None,
3025            ask: None,
3026            ask_volume: None,
3027            vwap: None,
3028            open: None,
3029            close: parse_decimal(data, "p").map(Price::from),
3030            last: parse_decimal(data, "p").map(Price::from),
3031            previous_close: None,
3032            change: None,
3033            percentage: None,
3034            average: None,
3035            base_volume: None,
3036            quote_volume: None,
3037            info: value_to_hashmap(data),
3038        });
3039    }
3040
3041    let timestamp = if event == "bookTicker" {
3042        data["E"]
3043            .as_i64()
3044            .or_else(|| data["time"].as_i64())
3045            .unwrap_or_else(|| chrono::Utc::now().timestamp_millis())
3046    } else {
3047        data["C"]
3048            .as_i64()
3049            .or_else(|| data["E"].as_i64())
3050            .or_else(|| data["time"].as_i64())
3051            .unwrap_or_else(|| chrono::Utc::now().timestamp_millis())
3052    };
3053
3054    let last = parse_decimal_multi(data, &["c", "price"]);
3055
3056    Ok(Ticker {
3057        symbol,
3058        timestamp,
3059        datetime: Some(
3060            chrono::DateTime::from_timestamp_millis(timestamp)
3061                .map(|dt| dt.to_rfc3339())
3062                .unwrap_or_default(),
3063        ),
3064        high: parse_decimal(data, "h").map(Price::from),
3065        low: parse_decimal(data, "l").map(Price::from),
3066        bid: parse_decimal_multi(data, &["b", "bidPrice"]).map(Price::from),
3067        bid_volume: parse_decimal_multi(data, &["B", "bidQty"]).map(Amount::from),
3068        ask: parse_decimal_multi(data, &["a", "askPrice"]).map(Price::from),
3069        ask_volume: parse_decimal_multi(data, &["A", "askQty"]).map(Amount::from),
3070        vwap: parse_decimal(data, "w").map(Price::from),
3071        open: parse_decimal(data, "o").map(Price::from),
3072        close: last.map(Price::from),
3073        last: last.map(Price::from),
3074        previous_close: parse_decimal(data, "x").map(Price::from),
3075        change: parse_decimal(data, "p").map(Price::from),
3076        percentage: parse_decimal(data, "P"),
3077        average: None,
3078        base_volume: parse_decimal(data, "v").map(Amount::from),
3079        quote_volume: parse_decimal(data, "q").map(Amount::from),
3080        info: value_to_hashmap(data),
3081    })
3082}
3083
3084/// Parse WebSocket trade data from Binance streams.
3085///
3086/// Supports multiple Binance WebSocket trade formats:
3087/// - `trade`: Public trade stream
3088/// - `aggTrade`: Aggregated trade stream
3089/// - `executionReport`: Private trade execution report
3090///
3091/// # Arguments
3092///
3093/// * `data` - WebSocket message JSON data
3094/// * `market` - Optional market information
3095///
3096/// # Returns
3097///
3098/// Returns a CCXT [`Trade`] structure.
3099///
3100/// # Example WebSocket Messages
3101///
3102/// ```json
3103/// // trade event
3104/// {
3105///     "e": "trade",
3106///     "E": 1579481530911,
3107///     "s": "ETHBTC",
3108///     "t": 158410082,
3109///     "p": "0.01914100",
3110///     "q": "0.00700000",
3111///     "b": 4110671841,
3112///     "a": 4110671533,
3113///     "T": 1579481530910,
3114///     "m": true,
3115///     "M": true
3116/// }
3117///
3118/// // aggTrade event
3119/// {
3120///     "e": "aggTrade",
3121///     "E": 1579481530911,
3122///     "s": "ETHBTC",
3123///     "a": 158410082,
3124///     "p": "0.01914100",
3125///     "q": "0.00700000",
3126///     "f": 4110671841,
3127///     "l": 4110671533,
3128///     "T": 1579481530910,
3129///     "m": true
3130/// }
3131/// ```
3132pub fn parse_ws_trade(data: &Value, market: Option<&Market>) -> Result<Trade> {
3133    let market_id = data["s"]
3134        .as_str()
3135        .or_else(|| data["symbol"].as_str())
3136        .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?;
3137
3138    let symbol = if let Some(m) = market {
3139        m.symbol.clone()
3140    } else {
3141        market_id.to_string()
3142    };
3143
3144    let id = data["t"]
3145        .as_u64()
3146        .or_else(|| data["a"].as_u64())
3147        .map(|i| i.to_string());
3148
3149    let timestamp = data["T"].as_i64().unwrap_or(0);
3150
3151    let price = parse_f64(data, "L")
3152        .or_else(|| parse_f64(data, "p"))
3153        .and_then(Decimal::from_f64_retain);
3154
3155    let amount = parse_f64(data, "q").and_then(Decimal::from_f64_retain);
3156
3157    let cost = match (price, amount) {
3158        (Some(p), Some(a)) => Some(p * a),
3159        _ => None,
3160    };
3161
3162    let side = if data["m"].as_bool().unwrap_or(false) {
3163        OrderSide::Sell
3164    } else {
3165        OrderSide::Buy
3166    };
3167
3168    let taker_or_maker = if data["m"].as_bool().unwrap_or(false) {
3169        Some(TakerOrMaker::Maker)
3170    } else {
3171        Some(TakerOrMaker::Taker)
3172    };
3173
3174    Ok(Trade {
3175        id,
3176        order: data["orderId"]
3177            .as_u64()
3178            .or_else(|| data["orderid"].as_u64())
3179            .map(|i| i.to_string()),
3180        timestamp,
3181        datetime: Some(
3182            chrono::DateTime::from_timestamp_millis(timestamp)
3183                .map(|dt| dt.to_rfc3339())
3184                .unwrap_or_default(),
3185        ),
3186        symbol,
3187        trade_type: None,
3188        side,
3189        taker_or_maker: taker_or_maker,
3190        price: Price::from(price.unwrap_or(Decimal::ZERO)),
3191        amount: Amount::from(amount.unwrap_or(Decimal::ZERO)),
3192        cost: cost.map(Cost::from),
3193        fee: None,
3194        info: value_to_hashmap(data),
3195    })
3196}
3197
3198/// Parse WebSocket order book data from Binance depth stream.
3199///
3200/// # Arguments
3201///
3202/// * `data` - WebSocket message JSON data
3203/// * `symbol` - Trading pair symbol
3204///
3205/// # Returns
3206///
3207/// Returns a CCXT [`OrderBook`] structure.
3208///
3209/// # Example WebSocket Message
3210///
3211/// ```json
3212/// {
3213///     "e": "depthUpdate",
3214///     "E": 1579481530911,
3215///     "s": "ETHBTC",
3216///     "U": 157,
3217///     "u": 160,
3218///     "b": [
3219///         ["0.0024", "10"]
3220///     ],
3221///     "a": [
3222///         ["0.0026", "100"]
3223///     ]
3224/// }
3225/// ```
3226pub fn parse_ws_orderbook(data: &Value, symbol: String) -> Result<OrderBook> {
3227    let timestamp = data["E"]
3228        .as_i64()
3229        .or_else(|| data["T"].as_i64())
3230        .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
3231
3232    let bids = parse_orderbook_side(&data["b"])?;
3233    let asks = parse_orderbook_side(&data["a"])?;
3234
3235    Ok(OrderBook {
3236        symbol,
3237        timestamp,
3238        datetime: Some(
3239            chrono::DateTime::from_timestamp_millis(timestamp)
3240                .map(|dt| dt.to_rfc3339())
3241                .unwrap_or_default(),
3242        ),
3243        nonce: data["u"].as_i64(),
3244        bids,
3245        asks,
3246        buffered_deltas: std::collections::VecDeque::new(),
3247        bids_map: std::collections::BTreeMap::new(),
3248        asks_map: std::collections::BTreeMap::new(),
3249        is_synced: false,
3250        needs_resync: false,
3251        last_resync_time: 0,
3252        info: value_to_hashmap(data),
3253    })
3254}
3255
3256/// Parse WebSocket OHLCV (candlestick/kline) data from Binance streams.
3257///
3258/// # Arguments
3259///
3260/// * `data` - Kline object from WebSocket message JSON data
3261///
3262/// # Returns
3263///
3264/// Returns a CCXT [`OHLCV`] structure.
3265///
3266/// # Example WebSocket Message
3267///
3268/// ```json
3269/// {
3270///     "e": "kline",
3271///     "E": 1579481530911,
3272///     "s": "ETHBTC",
3273///     "k": {
3274///         "t": 1579481400000,
3275///         "T": 1579481459999,
3276///         "s": "ETHBTC",
3277///         "i": "1m",
3278///         "f": 158251292,
3279///         "L": 158414513,
3280///         "o": "0.01916500",
3281///         "c": "0.01912500",
3282///         "h": "0.01956500",
3283///         "l": "0.01887700",
3284///         "v": "173518.11900000",
3285///         "n": 163222,
3286///         "x": false,
3287///         "q": "3332.40703994",
3288///         "V": "91515.47800000",
3289///         "Q": "1757.42139293"
3290///     }
3291/// }
3292/// ```
3293pub fn parse_ws_ohlcv(data: &Value) -> Result<OHLCV> {
3294    let kline = data["k"]
3295        .as_object()
3296        .ok_or_else(|| Error::from(ParseError::missing_field("k")))?;
3297
3298    let timestamp = kline
3299        .get("t")
3300        .and_then(|v| v.as_i64())
3301        .ok_or_else(|| Error::from(ParseError::missing_field("t")))?;
3302
3303    let open = kline
3304        .get("o")
3305        .and_then(|v| v.as_str())
3306        .and_then(|s| s.parse::<f64>().ok())
3307        .ok_or_else(|| Error::from(ParseError::missing_field("o")))?;
3308
3309    let high = kline
3310        .get("h")
3311        .and_then(|v| v.as_str())
3312        .and_then(|s| s.parse::<f64>().ok())
3313        .ok_or_else(|| Error::from(ParseError::missing_field("h")))?;
3314
3315    let low = kline
3316        .get("l")
3317        .and_then(|v| v.as_str())
3318        .and_then(|s| s.parse::<f64>().ok())
3319        .ok_or_else(|| Error::from(ParseError::missing_field("l")))?;
3320
3321    let close = kline
3322        .get("c")
3323        .and_then(|v| v.as_str())
3324        .and_then(|s| s.parse::<f64>().ok())
3325        .ok_or_else(|| Error::from(ParseError::missing_field("c")))?;
3326
3327    let volume = kline
3328        .get("v")
3329        .and_then(|v| v.as_str())
3330        .and_then(|s| s.parse::<f64>().ok())
3331        .ok_or_else(|| Error::from(ParseError::missing_field("v")))?;
3332
3333    Ok(OHLCV {
3334        timestamp,
3335        open,
3336        high,
3337        low,
3338        close,
3339        volume,
3340    })
3341}
3342
3343/// Parse WebSocket BidAsk (best bid/ask prices) data from Binance streams.
3344///
3345/// # Arguments
3346///
3347/// * `data` - WebSocket message JSON data
3348///
3349/// # Returns
3350///
3351/// Returns a CCXT [`BidAsk`] structure.
3352///
3353/// # Example WebSocket Message
3354///
3355/// ```json
3356/// {
3357///     "u": 400900217,
3358///     "s": "BNBUSDT",
3359///     "b": "25.35190000",
3360///     "B": "31.21000000",
3361///     "a": "25.36520000",
3362///     "A": "40.66000000"
3363/// }
3364/// ```
3365pub fn parse_ws_bid_ask(data: &Value) -> Result<BidAsk> {
3366    let symbol = data["s"]
3367        .as_str()
3368        .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
3369        .to_string();
3370
3371    let bid_price = data["b"]
3372        .as_str()
3373        .and_then(|s| s.parse::<Decimal>().ok())
3374        .ok_or_else(|| Error::from(ParseError::missing_field("bid_price")))?;
3375
3376    let bid_quantity = data["B"]
3377        .as_str()
3378        .and_then(|s| s.parse::<Decimal>().ok())
3379        .ok_or_else(|| Error::from(ParseError::missing_field("bid_quantity")))?;
3380
3381    let ask_price = data["a"]
3382        .as_str()
3383        .and_then(|s| s.parse::<Decimal>().ok())
3384        .ok_or_else(|| Error::from(ParseError::missing_field("ask_price")))?;
3385
3386    let ask_quantity = data["A"]
3387        .as_str()
3388        .and_then(|s| s.parse::<Decimal>().ok())
3389        .ok_or_else(|| Error::from(ParseError::missing_field("ask_quantity")))?;
3390
3391    let timestamp = data["E"].as_i64().unwrap_or(0);
3392
3393    Ok(BidAsk {
3394        symbol,
3395        bid_price,
3396        bid_quantity,
3397        ask_price,
3398        ask_quantity,
3399        timestamp,
3400    })
3401}
3402
3403/// Parse WebSocket mark price data from Binance futures streams.
3404///
3405/// # Binance WebSocket Mark Price Format
3406/// ```json
3407/// {
3408///     "e": "markPriceUpdate",
3409///     "E": 1609459200000,
3410///     "s": "BTCUSDT",
3411///     "p": "50250.50000000",
3412///     "i": "50000.00000000",
3413///     "P": "50500.00000000",
3414///     "r": "0.00010000",
3415///     "T": 1609459200000
3416/// }
3417/// ```
3418pub fn parse_ws_mark_price(data: &Value) -> Result<MarkPrice> {
3419    let symbol = data["s"]
3420        .as_str()
3421        .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
3422        .to_string();
3423
3424    let mark_price = data["p"]
3425        .as_str()
3426        .and_then(|s| s.parse::<Decimal>().ok())
3427        .ok_or_else(|| Error::from(ParseError::missing_field("mark_price")))?;
3428
3429    let index_price = data["i"].as_str().and_then(|s| s.parse::<Decimal>().ok());
3430
3431    let estimated_settle_price = data["P"].as_str().and_then(|s| s.parse::<Decimal>().ok());
3432
3433    let last_funding_rate = data["r"].as_str().and_then(|s| s.parse::<Decimal>().ok());
3434
3435    let next_funding_time = data["T"].as_i64();
3436
3437    let interest_rate = None;
3438
3439    let timestamp = data["E"].as_i64().unwrap_or(0);
3440
3441    Ok(MarkPrice {
3442        symbol,
3443        mark_price,
3444        index_price,
3445        estimated_settle_price,
3446        last_funding_rate,
3447        next_funding_time,
3448        interest_rate,
3449        timestamp,
3450    })
3451}
3452
3453// ============================================================================
3454// Transfer and Deposit/Withdrawal Parser Functions
3455// ============================================================================
3456
3457/// Parse deposit and withdrawal fee information from Binance API.
3458///
3459/// # Binance API Response Format
3460///
3461/// Endpoint: `/sapi/v1/capital/config/getall`
3462/// ```json
3463/// {
3464///   "coin": "BTC",
3465///   "name": "Bitcoin",
3466///   "networkList": [
3467///     {
3468///       "network": "BTC",
3469///       "coin": "BTC",
3470///       "withdrawIntegerMultiple": "0.00000001",
3471///       "isDefault": true,
3472///       "depositEnable": true,
3473///       "withdrawEnable": true,
3474///       "depositDesc": "",
3475///       "withdrawDesc": "",
3476///       "name": "Bitcoin",
3477///       "resetAddressStatus": false,
3478///       "addressRegex": "^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$|^(bc1)[0-9A-Za-z]{39,59}$",
3479///       "memoRegex": "",
3480///       "withdrawFee": "0.0005",
3481///       "withdrawMin": "0.001",
3482///       "withdrawMax": "9999999",
3483///       "minConfirm": 1,
3484///       "unLockConfirm": 2
3485///     }
3486///   ]
3487/// }
3488/// ```
3489///
3490/// # Returns
3491///
3492/// Returns a [`DepositWithdrawFee`](ccxt_core::types::DepositWithdrawFee) structure.
3493pub fn parse_deposit_withdraw_fee(data: &Value) -> Result<ccxt_core::types::DepositWithdrawFee> {
3494    let currency = data["coin"]
3495        .as_str()
3496        .ok_or_else(|| Error::from(ParseError::missing_field("coin")))?
3497        .to_string();
3498
3499    let mut networks = Vec::new();
3500    let mut withdraw_fee = 0.0;
3501    let mut withdraw_min = 0.0;
3502    let mut withdraw_max = 0.0;
3503    let mut deposit_enable = false;
3504    let mut withdraw_enable = false;
3505
3506    if let Some(network_list) = data["networkList"].as_array() {
3507        for network_data in network_list {
3508            let network = parse_network_info(network_data)?;
3509
3510            if network_data["isDefault"].as_bool().unwrap_or(false) {
3511                withdraw_fee = network.withdraw_fee;
3512                withdraw_min = network.withdraw_min;
3513                withdraw_max = network.withdraw_max;
3514                deposit_enable = network.deposit_enable;
3515                withdraw_enable = network.withdraw_enable;
3516            }
3517
3518            networks.push(network);
3519        }
3520    }
3521
3522    if !networks.is_empty() && withdraw_fee == 0.0 {
3523        let first = &networks[0];
3524        withdraw_fee = first.withdraw_fee;
3525        withdraw_min = first.withdraw_min;
3526        withdraw_max = first.withdraw_max;
3527        deposit_enable = first.deposit_enable;
3528        withdraw_enable = first.withdraw_enable;
3529    }
3530
3531    Ok(DepositWithdrawFee {
3532        currency,
3533        withdraw_fee,
3534        withdraw_min,
3535        withdraw_max,
3536        deposit_enable,
3537        withdraw_enable,
3538        networks,
3539        info: Some(data.clone()),
3540    })
3541}
3542
3543/// Parse network information from Binance API.
3544///
3545/// # Binance API Response Format
3546///
3547/// Single network entry within the `networkList` array:
3548/// ```json
3549/// {
3550///   "network": "BTC",
3551///   "coin": "BTC",
3552///   "withdrawIntegerMultiple": "0.00000001",
3553///   "isDefault": true,
3554///   "depositEnable": true,
3555///   "withdrawEnable": true,
3556///   "depositDesc": "",
3557///   "withdrawDesc": "",
3558///   "name": "Bitcoin",
3559///   "withdrawFee": "0.0005",
3560///   "withdrawMin": "0.001",
3561///   "withdrawMax": "9999999",
3562///   "minConfirm": 1,
3563///   "unLockConfirm": 2
3564/// }
3565/// ```
3566///
3567/// # Returns
3568///
3569/// Returns a [`NetworkInfo`](ccxt_core::types::NetworkInfo) structure.
3570pub fn parse_network_info(data: &Value) -> Result<ccxt_core::types::NetworkInfo> {
3571    use ccxt_core::error::{Error, ParseError};
3572    use ccxt_core::types::NetworkInfo;
3573
3574    let network = data["network"]
3575        .as_str()
3576        .ok_or_else(|| Error::from(ParseError::missing_field("network")))?
3577        .to_string();
3578
3579    let name = data["name"].as_str().unwrap_or(&network).to_string();
3580
3581    let withdraw_fee = data["withdrawFee"]
3582        .as_str()
3583        .and_then(|s| s.parse::<f64>().ok())
3584        .or_else(|| data["withdrawFee"].as_f64())
3585        .unwrap_or(0.0);
3586
3587    let withdraw_min = data["withdrawMin"]
3588        .as_str()
3589        .and_then(|s| s.parse::<f64>().ok())
3590        .or_else(|| data["withdrawMin"].as_f64())
3591        .unwrap_or(0.0);
3592
3593    let withdraw_max = data["withdrawMax"]
3594        .as_str()
3595        .and_then(|s| s.parse::<f64>().ok())
3596        .or_else(|| data["withdrawMax"].as_f64())
3597        .unwrap_or(0.0);
3598
3599    let deposit_enable = data["depositEnable"].as_bool().unwrap_or(false);
3600
3601    let withdraw_enable = data["withdrawEnable"].as_bool().unwrap_or(false);
3602
3603    let _is_default = data["isDefault"].as_bool().unwrap_or(false);
3604
3605    let _min_confirm = data["minConfirm"].as_i64().map(|v| v as u32);
3606
3607    let _unlock_confirm = data["unLockConfirm"].as_i64().map(|v| v as u32);
3608
3609    let deposit_confirmations = data["minConfirm"].as_u64().map(|v| v as u32);
3610
3611    let withdraw_confirmations = data["unlockConfirm"].as_u64().map(|v| v as u32);
3612
3613    Ok(NetworkInfo {
3614        network,
3615        name,
3616        withdraw_fee,
3617        withdraw_min,
3618        withdraw_max,
3619        deposit_enable,
3620        withdraw_enable,
3621        deposit_confirmations,
3622        withdraw_confirmations,
3623    })
3624}
3625
3626/// Parse multiple deposit and withdrawal fee information entries from Binance API.
3627///
3628/// # Returns
3629///
3630/// Returns a vector of [`DepositWithdrawFee`](ccxt_core::types::DepositWithdrawFee) structures.
3631pub fn parse_deposit_withdraw_fees(
3632    data: &Value,
3633) -> Result<Vec<ccxt_core::types::DepositWithdrawFee>> {
3634    if let Some(array) = data.as_array() {
3635        array
3636            .iter()
3637            .map(|item| parse_deposit_withdraw_fee(item))
3638            .collect()
3639    } else {
3640        Ok(vec![parse_deposit_withdraw_fee(data)?])
3641    }
3642}
3643
3644#[cfg(test)]
3645mod tests {
3646    use super::*;
3647    use serde_json::json;
3648
3649    #[test]
3650    fn test_parse_market() {
3651        let data = json!({
3652            "symbol": "BTCUSDT",
3653            "baseAsset": "BTC",
3654            "quoteAsset": "USDT",
3655            "status": "TRADING",
3656            "isMarginTradingAllowed": true,
3657            "filters": [
3658                {
3659                    "filterType": "PRICE_FILTER",
3660                    "tickSize": "0.01"
3661                },
3662                {
3663                    "filterType": "LOT_SIZE",
3664                    "stepSize": "0.00001",
3665                    "minQty": "0.00001",
3666                    "maxQty": "9000"
3667                },
3668                {
3669                    "filterType": "MIN_NOTIONAL",
3670                    "minNotional": "10.0"
3671                }
3672            ]
3673        });
3674
3675        let market = parse_market(&data).unwrap();
3676        assert_eq!(market.symbol, "BTC/USDT");
3677        assert_eq!(market.base, "BTC");
3678        assert_eq!(market.quote, "USDT");
3679        assert!(market.active);
3680        assert!(market.margin);
3681        assert_eq!(
3682            market.precision.price,
3683            Some(Decimal::from_str_radix("0.01", 10).unwrap())
3684        );
3685        assert_eq!(
3686            market.precision.amount,
3687            Some(Decimal::from_str_radix("0.00001", 10).unwrap())
3688        );
3689    }
3690
3691    #[test]
3692    fn test_parse_ticker() {
3693        let data = json!({
3694            "symbol": "BTCUSDT",
3695            "lastPrice": "50000.00",
3696            "openPrice": "49000.00",
3697            "highPrice": "51000.00",
3698            "lowPrice": "48500.00",
3699            "volume": "1000.5",
3700            "quoteVolume": "50000000.0",
3701            "bidPrice": "49999.00",
3702            "bidQty": "1.5",
3703            "askPrice": "50001.00",
3704            "askQty": "2.0",
3705            "closeTime": 1609459200000u64,
3706            "priceChange": "1000.00",
3707            "priceChangePercent": "2.04"
3708        });
3709
3710        let ticker = parse_ticker(&data, None).unwrap();
3711        assert_eq!(
3712            ticker.last,
3713            Some(Price::new(Decimal::from_str_radix("50000.00", 10).unwrap()))
3714        );
3715        assert_eq!(
3716            ticker.high,
3717            Some(Price::new(Decimal::from_str_radix("51000.00", 10).unwrap()))
3718        );
3719        assert_eq!(
3720            ticker.low,
3721            Some(Price::new(Decimal::from_str_radix("48500.00", 10).unwrap()))
3722        );
3723        assert_eq!(
3724            ticker.bid,
3725            Some(Price::new(Decimal::from_str_radix("49999.00", 10).unwrap()))
3726        );
3727        assert_eq!(
3728            ticker.ask,
3729            Some(Price::new(Decimal::from_str_radix("50001.00", 10).unwrap()))
3730        );
3731    }
3732
3733    #[test]
3734    fn test_parse_trade() {
3735        let data = json!({
3736            "id": 12345,
3737            "price": "50000.00",
3738            "qty": "0.5",
3739            "time": 1609459200000u64,
3740            "isBuyerMaker": false,
3741            "symbol": "BTCUSDT"
3742        });
3743
3744        let trade = parse_trade(&data, None).unwrap();
3745        assert_eq!(trade.id, Some("12345".to_string()));
3746        assert_eq!(
3747            trade.price,
3748            Price::new(Decimal::from_str_radix("50000.00", 10).unwrap())
3749        );
3750        assert_eq!(
3751            trade.amount,
3752            Amount::new(Decimal::from_str_radix("0.5", 10).unwrap())
3753        );
3754        assert_eq!(trade.side, OrderSide::Buy);
3755    }
3756
3757    #[test]
3758    fn test_parse_order() {
3759        let data = json!({
3760            "orderId": 12345,
3761            "symbol": "BTCUSDT",
3762            "status": "FILLED",
3763            "side": "BUY",
3764            "type": "LIMIT",
3765
3766            "price": "50000.00",
3767            "origQty": "0.5",
3768            "executedQty": "0.5",
3769            "cummulativeQuoteQty": "25000.00",
3770            "time": 1609459200000u64,
3771            "updateTime": 1609459200000u64
3772        });
3773
3774        let order = parse_order(&data, None).unwrap();
3775        assert_eq!(order.id, "12345".to_string());
3776        assert_eq!(order.symbol, "BTCUSDT");
3777        assert_eq!(order.order_type, OrderType::Limit);
3778        assert_eq!(order.side, OrderSide::Buy);
3779        assert_eq!(
3780            order.price,
3781            Some(Decimal::from_str_radix("50000.00", 10).unwrap())
3782        );
3783        assert_eq!(order.amount, Decimal::from_str_radix("0.5", 10).unwrap());
3784        assert_eq!(
3785            order.filled,
3786            Some(Decimal::from_str_radix("0.5", 10).unwrap())
3787        );
3788    }
3789
3790    #[test]
3791    fn test_parse_balance() {
3792        let data = json!({
3793            "balances": [
3794                {
3795                    "asset": "BTC",
3796                    "free": "1.5",
3797                    "locked": "0.5"
3798                }
3799            ]
3800        });
3801
3802        let balance = parse_balance(&data).unwrap();
3803        let btc_balance = balance.balances.get("BTC").unwrap();
3804        assert_eq!(
3805            btc_balance.free,
3806            Decimal::from_str_radix("1.5", 10).unwrap()
3807        );
3808        assert_eq!(
3809            btc_balance.used,
3810            Decimal::from_str_radix("0.5", 10).unwrap()
3811        );
3812        assert_eq!(
3813            btc_balance.total,
3814            Decimal::from_str_radix("2.0", 10).unwrap()
3815        );
3816    }
3817
3818    #[test]
3819    fn test_parse_market_with_filters() {
3820        let data = json!({
3821            "symbol": "ETHUSDT",
3822            "baseAsset": "ETH",
3823            "quoteAsset": "USDT",
3824            "status": "TRADING",
3825            "filters": [
3826                {
3827                    "filterType": "PRICE_FILTER",
3828                    "tickSize": "0.01",
3829                    "minPrice": "0.01",
3830                    "maxPrice": "1000000.00"
3831                },
3832                {
3833                    "filterType": "LOT_SIZE",
3834                    "stepSize": "0.0001",
3835                    "minQty": "0.0001",
3836                    "maxQty": "90000"
3837                },
3838                {
3839                    "filterType": "MIN_NOTIONAL",
3840                    "minNotional": "10.0"
3841                },
3842                {
3843                    "filterType": "MARKET_LOT_SIZE",
3844                    "stepSize": "0.0001",
3845                    "minQty": "0.0001",
3846                    "maxQty": "50000"
3847                }
3848            ]
3849        });
3850
3851        let market = parse_market(&data).unwrap();
3852        assert_eq!(market.symbol, "ETH/USDT");
3853        assert!(market.limits.amount.is_some());
3854        assert!(market.limits.amount.as_ref().unwrap().min.is_some());
3855        assert!(market.limits.amount.as_ref().unwrap().max.is_some());
3856        assert!(market.limits.price.is_some());
3857        assert!(market.limits.price.as_ref().unwrap().min.is_some());
3858        assert!(market.limits.price.as_ref().unwrap().max.is_some());
3859        assert_eq!(
3860            market.limits.cost.as_ref().unwrap().min,
3861            Some(Decimal::from_str_radix("10.0", 10).unwrap())
3862        );
3863    }
3864
3865    #[test]
3866    fn test_parse_ticker_edge_cases() {
3867        let data = json!({
3868            "symbol": "BTCUSDT",
3869            "lastPrice": "50000.00",
3870            "closeTime": 1609459200000u64
3871        });
3872
3873        let ticker = parse_ticker(&data, None).unwrap();
3874        assert_eq!(
3875            ticker.last,
3876            Some(Price::new(Decimal::from_str_radix("50000.00", 10).unwrap()))
3877        );
3878        assert_eq!(ticker.symbol, "BTCUSDT");
3879        assert_eq!(ticker.bid, None);
3880        assert_eq!(ticker.ask, None);
3881    }
3882
3883    #[test]
3884    fn test_parse_trade_timestamp() {
3885        let data = json!({
3886            "id": 99999,
3887            "price": "45000.50",
3888            "qty": "1.25",
3889            "time": 1609459200000u64,
3890            "isBuyerMaker": true,
3891            "symbol": "BTCUSDT"
3892        });
3893
3894        let trade = parse_trade(&data, None).unwrap();
3895        assert_eq!(trade.timestamp, 1609459200000);
3896        assert_eq!(trade.side, OrderSide::Sell);
3897    }
3898
3899    #[test]
3900    fn test_parse_order_status() {
3901        let statuses = vec![
3902            ("NEW", "open"),
3903            ("PARTIALLY_FILLED", "open"),
3904            ("FILLED", "closed"),
3905            ("CANCELED", "canceled"),
3906            ("REJECTED", "rejected"),
3907            ("EXPIRED", "expired"),
3908        ];
3909
3910        for (binance_status, expected_status) in statuses {
3911            let data = json!({
3912                "orderId": 123,
3913                "symbol": "BTCUSDT",
3914                "status": binance_status,
3915                "side": "BUY",
3916                "type": "LIMIT",
3917                "price": "50000.00",
3918                "origQty": "1.0",
3919                "executedQty": "0.0",
3920                "time": 1609459200000u64
3921            });
3922
3923            let order = parse_order(&data, None).unwrap();
3924            // Convert expected_status string to OrderStatus enum
3925            let status_enum = match expected_status {
3926                "open" => OrderStatus::Open,
3927                "closed" => OrderStatus::Closed,
3928                "canceled" | "cancelled" => OrderStatus::Cancelled,
3929                "expired" => OrderStatus::Expired,
3930                "rejected" => OrderStatus::Rejected,
3931                _ => OrderStatus::Open,
3932            };
3933            assert_eq!(order.status, status_enum);
3934        }
3935    }
3936
3937    #[test]
3938    fn test_parse_balance_locked() {
3939        let data = json!({
3940            "balances": [
3941                {
3942                    "asset": "USDT",
3943                    "free": "10000.50",
3944                    "locked": "500.25"
3945                }
3946            ]
3947        });
3948
3949        let balance = parse_balance(&data).unwrap();
3950        let usdt_balance = balance.balances.get("USDT").unwrap();
3951        assert_eq!(
3952            usdt_balance.free,
3953            Decimal::from_str_radix("10000.50", 10).unwrap()
3954        );
3955        assert_eq!(
3956            usdt_balance.used,
3957            Decimal::from_str_radix("500.25", 10).unwrap()
3958        );
3959        assert_eq!(
3960            usdt_balance.total,
3961            Decimal::from_str_radix("10500.75", 10).unwrap()
3962        );
3963    }
3964
3965    #[test]
3966    fn test_parse_empty_response() {
3967        let data = json!({
3968            "lastUpdateId": 12345,
3969            "bids": [],
3970            "asks": []
3971        });
3972
3973        let orderbook = parse_orderbook(&data, "BTC/USDT".to_string()).unwrap();
3974        assert_eq!(orderbook.bids.len(), 0);
3975        assert_eq!(orderbook.asks.len(), 0);
3976    }
3977
3978    #[test]
3979    fn test_currency_precision() {
3980        let data = json!({
3981            "symbol": "BTCUSDT",
3982            "baseAsset": "BTC",
3983            "quoteAsset": "USDT",
3984            "status": "TRADING",
3985            "filters": [
3986                {
3987                    "filterType": "LOT_SIZE",
3988                    "stepSize": "0.00000001"
3989                },
3990                {
3991                    "filterType": "PRICE_FILTER",
3992                    "tickSize": "0.01"
3993                }
3994            ]
3995        });
3996
3997        let market = parse_market(&data).unwrap();
3998        assert_eq!(
3999            market.precision.amount,
4000            Some(Decimal::from_str_radix("0.00000001", 10).unwrap())
4001        );
4002        assert_eq!(
4003            market.precision.price,
4004            Some(Decimal::from_str_radix("0.01", 10).unwrap())
4005        );
4006    }
4007
4008    #[test]
4009    fn test_market_limits() {
4010        let data = json!({
4011            "symbol": "ETHBTC",
4012            "baseAsset": "ETH",
4013            "quoteAsset": "BTC",
4014            "status": "TRADING",
4015            "filters": [
4016                {
4017                    "filterType": "LOT_SIZE",
4018                    "minQty": "0.001",
4019                    "maxQty": "100000",
4020                    "stepSize": "0.001"
4021                },
4022                {
4023                    "filterType": "PRICE_FILTER",
4024                    "minPrice": "0.00000100",
4025                    "maxPrice": "100000.00000000",
4026                    "tickSize": "0.00000100"
4027                },
4028                {
4029                    "filterType": "MIN_NOTIONAL",
4030                    "minNotional": "0.0001"
4031                }
4032            ]
4033        });
4034
4035        let market = parse_market(&data).unwrap();
4036        assert_eq!(
4037            market.limits.amount.as_ref().unwrap().min,
4038            Some(Decimal::from_str_radix("0.001", 10).unwrap())
4039        );
4040        assert_eq!(
4041            market.limits.amount.as_ref().unwrap().max,
4042            Some(Decimal::from_str_radix("100000.0", 10).unwrap())
4043        );
4044        assert_eq!(
4045            market.limits.price.as_ref().unwrap().min,
4046            Some(Decimal::from_str_radix("0.000001", 10).unwrap())
4047        );
4048        assert_eq!(
4049            market.limits.price.as_ref().unwrap().max,
4050            Some(Decimal::from_str_radix("100000.0", 10).unwrap())
4051        );
4052        assert_eq!(
4053            market.limits.cost.as_ref().unwrap().min,
4054            Some(Decimal::from_str_radix("0.0001", 10).unwrap())
4055        );
4056    }
4057
4058    #[test]
4059    fn test_symbol_normalization() {
4060        let symbols = vec![
4061            ("BTCUSDT", "BTC/USDT"),
4062            ("ETHBTC", "ETH/BTC"),
4063            ("BNBBUSD", "BNB/BUSD"),
4064        ];
4065
4066        for (binance_symbol, ccxt_symbol) in symbols {
4067            let data = json!({
4068                "symbol": binance_symbol,
4069                "baseAsset": &ccxt_symbol[..ccxt_symbol.find('/').unwrap()],
4070                "quoteAsset": &ccxt_symbol[ccxt_symbol.find('/').unwrap() + 1..],
4071                "status": "TRADING"
4072            });
4073
4074            let market = parse_market(&data).unwrap();
4075            assert_eq!(market.symbol, ccxt_symbol);
4076        }
4077    }
4078
4079    #[test]
4080    fn test_timeframe_conversion() {
4081        let timeframes = vec![
4082            ("1m", 60000),
4083            ("5m", 300000),
4084            ("15m", 900000),
4085            ("1h", 3600000),
4086            ("4h", 14400000),
4087            ("1d", 86400000),
4088        ];
4089
4090        for (tf_str, expected_ms) in timeframes {
4091            let ms = match tf_str {
4092                "1m" => 60000,
4093                "5m" => 300000,
4094                "15m" => 900000,
4095                "1h" => 3600000,
4096                "4h" => 14400000,
4097                "1d" => 86400000,
4098                _ => 0,
4099            };
4100            assert_eq!(ms, expected_ms);
4101        }
4102    }
4103}
4104
4105// ============================================================================
4106// Deposit/Withdrawal Parser Functions
4107// ============================================================================
4108
4109/// Check if a currency is a fiat currency.
4110///
4111/// # Arguments
4112///
4113/// * `currency` - Currency code
4114///
4115/// # Returns
4116///
4117/// Returns `true` if the currency is a fiat currency.
4118pub fn is_fiat_currency(currency: &str) -> bool {
4119    matches!(
4120        currency.to_uppercase().as_str(),
4121        "USD" | "EUR" | "GBP" | "JPY" | "CNY" | "KRW" | "AUD" | "CAD" | "CHF" | "HKD" | "SGD"
4122    )
4123}
4124
4125/// Extract internal transfer ID from transaction ID.
4126///
4127/// Extracts the actual ID from internal transfer transaction IDs.
4128///
4129/// # Arguments
4130///
4131/// * `txid` - Original transaction ID
4132///
4133/// # Returns
4134///
4135/// Returns the processed transaction ID.
4136pub fn extract_internal_transfer_id(txid: &str) -> String {
4137    const PREFIX: &str = "Internal transfer ";
4138    if txid.starts_with(PREFIX) {
4139        txid[PREFIX.len()..].to_string()
4140    } else {
4141        txid.to_string()
4142    }
4143}
4144
4145/// Parse transaction status based on transaction type.
4146///
4147/// Deposits and withdrawals use different status code mappings.
4148///
4149/// # Arguments
4150///
4151/// * `status_value` - Status value (may be integer or string)
4152/// * `is_deposit` - Whether this is a deposit transaction
4153///
4154/// # Returns
4155///
4156/// Returns a [`TransactionStatus`](ccxt_core::types::TransactionStatus).
4157pub fn parse_transaction_status_by_type(
4158    status_value: &Value,
4159    is_deposit: bool,
4160) -> ccxt_core::types::TransactionStatus {
4161    use ccxt_core::types::TransactionStatus;
4162
4163    if let Some(status_int) = status_value.as_i64() {
4164        if is_deposit {
4165            match status_int {
4166                0 => TransactionStatus::Pending,
4167                1 => TransactionStatus::Ok,
4168                6 => TransactionStatus::Ok,
4169                _ => TransactionStatus::Pending,
4170            }
4171        } else {
4172            match status_int {
4173                0 => TransactionStatus::Pending,
4174                1 => TransactionStatus::Canceled,
4175                2 => TransactionStatus::Pending,
4176                3 => TransactionStatus::Failed,
4177                4 => TransactionStatus::Pending,
4178                5 => TransactionStatus::Failed,
4179                6 => TransactionStatus::Ok,
4180                _ => TransactionStatus::Pending,
4181            }
4182        }
4183    } else if let Some(status_str) = status_value.as_str() {
4184        match status_str {
4185            "Processing" => TransactionStatus::Pending,
4186            "Failed" => TransactionStatus::Failed,
4187            "Successful" => TransactionStatus::Ok,
4188            "Refunding" => TransactionStatus::Canceled,
4189            "Refunded" => TransactionStatus::Canceled,
4190            "Refund Failed" => TransactionStatus::Failed,
4191            _ => TransactionStatus::Pending,
4192        }
4193    } else {
4194        TransactionStatus::Pending
4195    }
4196}
4197
4198/// 解析单个交易记录(充值或提现)
4199///
4200/// # Arguments
4201///
4202/// * `data` - Binance transaction data JSON
4203/// * `transaction_type` - Transaction type (deposit or withdrawal)
4204///
4205/// # Returns
4206///
4207/// Returns a [`Transaction`](ccxt_core::types::Transaction).
4208///
4209/// # Binance API Response Format (Deposit)
4210///
4211/// ```json
4212/// {
4213///     "id": "abc123",
4214///     "amount": "0.5",
4215///     "coin": "BTC",
4216///     "network": "BTC",
4217///     "status": 1,
4218///     "address": "1A1zP1...",
4219///     "addressTag": "",
4220///     "txId": "hash123...",
4221///     "insertTime": 1609459200000,
4222///     "transferType": 0,
4223///     "confirmTimes": "2/2",
4224///     "unlockConfirm": 2,
4225///     "walletType": 0
4226/// }
4227/// ```
4228///
4229/// # Binance API Response Format (Withdrawal)
4230///
4231/// ```json
4232/// {
4233///     "id": "def456",
4234///     "amount": "0.3",
4235///     "transactionFee": "0.0005",
4236///     "coin": "BTC",
4237///     "status": 6,
4238///     "address": "1A1zP1...",
4239///     "txId": "hash456...",
4240///     "applyTime": "2021-01-01 00:00:00",
4241///     "network": "BTC",
4242///     "transferType": 0
4243/// }
4244/// ```
4245pub fn parse_transaction(
4246    data: &Value,
4247    transaction_type: ccxt_core::types::TransactionType,
4248) -> Result<ccxt_core::types::Transaction> {
4249    use ccxt_core::types::{Transaction, TransactionFee, TransactionStatus, TransactionType};
4250
4251    let is_deposit = matches!(transaction_type, TransactionType::Deposit);
4252
4253    // Parse ID: deposits use "id", withdrawals prefer "id" then "withdrawOrderId"
4254    let id = if is_deposit {
4255        data["id"]
4256            .as_str()
4257            .or_else(|| data["orderNo"].as_str())
4258            .unwrap_or("")
4259            .to_string()
4260    } else {
4261        data["id"]
4262            .as_str()
4263            .or_else(|| data["withdrawOrderId"].as_str())
4264            .unwrap_or("")
4265            .to_string()
4266    };
4267
4268    // Parse currency: use "coin" for both deposits/withdrawals, "fiatCurrency" for fiat
4269    let currency = data["coin"]
4270        .as_str()
4271        .or_else(|| data["fiatCurrency"].as_str())
4272        .unwrap_or("")
4273        .to_string();
4274
4275    let amount = data["amount"]
4276        .as_str()
4277        .and_then(|s| Decimal::from_str(s).ok())
4278        .unwrap_or_else(|| Decimal::ZERO);
4279
4280    let fee = if is_deposit {
4281        None
4282    } else {
4283        data["transactionFee"]
4284            .as_str()
4285            .or_else(|| data["totalFee"].as_str())
4286            .and_then(|s| Decimal::from_str(s).ok())
4287            .map(|cost| TransactionFee {
4288                currency: currency.clone(),
4289                cost,
4290            })
4291    };
4292
4293    let timestamp = if is_deposit {
4294        data["insertTime"].as_i64()
4295    } else {
4296        data["createTime"].as_i64().or_else(|| {
4297            data["applyTime"].as_str().and_then(|s| {
4298                chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
4299                    .ok()
4300                    .map(|dt| dt.and_utc().timestamp_millis())
4301            })
4302        })
4303    };
4304
4305    let datetime = timestamp.and_then(|ts| ccxt_core::time::iso8601(ts).ok());
4306
4307    let network = data["network"].as_str().map(|s| s.to_string());
4308
4309    let address = data["address"]
4310        .as_str()
4311        .or_else(|| data["depositAddress"].as_str())
4312        .map(|s| s.to_string());
4313
4314    let tag = data["addressTag"]
4315        .as_str()
4316        .or_else(|| data["tag"].as_str())
4317        .filter(|s| !s.is_empty())
4318        .map(|s| s.to_string());
4319
4320    let mut txid = data["txId"]
4321        .as_str()
4322        .or_else(|| data["hash"].as_str())
4323        .map(|s| s.to_string());
4324
4325    let transfer_type = data["transferType"].as_i64();
4326    let is_internal = transfer_type == Some(1);
4327
4328    if is_internal {
4329        if let Some(ref tx) = txid {
4330            txid = Some(extract_internal_transfer_id(tx));
4331        }
4332    }
4333
4334    let status = if let Some(status_value) = data.get("status") {
4335        parse_transaction_status_by_type(status_value, is_deposit)
4336    } else {
4337        TransactionStatus::Pending
4338    };
4339
4340    let updated = data["updateTime"].as_i64();
4341
4342    let comment = data["info"]
4343        .as_str()
4344        .or_else(|| data["comment"].as_str())
4345        .map(|s| s.to_string());
4346
4347    Ok(Transaction {
4348        info: Some(data.clone()),
4349        id,
4350        txid,
4351        timestamp,
4352        datetime,
4353        network,
4354        address: address.clone(),
4355        address_to: if is_deposit { address.clone() } else { None },
4356        address_from: if !is_deposit { address } else { None },
4357        tag: tag.clone(),
4358        tag_to: if is_deposit { tag.clone() } else { None },
4359        tag_from: if !is_deposit { tag } else { None },
4360        transaction_type,
4361        amount,
4362        currency,
4363        status,
4364        updated,
4365        internal: Some(is_internal),
4366        comment,
4367        fee,
4368    })
4369}
4370
4371/// Parse deposit address from Binance API response.
4372///
4373/// # Arguments
4374///
4375/// * `data` - Binance deposit address JSON data
4376///
4377/// # Returns
4378///
4379/// Returns a [`DepositAddress`](ccxt_core::types::DepositAddress).
4380///
4381/// # Binance API Response Format
4382///
4383/// ```json
4384/// {
4385///     "coin": "BTC",
4386///     "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
4387///     "tag": "",
4388///     "url": "https://btc.com/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
4389/// }
4390/// ```
4391pub fn parse_deposit_address(data: &Value) -> Result<ccxt_core::types::DepositAddress> {
4392    use ccxt_core::types::DepositAddress;
4393
4394    let currency = data["coin"]
4395        .as_str()
4396        .ok_or_else(|| Error::from(ParseError::missing_field("coin")))?
4397        .to_string();
4398
4399    let address = data["address"]
4400        .as_str()
4401        .ok_or_else(|| Error::from(ParseError::missing_field("address")))?
4402        .to_string();
4403
4404    let network = data["network"].as_str().map(|s| s.to_string()).or_else(|| {
4405        data["url"].as_str().and_then(|url| {
4406            if url.contains("btc.com") {
4407                Some("BTC".to_string())
4408            } else if url.contains("etherscan.io") {
4409                Some("ETH".to_string())
4410            } else if url.contains("tronscan.org") {
4411                Some("TRX".to_string())
4412            } else {
4413                None
4414            }
4415        })
4416    });
4417
4418    let tag = data["tag"]
4419        .as_str()
4420        .or_else(|| data["addressTag"].as_str())
4421        .filter(|s| !s.is_empty())
4422        .map(|s| s.to_string());
4423
4424    Ok(DepositAddress {
4425        info: Some(data.clone()),
4426        currency,
4427        network,
4428        address,
4429        tag,
4430    })
4431}
4432// ============================================================================
4433// P1.4 Market Data Enhancement - Parser Functions
4434// ============================================================================
4435
4436/// Parse currency information from Binance API response.
4437///
4438/// # Arguments
4439///
4440/// * `data` - Binance currency data JSON
4441///
4442/// # Returns
4443///
4444/// Returns a CCXT [`Currency`](ccxt_core::types::Currency).
4445///
4446/// # Binance API Response Format
4447/// ```json
4448/// {
4449///   "coin": "BTC",
4450///   "name": "Bitcoin",
4451///   "networkList": [
4452///     {
4453///       "network": "BTC",
4454///       "coin": "BTC",
4455///       "withdrawIntegerMultiple": "0.00000001",
4456///       "isDefault": true,
4457///       "depositEnable": true,
4458///       "withdrawEnable": true,
4459///       "depositDesc": "",
4460///       "withdrawDesc": "",
4461///       "name": "Bitcoin",
4462///       "resetAddressStatus": false,
4463///       "addressRegex": "^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$|^(bc1)[0-9A-Za-z]{39,59}$",
4464///       "memoRegex": "",
4465///       "withdrawFee": "0.0005",
4466///       "withdrawMin": "0.001",
4467///       "withdrawMax": "9000",
4468///       "minConfirm": 1,
4469///       "unLockConfirm": 2
4470///     }
4471///   ],
4472///   "trading": true,
4473///   "isLegalMoney": false
4474/// }
4475/// ```
4476pub fn parse_currency(data: &Value) -> Result<ccxt_core::types::Currency> {
4477    use ccxt_core::types::{Currency, CurrencyNetwork, MinMax};
4478
4479    let code = data["coin"]
4480        .as_str()
4481        .ok_or_else(|| Error::from(ParseError::missing_field("coin")))?
4482        .to_string();
4483
4484    let id = code.clone();
4485    let name = data["name"].as_str().map(|s| s.to_string());
4486
4487    let active = data["trading"].as_bool().unwrap_or(true);
4488
4489    let mut networks = HashMap::new();
4490    let mut global_deposit = false;
4491    let mut global_withdraw = false;
4492    let mut global_fee = None;
4493    let mut global_precision = None;
4494    let mut global_limits = MinMax::default();
4495
4496    if let Some(network_list) = data["networkList"].as_array() {
4497        for network_data in network_list {
4498            let network_id = network_data["network"]
4499                .as_str()
4500                .unwrap_or(&code)
4501                .to_string();
4502
4503            let is_default = network_data["isDefault"].as_bool().unwrap_or(false);
4504            let deposit_enable = network_data["depositEnable"].as_bool().unwrap_or(false);
4505            let withdraw_enable = network_data["withdrawEnable"].as_bool().unwrap_or(false);
4506
4507            if is_default {
4508                global_deposit = deposit_enable;
4509                global_withdraw = withdraw_enable;
4510            }
4511
4512            let fee = network_data["withdrawFee"]
4513                .as_str()
4514                .and_then(|s| Decimal::from_str(s).ok());
4515
4516            if is_default && fee.is_some() {
4517                global_fee = fee;
4518            }
4519
4520            let precision = network_data["withdrawIntegerMultiple"]
4521                .as_str()
4522                .and_then(|s| Decimal::from_str(s).ok());
4523
4524            if is_default && precision.is_some() {
4525                global_precision = precision;
4526            }
4527
4528            let withdraw_min = network_data["withdrawMin"]
4529                .as_str()
4530                .and_then(|s| Decimal::from_str(s).ok());
4531
4532            let withdraw_max = network_data["withdrawMax"]
4533                .as_str()
4534                .and_then(|s| Decimal::from_str(s).ok());
4535
4536            let limits = MinMax {
4537                min: withdraw_min,
4538                max: withdraw_max,
4539            };
4540
4541            if is_default {
4542                global_limits = limits.clone();
4543            }
4544
4545            let network = CurrencyNetwork {
4546                network: network_id.clone(),
4547                id: Some(network_id.clone()),
4548                name: network_data["name"].as_str().map(|s| s.to_string()),
4549                active: deposit_enable && withdraw_enable,
4550                deposit: deposit_enable,
4551                withdraw: withdraw_enable,
4552                fee,
4553                precision,
4554                limits,
4555                info: value_to_hashmap(network_data),
4556            };
4557
4558            networks.insert(network_id, network);
4559        }
4560    }
4561
4562    Ok(Currency {
4563        code,
4564        id,
4565        name,
4566        active,
4567        deposit: global_deposit,
4568        withdraw: global_withdraw,
4569        fee: global_fee,
4570        precision: global_precision,
4571        limits: global_limits,
4572        networks,
4573        currency_type: if data["isLegalMoney"].as_bool().unwrap_or(false) {
4574            Some("fiat".to_string())
4575        } else {
4576            Some("crypto".to_string())
4577        },
4578        info: value_to_hashmap(data),
4579    })
4580}
4581
4582/// Parse multiple currencies from Binance API response.
4583///
4584/// # Arguments
4585///
4586/// * `data` - Binance currency data JSON array
4587///
4588/// # Returns
4589///
4590/// Returns a vector of [`Currency`](ccxt_core::types::Currency).
4591pub fn parse_currencies(data: &Value) -> Result<Vec<ccxt_core::types::Currency>> {
4592    if let Some(array) = data.as_array() {
4593        array.iter().map(parse_currency).collect()
4594    } else {
4595        Ok(vec![parse_currency(data)?])
4596    }
4597}
4598
4599/// Parse 24-hour statistics from Binance API response.
4600///
4601/// # Arguments
4602///
4603/// * `data` - Binance 24-hour statistics JSON data
4604///
4605/// # Returns
4606///
4607/// Returns a [`Stats24hr`](ccxt_core::types::Stats24hr).
4608///
4609/// # Binance API Response Format
4610/// ```json
4611/// {
4612///   "symbol": "BTCUSDT",
4613///   "priceChange": "1000.00",
4614///   "priceChangePercent": "2.04",
4615///   "weightedAvgPrice": "49500.00",
4616///   "prevClosePrice": "49000.00",
4617///   "lastPrice": "50000.00",
4618///   "lastQty": "0.5",
4619///   "bidPrice": "49999.00",
4620///   "bidQty": "1.5",
4621///   "askPrice": "50001.00",
4622///   "askQty": "2.0",
4623///   "openPrice": "49000.00",
4624///   "highPrice": "51000.00",
4625///   "lowPrice": "48500.00",
4626///   "volume": "1000.5",
4627///   "quoteVolume": "50000000.0",
4628///   "openTime": 1609459200000,
4629///   "closeTime": 1609545600000,
4630///   "firstId": 100000,
4631///   "lastId": 200000,
4632///   "count": 100000
4633/// }
4634/// ```
4635pub fn parse_stats_24hr(data: &Value) -> Result<ccxt_core::types::Stats24hr> {
4636    use ccxt_core::types::Stats24hr;
4637
4638    let symbol = data["symbol"]
4639        .as_str()
4640        .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
4641        .to_string();
4642
4643    let price_change = data["priceChange"]
4644        .as_str()
4645        .and_then(|s| Decimal::from_str(s).ok());
4646
4647    let price_change_percent = data["priceChangePercent"]
4648        .as_str()
4649        .and_then(|s| Decimal::from_str(s).ok());
4650
4651    let weighted_avg_price = data["weightedAvgPrice"]
4652        .as_str()
4653        .and_then(|s| Decimal::from_str(s).ok());
4654
4655    let prev_close_price = data["prevClosePrice"]
4656        .as_str()
4657        .and_then(|s| Decimal::from_str(s).ok());
4658
4659    let last_price = data["lastPrice"]
4660        .as_str()
4661        .and_then(|s| Decimal::from_str(s).ok());
4662
4663    let last_qty = data["lastQty"]
4664        .as_str()
4665        .and_then(|s| Decimal::from_str(s).ok());
4666
4667    let bid_price = data["bidPrice"]
4668        .as_str()
4669        .and_then(|s| Decimal::from_str(s).ok());
4670
4671    let bid_qty = data["bidQty"]
4672        .as_str()
4673        .and_then(|s| Decimal::from_str(s).ok());
4674
4675    let ask_price = data["askPrice"]
4676        .as_str()
4677        .and_then(|s| Decimal::from_str(s).ok());
4678
4679    let ask_qty = data["askQty"]
4680        .as_str()
4681        .and_then(|s| Decimal::from_str(s).ok());
4682
4683    // 解析OHLC数据
4684    let open_price = data["openPrice"]
4685        .as_str()
4686        .and_then(|s| Decimal::from_str(s).ok());
4687
4688    let high_price = data["highPrice"]
4689        .as_str()
4690        .and_then(|s| Decimal::from_str(s).ok());
4691
4692    let low_price = data["lowPrice"]
4693        .as_str()
4694        .and_then(|s| Decimal::from_str(s).ok());
4695
4696    // 解析成交量
4697    let volume = data["volume"]
4698        .as_str()
4699        .and_then(|s| Decimal::from_str(s).ok());
4700
4701    let quote_volume = data["quoteVolume"]
4702        .as_str()
4703        .and_then(|s| Decimal::from_str(s).ok());
4704
4705    let open_time = data["openTime"].as_i64();
4706    let close_time = data["closeTime"].as_i64();
4707    let first_id = data["firstId"].as_i64();
4708    let last_id = data["lastId"].as_i64();
4709    let count = data["count"].as_i64();
4710
4711    Ok(Stats24hr {
4712        symbol,
4713        price_change,
4714        price_change_percent,
4715        weighted_avg_price,
4716        prev_close_price,
4717        last_price,
4718        last_qty,
4719        bid_price,
4720        bid_qty,
4721        ask_price,
4722        ask_qty,
4723        open_price,
4724        high_price,
4725        low_price,
4726        volume,
4727        quote_volume,
4728        open_time,
4729        close_time,
4730        first_id,
4731        last_id,
4732        count,
4733        info: value_to_hashmap(data),
4734    })
4735}
4736
4737/// Parse aggregated trade from Binance API response.
4738///
4739/// # Arguments
4740///
4741/// * `data` - Binance aggregated trade JSON data
4742/// * `symbol` - Optional trading pair symbol
4743///
4744/// # Returns
4745///
4746/// Returns an [`AggTrade`](ccxt_core::types::AggTrade).
4747///
4748/// # Binance API Response Format
4749/// ```json
4750/// {
4751///   "a": 26129,
4752///   "p": "0.01633102",
4753///   "q": "4.70443515",
4754///   "f": 27781,
4755///   "l": 27781,
4756///   "T": 1498793709153,
4757///   "m": true,
4758///   "M": true
4759/// }
4760/// ```
4761pub fn parse_agg_trade(data: &Value, symbol: Option<String>) -> Result<ccxt_core::types::AggTrade> {
4762    use ccxt_core::types::AggTrade;
4763
4764    let agg_id = data["a"]
4765        .as_i64()
4766        .ok_or_else(|| Error::from(ParseError::missing_field("a")))?;
4767
4768    let price = data["p"]
4769        .as_str()
4770        .and_then(|s| Decimal::from_str(s).ok())
4771        .ok_or_else(|| Error::from(ParseError::missing_field("p")))?;
4772
4773    let quantity = data["q"]
4774        .as_str()
4775        .and_then(|s| Decimal::from_str(s).ok())
4776        .ok_or_else(|| Error::from(ParseError::missing_field("q")))?;
4777
4778    let first_trade_id = data["f"]
4779        .as_i64()
4780        .ok_or_else(|| Error::from(ParseError::missing_field("f")))?;
4781
4782    let last_trade_id = data["l"]
4783        .as_i64()
4784        .ok_or_else(|| Error::from(ParseError::missing_field("l")))?;
4785
4786    let timestamp = data["T"]
4787        .as_i64()
4788        .ok_or_else(|| Error::from(ParseError::missing_field("T")))?;
4789
4790    let is_buyer_maker = data["m"].as_bool().unwrap_or(false);
4791    let is_best_match = data["M"].as_bool();
4792
4793    Ok(AggTrade {
4794        agg_id,
4795        price,
4796        quantity,
4797        first_trade_id,
4798        last_trade_id,
4799        timestamp,
4800        is_buyer_maker,
4801        is_best_match,
4802        symbol,
4803    })
4804}
4805
4806/// Parse trading limits from Binance API response.
4807///
4808/// # Arguments
4809///
4810/// * `data` - Binance trading limits JSON data
4811/// * `_symbol` - Trading pair symbol (unused)
4812///
4813/// # Returns
4814///
4815/// Returns a [`TradingLimits`](ccxt_core::types::TradingLimits).
4816///
4817/// # Binance API Response Format
4818/// ```json
4819/// {
4820///   "symbol": "BTCUSDT",
4821///   "filters": [
4822///     {
4823///       "filterType": "PRICE_FILTER",
4824///       "minPrice": "0.01000000",
4825///       "maxPrice": "100000.00000000",
4826///       "tickSize": "0.01000000"
4827///     },
4828///     {
4829///       "filterType": "LOT_SIZE",
4830///       "minQty": "0.00001000",
4831///       "maxQty": "9000.00000000",
4832///       "stepSize": "0.00001000"
4833///     },
4834///     {
4835///       "filterType": "MIN_NOTIONAL",
4836///       "minNotional": "10.00000000"
4837///     }
4838///   ]
4839/// }
4840/// ```
4841pub fn parse_trading_limits(
4842    data: &Value,
4843    _symbol: String,
4844) -> Result<ccxt_core::types::TradingLimits> {
4845    use ccxt_core::types::{MinMax, TradingLimits};
4846
4847    let mut price_limits = MinMax::default();
4848    let mut amount_limits = MinMax::default();
4849    let mut cost_limits = MinMax::default();
4850
4851    if let Some(filters) = data["filters"].as_array() {
4852        for filter in filters {
4853            let filter_type = filter["filterType"].as_str().unwrap_or("");
4854
4855            match filter_type {
4856                "PRICE_FILTER" => {
4857                    price_limits.min = filter["minPrice"]
4858                        .as_str()
4859                        .and_then(|s| Decimal::from_str(s).ok());
4860                    price_limits.max = filter["maxPrice"]
4861                        .as_str()
4862                        .and_then(|s| Decimal::from_str(s).ok());
4863                }
4864                "LOT_SIZE" => {
4865                    amount_limits.min = filter["minQty"]
4866                        .as_str()
4867                        .and_then(|s| Decimal::from_str(s).ok());
4868                    amount_limits.max = filter["maxQty"]
4869                        .as_str()
4870                        .and_then(|s| Decimal::from_str(s).ok());
4871                }
4872                "MIN_NOTIONAL" | "NOTIONAL" => {
4873                    cost_limits.min = filter["minNotional"]
4874                        .as_str()
4875                        .and_then(|s| Decimal::from_str(s).ok());
4876                }
4877                _ => {}
4878            }
4879        }
4880    }
4881
4882    Ok(TradingLimits {
4883        min: None,
4884        max: None,
4885        amount: Some(amount_limits),
4886        price: Some(price_limits),
4887        cost: Some(cost_limits),
4888    })
4889}
4890/// Parse leverage tier from Binance API response.
4891///
4892/// # Arguments
4893///
4894/// * `data` - Binance leverage tier JSON data
4895/// * `market` - Market information for symbol mapping
4896///
4897/// # Returns
4898///
4899/// Returns a [`LeverageTier`](ccxt_core::types::LeverageTier).
4900pub fn parse_leverage_tier(data: &Value, market: &Market) -> Result<LeverageTier> {
4901    let tier = data["bracket"]
4902        .as_i64()
4903        .or_else(|| data["tier"].as_i64())
4904        .unwrap_or(0) as i32;
4905
4906    let min_notional = parse_decimal(data, "notionalFloor")
4907        .or_else(|| parse_decimal(data, "minNotional"))
4908        .unwrap_or(Decimal::ZERO);
4909
4910    let max_notional = parse_decimal(data, "notionalCap")
4911        .or_else(|| parse_decimal(data, "maxNotional"))
4912        .unwrap_or(Decimal::MAX);
4913
4914    let maintenance_margin_rate = parse_decimal(data, "maintMarginRatio")
4915        .or_else(|| parse_decimal(data, "maintenanceMarginRate"))
4916        .unwrap_or(Decimal::ZERO);
4917
4918    let max_leverage = data["initialLeverage"]
4919        .as_i64()
4920        .or_else(|| data["maxLeverage"].as_i64())
4921        .unwrap_or(1) as i32;
4922
4923    Ok(LeverageTier {
4924        info: data.clone(),
4925        tier,
4926        symbol: market.symbol.clone(),
4927        currency: market.quote.clone(),
4928        min_notional,
4929        max_notional,
4930        maintenance_margin_rate,
4931        max_leverage,
4932    })
4933}
4934
4935/// Parse isolated margin borrow rates from Binance API response.
4936///
4937/// # Arguments
4938///
4939/// * `data` - Binance isolated margin borrow rates JSON array
4940///
4941/// # Returns
4942///
4943/// Returns a `HashMap` of [`IsolatedBorrowRate`](ccxt_core::types::IsolatedBorrowRate) keyed by symbol.
4944pub fn parse_isolated_borrow_rates(
4945    data: &Value,
4946) -> Result<std::collections::HashMap<String, ccxt_core::types::IsolatedBorrowRate>> {
4947    use ccxt_core::types::IsolatedBorrowRate;
4948    use std::collections::HashMap;
4949
4950    let mut rates_map = HashMap::new();
4951
4952    if let Some(array) = data.as_array() {
4953        for item in array {
4954            let symbol = item["symbol"].as_str().unwrap_or("");
4955            let base = item["base"].as_str().unwrap_or("");
4956            let quote = item["quote"].as_str().unwrap_or("");
4957
4958            let base_rate = item["dailyInterestRate"]
4959                .as_str()
4960                .and_then(|s| s.parse::<f64>().ok())
4961                .unwrap_or(0.0);
4962
4963            let quote_rate = item["quoteDailyInterestRate"]
4964                .as_str()
4965                .and_then(|s| s.parse::<f64>().ok())
4966                .or_else(|| {
4967                    item["dailyInterestRate"]
4968                        .as_str()
4969                        .and_then(|s| s.parse::<f64>().ok())
4970                })
4971                .unwrap_or(0.0);
4972
4973            let timestamp = item["timestamp"].as_i64().or_else(|| item["time"].as_i64());
4974
4975            let datetime = timestamp.and_then(|ts| {
4976                chrono::DateTime::from_timestamp_millis(ts).map(|dt| dt.to_rfc3339())
4977            });
4978
4979            let isolated_rate = IsolatedBorrowRate {
4980                symbol: symbol.to_string(),
4981                base: base.to_string(),
4982                base_rate,
4983                quote: quote.to_string(),
4984                quote_rate,
4985                period: 86400000, // 1 day in milliseconds
4986                timestamp,
4987                datetime,
4988                info: item.clone(),
4989            };
4990
4991            rates_map.insert(symbol.to_string(), isolated_rate);
4992        }
4993    }
4994
4995    Ok(rates_map)
4996}
4997
4998/// Parse multiple borrow interest records from Binance API response.
4999///
5000/// # Arguments
5001///
5002/// * `data` - Binance borrow interest records JSON array
5003///
5004/// # Returns
5005///
5006/// Returns a vector of [`BorrowInterest`](ccxt_core::types::BorrowInterest).
5007pub fn parse_borrow_interests(data: &Value) -> Result<Vec<BorrowInterest>> {
5008    let mut interests = Vec::new();
5009
5010    if let Some(array) = data.as_array() {
5011        for item in array {
5012            match parse_borrow_interest(item) {
5013                Ok(interest) => interests.push(interest),
5014                Err(e) => {
5015                    eprintln!("Failed to parse borrow interest: {}", e);
5016                }
5017            }
5018        }
5019    }
5020
5021    Ok(interests)
5022}
5023
5024/// Parse borrow rate history from Binance API response.
5025///
5026/// # Arguments
5027///
5028/// * `data` - Binance borrow rate history JSON data
5029/// * `currency` - Currency code
5030///
5031/// # Returns
5032///
5033/// Returns a [`BorrowRateHistory`](ccxt_core::types::BorrowRateHistory).
5034pub fn parse_borrow_rate_history(data: &Value, currency: &str) -> Result<BorrowRateHistory> {
5035    let timestamp = data["timestamp"]
5036        .as_i64()
5037        .or_else(|| data["time"].as_i64())
5038        .unwrap_or(0);
5039
5040    let rate = data["hourlyInterestRate"]
5041        .as_str()
5042        .or_else(|| data["dailyInterestRate"].as_str())
5043        .or_else(|| data["rate"].as_str())
5044        .and_then(|s| s.parse::<f64>().ok())
5045        .or_else(|| data["hourlyInterestRate"].as_f64())
5046        .or_else(|| data["dailyInterestRate"].as_f64())
5047        .or_else(|| data["rate"].as_f64())
5048        .unwrap_or(0.0);
5049
5050    let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
5051        .map(|dt| dt.to_rfc3339())
5052        .unwrap_or_default();
5053
5054    let symbol = data["symbol"].as_str().map(|s| s.to_string());
5055    let vip_level = data["vipLevel"].as_i64().map(|v| v as i32);
5056
5057    Ok(BorrowRateHistory {
5058        currency: currency.to_string(),
5059        symbol,
5060        rate,
5061        timestamp,
5062        datetime,
5063        vip_level,
5064        info: data.clone(),
5065    })
5066}
5067
5068/// Parse ledger entry from Binance API response.
5069///
5070/// # Arguments
5071///
5072/// * `data` - Binance ledger entry JSON data
5073///
5074/// # Returns
5075///
5076/// Returns a [`LedgerEntry`](ccxt_core::types::LedgerEntry).
5077pub fn parse_ledger_entry(data: &Value) -> Result<LedgerEntry> {
5078    let id = data["tranId"]
5079        .as_i64()
5080        .or_else(|| data["id"].as_i64())
5081        .map(|v| v.to_string())
5082        .or_else(|| data["tranId"].as_str().map(|s| s.to_string()))
5083        .or_else(|| data["id"].as_str().map(|s| s.to_string()))
5084        .unwrap_or_default();
5085
5086    let currency = data["asset"]
5087        .as_str()
5088        .or_else(|| data["currency"].as_str())
5089        .unwrap_or("")
5090        .to_string();
5091
5092    let amount = data["amount"]
5093        .as_str()
5094        .and_then(|s| s.parse::<f64>().ok())
5095        .or_else(|| data["amount"].as_f64())
5096        .or_else(|| data["qty"].as_str().and_then(|s| s.parse::<f64>().ok()))
5097        .or_else(|| data["qty"].as_f64())
5098        .unwrap_or(0.0);
5099
5100    let timestamp = data["timestamp"]
5101        .as_i64()
5102        .or_else(|| data["time"].as_i64())
5103        .unwrap_or(0);
5104
5105    let type_str = data["type"].as_str().unwrap_or("");
5106    let (direction, entry_type) = match type_str {
5107        "DEPOSIT" => (LedgerDirection::In, LedgerEntryType::Deposit),
5108        "WITHDRAW" => (LedgerDirection::Out, LedgerEntryType::Withdrawal),
5109        "BUY" | "SELL" => (
5110            if amount >= 0.0 {
5111                LedgerDirection::In
5112            } else {
5113                LedgerDirection::Out
5114            },
5115            LedgerEntryType::Trade,
5116        ),
5117        "FEE" => (LedgerDirection::Out, LedgerEntryType::Fee),
5118        "REBATE" => (LedgerDirection::In, LedgerEntryType::Rebate),
5119        "TRANSFER" => (
5120            if amount >= 0.0 {
5121                LedgerDirection::In
5122            } else {
5123                LedgerDirection::Out
5124            },
5125            LedgerEntryType::Transfer,
5126        ),
5127        _ => (
5128            if amount >= 0.0 {
5129                LedgerDirection::In
5130            } else {
5131                LedgerDirection::Out
5132            },
5133            LedgerEntryType::Trade,
5134        ),
5135    };
5136
5137    let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
5138        .map(|dt| dt.to_rfc3339())
5139        .unwrap_or_default();
5140
5141    Ok(LedgerEntry {
5142        id,
5143        currency,
5144        account: None,
5145        reference_account: None,
5146        reference_id: None,
5147        type_: entry_type,
5148        direction,
5149        amount: amount.abs(),
5150        timestamp,
5151        datetime,
5152        before: None,
5153        after: None,
5154        status: None,
5155        fee: None,
5156        info: data.clone(),
5157    })
5158}
5159
5160#[cfg(test)]
5161mod transaction_tests {
5162    use super::*;
5163    use ccxt_core::types::{TransactionStatus, TransactionType};
5164    use serde_json::json;
5165
5166    #[test]
5167    fn test_is_fiat_currency() {
5168        assert!(is_fiat_currency("USD"));
5169        assert!(is_fiat_currency("eur"));
5170        assert!(is_fiat_currency("CNY"));
5171        assert!(!is_fiat_currency("BTC"));
5172        assert!(!is_fiat_currency("ETH"));
5173    }
5174
5175    #[test]
5176    fn test_extract_internal_transfer_id() {
5177        assert_eq!(
5178            extract_internal_transfer_id("Internal transfer 123456"),
5179            "123456"
5180        );
5181        assert_eq!(
5182            extract_internal_transfer_id("normal_hash_abc"),
5183            "normal_hash_abc"
5184        );
5185    }
5186
5187    #[test]
5188    fn test_parse_transaction_status_deposit() {
5189        assert_eq!(
5190            parse_transaction_status_by_type(&json!(0), true),
5191            TransactionStatus::Pending
5192        );
5193        assert_eq!(
5194            parse_transaction_status_by_type(&json!(1), true),
5195            TransactionStatus::Ok
5196        );
5197        assert_eq!(
5198            parse_transaction_status_by_type(&json!(6), true),
5199            TransactionStatus::Ok
5200        );
5201        assert_eq!(
5202            parse_transaction_status_by_type(&json!("Processing"), true),
5203            TransactionStatus::Pending
5204        );
5205        assert_eq!(
5206            parse_transaction_status_by_type(&json!("Successful"), true),
5207            TransactionStatus::Ok
5208        );
5209    }
5210
5211    #[test]
5212    fn test_parse_transaction_status_withdrawal() {
5213        assert_eq!(
5214            parse_transaction_status_by_type(&json!(0), false),
5215            TransactionStatus::Pending
5216        );
5217        assert_eq!(
5218            parse_transaction_status_by_type(&json!(1), false),
5219            TransactionStatus::Canceled
5220        );
5221        assert_eq!(
5222            parse_transaction_status_by_type(&json!(6), false),
5223            TransactionStatus::Ok
5224        );
5225    }
5226
5227    #[test]
5228    fn test_parse_deposit_transaction() {
5229        let data = json!({
5230            "id": "deposit123",
5231            "amount": "0.5",
5232            "coin": "BTC",
5233            "network": "BTC",
5234            "status": 1,
5235            "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
5236            "addressTag": "",
5237            "txId": "hash123abc",
5238            "insertTime": 1609459200000i64,
5239            "transferType": 0
5240        });
5241
5242        let tx = parse_transaction(&data, TransactionType::Deposit).unwrap();
5243        assert_eq!(tx.id, "deposit123");
5244        assert_eq!(tx.currency, "BTC");
5245        assert_eq!(tx.amount, Decimal::from_str("0.5").unwrap());
5246        assert_eq!(tx.status, TransactionStatus::Ok);
5247        assert_eq!(tx.txid, Some("hash123abc".to_string()));
5248        assert_eq!(tx.internal, Some(false));
5249        assert!(tx.is_deposit());
5250    }
5251
5252    #[test]
5253    fn test_parse_withdrawal_transaction() {
5254        let data = json!({
5255            "id": "withdrawal456",
5256            "amount": "0.3",
5257            "transactionFee": "0.0005",
5258            "coin": "BTC",
5259            "status": 6,
5260            "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
5261            "txId": "hash456def",
5262            "applyTime": "2021-01-01 00:00:00",
5263            "network": "BTC",
5264            "transferType": 0
5265        });
5266
5267        let tx = parse_transaction(&data, TransactionType::Withdrawal).unwrap();
5268        assert_eq!(tx.id, "withdrawal456");
5269        assert_eq!(tx.currency, "BTC");
5270        assert_eq!(tx.amount, Decimal::from_str("0.3").unwrap());
5271        assert_eq!(tx.status, TransactionStatus::Ok);
5272        assert!(tx.fee.is_some());
5273        assert_eq!(
5274            tx.fee.as_ref().unwrap().cost,
5275            Decimal::from_str("0.0005").unwrap()
5276        );
5277        assert!(tx.is_withdrawal());
5278    }
5279
5280    #[test]
5281    fn test_parse_internal_transfer() {
5282        let data = json!({
5283            "id": "internal789",
5284            "amount": "1.0",
5285            "coin": "USDT",
5286            "status": 1,
5287            "txId": "Internal transfer 789xyz",
5288            "insertTime": 1609459200000i64,
5289            "transferType": 1
5290        });
5291
5292        let tx = parse_transaction(&data, TransactionType::Deposit).unwrap();
5293        assert_eq!(tx.internal, Some(true));
5294        assert_eq!(tx.txid, Some("789xyz".to_string()));
5295    }
5296
5297    #[test]
5298    fn test_parse_deposit_address() {
5299        let data = json!({
5300            "coin": "BTC",
5301            "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
5302            "tag": "",
5303            "network": "BTC",
5304            "url": "https://btc.com/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
5305        });
5306
5307        let addr = parse_deposit_address(&data).unwrap();
5308        assert_eq!(addr.currency, "BTC");
5309        assert_eq!(addr.address, "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa");
5310        assert_eq!(addr.network, Some("BTC".to_string()));
5311        assert_eq!(addr.tag, None);
5312    }
5313
5314    #[test]
5315    fn test_parse_deposit_address_with_tag() {
5316        let data = json!({
5317            "coin": "XRP",
5318            "address": "rLHzPsX6oXkzU9rKmLwCdxoEFdLQsSz6Xg",
5319            "tag": "123456",
5320            "network": "XRP"
5321        });
5322
5323        let addr = parse_deposit_address(&data).unwrap();
5324        assert_eq!(addr.currency, "XRP");
5325        assert_eq!(addr.tag, Some("123456".to_string()));
5326    }
5327}
5328
5329// ============================================================================
5330// WebSocket Parser Tests
5331// ============================================================================
5332
5333#[cfg(test)]
5334mod ws_parser_tests {
5335    use super::*;
5336    use rust_decimal_macros::dec;
5337    use serde_json::json;
5338
5339    #[test]
5340    fn test_parse_ws_ticker_24hr() {
5341        let data = json!({
5342            "e": "24hrTicker",
5343            "E": 1609459200000i64,
5344            "s": "BTCUSDT",
5345            "p": "1000.00",
5346            "P": "2.04",
5347            "c": "50000.00",
5348            "o": "49000.00",
5349            "h": "51000.00",
5350            "l": "48500.00",
5351            "v": "1000.5",
5352            "q": "50000000.0",
5353            "b": "49999.00",
5354            "B": "1.5",
5355            "a": "50001.00",
5356            "A": "2.0"
5357        });
5358
5359        let ticker = parse_ws_ticker(&data, None).unwrap();
5360        assert_eq!(ticker.symbol, "BTCUSDT");
5361        assert_eq!(
5362            ticker.last,
5363            Some(Price::new(Decimal::from_str_radix("50000.00", 10).unwrap()))
5364        );
5365        assert_eq!(
5366            ticker.open,
5367            Some(Price::new(Decimal::from_str_radix("49000.00", 10).unwrap()))
5368        );
5369        assert_eq!(
5370            ticker.high,
5371            Some(Price::new(Decimal::from_str_radix("51000.00", 10).unwrap()))
5372        );
5373        assert_eq!(
5374            ticker.low,
5375            Some(Price::new(Decimal::from_str_radix("48500.00", 10).unwrap()))
5376        );
5377        assert_eq!(
5378            ticker.bid,
5379            Some(Price::new(Decimal::from_str_radix("49999.00", 10).unwrap()))
5380        );
5381        assert_eq!(
5382            ticker.ask,
5383            Some(Price::new(Decimal::from_str_radix("50001.00", 10).unwrap()))
5384        );
5385        assert_eq!(ticker.timestamp, 1609459200000);
5386    }
5387
5388    #[test]
5389    fn test_parse_ws_ticker_mark_price() {
5390        let data = json!({
5391            "e": "markPriceUpdate",
5392            "E": 1609459200000i64,
5393            "s": "BTCUSDT",
5394            "p": "50250.50",
5395            "i": "50000.00",
5396            "r": "0.00010000",
5397            "T": 1609459300000i64
5398        });
5399
5400        let ticker = parse_ws_ticker(&data, None).unwrap();
5401        assert_eq!(ticker.symbol, "BTCUSDT");
5402        assert_eq!(
5403            ticker.last,
5404            Some(Price::new(Decimal::from_str_radix("50250.50", 10).unwrap()))
5405        );
5406        assert_eq!(ticker.timestamp, 1609459200000);
5407    }
5408
5409    #[test]
5410    fn test_parse_ws_ticker_book_ticker() {
5411        let data = json!({
5412            "s": "BTCUSDT",
5413            "b": "49999.00",
5414            "B": "1.5",
5415            "a": "50001.00",
5416            "A": "2.0",
5417            "E": 1609459200000i64
5418        });
5419
5420        let ticker = parse_ws_ticker(&data, None).unwrap();
5421        assert_eq!(ticker.symbol, "BTCUSDT");
5422        assert_eq!(
5423            ticker.bid,
5424            Some(Price::new(Decimal::from_str_radix("49999.00", 10).unwrap()))
5425        );
5426        assert_eq!(
5427            ticker.ask,
5428            Some(Price::new(Decimal::from_str_radix("50001.00", 10).unwrap()))
5429        );
5430        assert_eq!(ticker.timestamp, 1609459200000);
5431    }
5432
5433    #[test]
5434    fn test_parse_ws_trade() {
5435        let data = json!({
5436            "e": "trade",
5437            "E": 1609459200000i64,
5438            "s": "BTCUSDT",
5439            "t": 12345,
5440            "p": "50000.00",
5441            "q": "0.5",
5442            "T": 1609459200000i64,
5443            "m": false
5444        });
5445
5446        let trade = parse_ws_trade(&data, None).unwrap();
5447        assert_eq!(trade.id, Some("12345".to_string()));
5448        assert_eq!(trade.symbol, "BTCUSDT");
5449        assert_eq!(
5450            trade.price,
5451            Price::new(Decimal::from_str_radix("50000.00", 10).unwrap())
5452        );
5453        assert_eq!(
5454            trade.amount,
5455            Amount::new(Decimal::from_str_radix("0.5", 10).unwrap())
5456        );
5457        assert_eq!(trade.timestamp, 1609459200000);
5458        assert_eq!(trade.side, OrderSide::Buy); // m=false indicates buy order
5459    }
5460
5461    #[test]
5462    fn test_parse_ws_trade_agg() {
5463        let data = json!({
5464            "e": "aggTrade",
5465            "E": 1609459200000i64,
5466            "s": "BTCUSDT",
5467            "a": 67890,
5468            "p": "50000.00",
5469            "q": "0.5",
5470            "T": 1609459200000i64,
5471            "m": true
5472        });
5473
5474        let trade = parse_ws_trade(&data, None).unwrap();
5475        assert_eq!(trade.id, Some("67890".to_string()));
5476        assert_eq!(trade.symbol, "BTCUSDT");
5477        assert_eq!(trade.side, OrderSide::Sell); // m=true indicates sell order
5478    }
5479
5480    #[test]
5481    fn test_parse_ws_orderbook() {
5482        let data = json!({
5483            "e": "depthUpdate",
5484            "E": 1609459200000i64,
5485            "s": "BTCUSDT",
5486            "U": 157,
5487            "u": 160,
5488            "b": [
5489                ["49999.00", "1.5"],
5490                ["49998.00", "2.0"]
5491            ],
5492            "a": [
5493                ["50001.00", "2.0"],
5494                ["50002.00", "1.5"]
5495            ]
5496        });
5497
5498        let orderbook = parse_ws_orderbook(&data, "BTCUSDT".to_string()).unwrap();
5499        assert_eq!(orderbook.symbol, "BTCUSDT");
5500        assert_eq!(orderbook.bids.len(), 2);
5501        assert_eq!(orderbook.asks.len(), 2);
5502        assert_eq!(
5503            orderbook.bids[0].price,
5504            Price::new(Decimal::from_str_radix("49999.00", 10).unwrap())
5505        );
5506        assert_eq!(
5507            orderbook.bids[0].amount,
5508            Amount::new(Decimal::from_str_radix("1.5", 10).unwrap())
5509        );
5510        assert_eq!(
5511            orderbook.asks[0].price,
5512            Price::new(Decimal::from_str_radix("50001.00", 10).unwrap())
5513        );
5514        assert_eq!(
5515            orderbook.asks[0].amount,
5516            Amount::new(Decimal::from_str_radix("2.0", 10).unwrap())
5517        );
5518        assert_eq!(orderbook.timestamp, 1609459200000);
5519    }
5520
5521    #[test]
5522    fn test_parse_ws_ohlcv() {
5523        let data = json!({
5524            "e": "kline",
5525            "E": 1609459200000i64,
5526            "s": "BTCUSDT",
5527            "k": {
5528                "t": 1609459200000i64,
5529                "o": "49000.00",
5530                "h": "51000.00",
5531                "l": "48500.00",
5532                "c": "50000.00",
5533                "v": "1000.5"
5534            }
5535        });
5536
5537        let ohlcv = parse_ws_ohlcv(&data).unwrap();
5538        assert_eq!(ohlcv.timestamp, 1609459200000);
5539        assert_eq!(ohlcv.open, 49000.00);
5540        assert_eq!(ohlcv.high, 51000.00);
5541        assert_eq!(ohlcv.low, 48500.00);
5542        assert_eq!(ohlcv.close, 50000.00);
5543        assert_eq!(ohlcv.volume, 1000.5);
5544    }
5545
5546    #[test]
5547    fn test_parse_ws_bid_ask() {
5548        let data = json!({
5549            "s": "BTCUSDT",
5550            "b": "49999.00",
5551            "B": "1.5",
5552            "a": "50001.00",
5553            "A": "2.0",
5554            "E": 1609459200000i64
5555        });
5556
5557        let bid_ask = parse_ws_bid_ask(&data).unwrap();
5558        assert_eq!(bid_ask.symbol, "BTCUSDT");
5559        assert_eq!(bid_ask.bid_price, dec!(49999.00));
5560        assert_eq!(bid_ask.bid_quantity, dec!(1.5));
5561        assert_eq!(bid_ask.ask_price, dec!(50001.00));
5562        assert_eq!(bid_ask.ask_quantity, dec!(2.0));
5563        assert_eq!(bid_ask.timestamp, 1609459200000);
5564
5565        // Test utility methods
5566        let spread = bid_ask.spread();
5567        assert_eq!(spread, dec!(2.0));
5568
5569        let mid_price = bid_ask.mid_price();
5570        assert_eq!(mid_price, dec!(50000.0));
5571    }
5572    #[test]
5573    fn test_parse_ws_mark_price() {
5574        let data = json!({
5575            "e": "markPriceUpdate",
5576            "E": 1609459200000i64,
5577            "s": "BTCUSDT",
5578            "p": "50250.50",
5579            "i": "50000.00",
5580            "P": "50500.00",
5581            "r": "0.00010000",
5582            "T": 1609459300000i64
5583        });
5584
5585        let mark_price = parse_ws_mark_price(&data).unwrap();
5586        assert_eq!(mark_price.symbol, "BTCUSDT");
5587        assert_eq!(mark_price.mark_price, dec!(50250.50));
5588        assert_eq!(mark_price.index_price, Some(dec!(50000.00)));
5589        assert_eq!(mark_price.estimated_settle_price, Some(dec!(50500.00)));
5590        assert_eq!(mark_price.last_funding_rate, Some(dec!(0.0001)));
5591        assert_eq!(mark_price.next_funding_time, Some(1609459300000));
5592        assert_eq!(mark_price.timestamp, 1609459200000);
5593
5594        // Test utility methods
5595        let basis = mark_price.basis();
5596        assert_eq!(basis, Some(dec!(250.50)));
5597
5598        let funding_rate_pct = mark_price.funding_rate_percent();
5599        assert_eq!(funding_rate_pct, Some(dec!(0.01)));
5600    }
5601}