ccxt_exchanges/binance/
parser.rs

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