ccxt_exchanges/binance/parser/
market.rs1use 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
12pub 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 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()); 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
211pub 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
326pub 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}