ccxt_exchanges/binance/
parser.rs

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