ccxt_exchanges/binance/parser/
market.rs

1use super::value_to_hashmap;
2use ccxt_core::{
3    Result,
4    error::{Error, ParseError},
5    types::{Market, MarketLimits, MarketPrecision, MarketType, MinMax},
6};
7use rust_decimal::Decimal;
8use rust_decimal::prelude::FromStr;
9use serde_json::Value;
10
11/// Parse market data from Binance exchange info.
12pub fn parse_market(data: &Value) -> Result<Market> {
13    let symbol = data["symbol"]
14        .as_str()
15        .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
16        .to_string();
17
18    let base_asset = data["baseAsset"]
19        .as_str()
20        .ok_or_else(|| Error::from(ParseError::missing_field("baseAsset")))?
21        .to_string();
22
23    let quote_asset = data["quoteAsset"]
24        .as_str()
25        .ok_or_else(|| Error::from(ParseError::missing_field("quoteAsset")))?
26        .to_string();
27
28    let status = data["status"]
29        .as_str()
30        .ok_or_else(|| Error::from(ParseError::missing_field("status")))?;
31
32    let active = status == "TRADING";
33    let margin = data["isMarginTradingAllowed"].as_bool().unwrap_or(false);
34
35    let mut price_precision: Option<Decimal> = None;
36    let mut amount_precision: Option<Decimal> = None;
37    let mut min_amount: Option<Decimal> = None;
38    let mut max_amount: Option<Decimal> = None;
39    let mut min_cost: Option<Decimal> = None;
40    let mut min_price: Option<Decimal> = None;
41    let mut max_price: Option<Decimal> = None;
42
43    if let Some(filters) = data["filters"].as_array() {
44        for filter in filters {
45            let filter_type = filter["filterType"].as_str().unwrap_or("");
46
47            match filter_type {
48                "PRICE_FILTER" => {
49                    if let Some(tick_size) = filter["tickSize"].as_str() {
50                        if let Ok(dec) = Decimal::from_str(tick_size) {
51                            price_precision = Some(dec);
52                        }
53                    }
54                    if let Some(min) = filter["minPrice"].as_str() {
55                        min_price = Decimal::from_str(min).ok();
56                    }
57                    if let Some(max) = filter["maxPrice"].as_str() {
58                        max_price = Decimal::from_str(max).ok();
59                    }
60                }
61                "LOT_SIZE" => {
62                    if let Some(step_size) = filter["stepSize"].as_str() {
63                        if let Ok(dec) = Decimal::from_str(step_size) {
64                            amount_precision = Some(dec);
65                        }
66                    }
67                    if let Some(min) = filter["minQty"].as_str() {
68                        min_amount = Decimal::from_str(min).ok();
69                    }
70                    if let Some(max) = filter["maxQty"].as_str() {
71                        max_amount = Decimal::from_str(max).ok();
72                    }
73                }
74                "MIN_NOTIONAL" | "NOTIONAL" => {
75                    if let Some(min) = filter["minNotional"].as_str() {
76                        min_cost = Decimal::from_str(min).ok();
77                    }
78                }
79                _ => {}
80            }
81        }
82    }
83
84    let unified_symbol = format!("{}/{}", base_asset, quote_asset);
85    let parsed_symbol = ccxt_core::symbol::SymbolParser::parse(&unified_symbol).ok();
86
87    Ok(Market {
88        id: symbol.clone(),
89        symbol: unified_symbol,
90        parsed_symbol,
91        base: base_asset.clone(),
92        quote: quote_asset.clone(),
93        settle: None,
94        base_id: Some(base_asset),
95        quote_id: Some(quote_asset),
96        settle_id: None,
97        market_type: MarketType::Spot,
98        active,
99        margin,
100        contract: Some(false),
101        linear: None,
102        inverse: None,
103        taker: Decimal::from_str("0.001").ok(),
104        maker: Decimal::from_str("0.001").ok(),
105        contract_size: None,
106        expiry: None,
107        expiry_datetime: None,
108        strike: None,
109        option_type: None,
110        percentage: Some(true),
111        tier_based: Some(false),
112        fee_side: Some("quote".to_string()),
113        precision: MarketPrecision {
114            price: price_precision,
115            amount: amount_precision,
116            base: None,
117            quote: None,
118        },
119        limits: MarketLimits {
120            amount: Some(MinMax {
121                min: min_amount,
122                max: max_amount,
123            }),
124            price: Some(MinMax {
125                min: min_price,
126                max: max_price,
127            }),
128            cost: Some(MinMax {
129                min: min_cost,
130                max: None,
131            }),
132            leverage: None,
133        },
134        info: value_to_hashmap(data),
135    })
136}
137
138/// Parse currency information from Binance API response.
139pub fn parse_currency(data: &Value) -> Result<ccxt_core::types::Currency> {
140    use ccxt_core::types::{Currency, CurrencyNetwork, MinMax};
141    use std::collections::HashMap;
142
143    let code = data["coin"]
144        .as_str()
145        .ok_or_else(|| Error::from(ParseError::missing_field("coin")))?
146        .to_string();
147
148    let id = code.clone();
149    let name = data["name"].as_str().map(ToString::to_string);
150    let active = data["trading"].as_bool().unwrap_or(true);
151
152    let mut networks = HashMap::new();
153    let mut global_deposit = false;
154    let mut global_withdraw = false;
155    let mut global_fee = None;
156    let mut global_precision = None;
157    let mut global_limits = MinMax::default();
158
159    if let Some(network_list) = data["networkList"].as_array() {
160        for network_data in network_list {
161            let network_id = network_data["network"]
162                .as_str()
163                .unwrap_or(&code)
164                .to_string();
165
166            let is_default = network_data["isDefault"].as_bool().unwrap_or(false);
167            let deposit_enable = network_data["depositEnable"].as_bool().unwrap_or(false);
168            let withdraw_enable = network_data["withdrawEnable"].as_bool().unwrap_or(false);
169
170            if is_default {
171                global_deposit = deposit_enable;
172                global_withdraw = withdraw_enable;
173            }
174
175            let fee = network_data["withdrawFee"]
176                .as_str()
177                .and_then(|s| Decimal::from_str(s).ok());
178
179            if is_default && fee.is_some() {
180                global_fee = fee;
181            }
182
183            let precision = network_data["withdrawIntegerMultiple"]
184                .as_str()
185                .and_then(|s| Decimal::from_str(s).ok());
186
187            if is_default && precision.is_some() {
188                global_precision = precision;
189            }
190
191            let withdraw_min = network_data["withdrawMin"]
192                .as_str()
193                .and_then(|s| Decimal::from_str(s).ok());
194
195            let withdraw_max = network_data["withdrawMax"]
196                .as_str()
197                .and_then(|s| Decimal::from_str(s).ok());
198
199            let limits = MinMax {
200                min: withdraw_min,
201                max: withdraw_max,
202            };
203
204            if is_default {
205                global_limits = limits.clone();
206            }
207
208            let network = CurrencyNetwork {
209                network: network_id.clone(),
210                id: Some(network_id.clone()),
211                name: network_data["name"].as_str().map(ToString::to_string),
212                active: deposit_enable && withdraw_enable,
213                deposit: deposit_enable,
214                withdraw: withdraw_enable,
215                fee,
216                precision,
217                limits,
218                info: value_to_hashmap(network_data),
219            };
220
221            networks.insert(network_id, network);
222        }
223    }
224
225    if !networks.is_empty() && global_fee.is_none() {
226        if let Some(first) = networks.values().next() {
227            global_fee = first.fee;
228            global_precision = first.precision;
229            global_limits = first.limits.clone();
230        }
231    }
232
233    Ok(Currency {
234        code,
235        id,
236        name,
237        active,
238        deposit: global_deposit,
239        withdraw: global_withdraw,
240        fee: global_fee,
241        precision: global_precision,
242        limits: global_limits,
243        networks,
244        currency_type: if data["isLegalMoney"].as_bool().unwrap_or(false) {
245            Some("fiat".to_string())
246        } else {
247            Some("crypto".to_string())
248        },
249        info: value_to_hashmap(data),
250    })
251}
252
253/// Parse multiple currencies from Binance API response.
254pub fn parse_currencies(data: &Value) -> Result<Vec<ccxt_core::types::Currency>> {
255    if let Some(array) = data.as_array() {
256        array.iter().map(parse_currency).collect()
257    } else {
258        Ok(vec![parse_currency(data)?])
259    }
260}