Skip to main content

ccxt_exchanges/binance/parser/
market.rs

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