1#![allow(dead_code, clippy::unnecessary_wraps)]
2
3use super::{parse_decimal, value_to_hashmap};
4use ccxt_core::{
5 Result,
6 error::{Error, ParseError},
7 types::{
8 AccountConfig, CommissionRate, FeeTradingFee, IndexPrice, LedgerDirection, LedgerEntry,
9 LedgerEntryType, Liquidation, MarkPrice, Market, MaxLeverage, MinMax, OpenInterest,
10 OpenInterestHistory, PremiumIndex, Stats24hr, TradingLimits,
11 },
12};
13use rust_decimal::Decimal;
14use rust_decimal::prelude::{FromPrimitive, FromStr, ToPrimitive};
15use serde_json::Value;
16
17pub fn parse_server_time(data: &Value) -> Result<ccxt_core::types::ServerTime> {
19 use ccxt_core::types::ServerTime;
20
21 let server_time = data["serverTime"]
22 .as_i64()
23 .ok_or_else(|| Error::from(ParseError::missing_field("serverTime")))?;
24
25 Ok(ServerTime::new(server_time))
26}
27
28pub fn parse_trading_fee(data: &Value) -> Result<FeeTradingFee> {
30 let symbol = data["symbol"]
31 .as_str()
32 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
33 .to_string();
34
35 let maker = data["makerCommission"]
36 .as_str()
37 .and_then(|s| Decimal::from_str(s).ok())
38 .or_else(|| data["makerCommission"].as_f64().and_then(Decimal::from_f64))
39 .ok_or_else(|| {
40 Error::from(ParseError::invalid_format(
41 "data",
42 "Invalid maker commission",
43 ))
44 })?;
45
46 let taker = data["takerCommission"]
47 .as_str()
48 .and_then(|s| Decimal::from_str(s).ok())
49 .or_else(|| data["takerCommission"].as_f64().and_then(Decimal::from_f64))
50 .ok_or_else(|| {
51 Error::from(ParseError::invalid_format(
52 "data",
53 "Invalid taker commission",
54 ))
55 })?;
56
57 Ok(FeeTradingFee::new(symbol, maker, taker))
58}
59
60pub fn parse_trading_fees(data: &Value) -> Result<Vec<FeeTradingFee>> {
62 if let Some(array) = data.as_array() {
63 array.iter().map(|item| parse_trading_fee(item)).collect()
64 } else {
65 Ok(vec![parse_trading_fee(data)?])
66 }
67}
68
69pub fn parse_stats_24hr(data: &Value) -> Result<ccxt_core::types::Stats24hr> {
71 let symbol = data["symbol"]
72 .as_str()
73 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
74 .to_string();
75
76 let price_change = data["priceChange"]
77 .as_str()
78 .and_then(|s| Decimal::from_str(s).ok());
79
80 let price_change_percent = data["priceChangePercent"]
81 .as_str()
82 .and_then(|s| Decimal::from_str(s).ok());
83
84 let weighted_avg_price = data["weightedAvgPrice"]
85 .as_str()
86 .and_then(|s| Decimal::from_str(s).ok());
87
88 let prev_close_price = data["prevClosePrice"]
89 .as_str()
90 .and_then(|s| Decimal::from_str(s).ok());
91
92 let last_price = data["lastPrice"]
93 .as_str()
94 .and_then(|s| Decimal::from_str(s).ok());
95
96 let last_qty = data["lastQty"]
97 .as_str()
98 .and_then(|s| Decimal::from_str(s).ok());
99
100 let bid_price = data["bidPrice"]
101 .as_str()
102 .and_then(|s| Decimal::from_str(s).ok());
103
104 let bid_qty = data["bidQty"]
105 .as_str()
106 .and_then(|s| Decimal::from_str(s).ok());
107
108 let ask_price = data["askPrice"]
109 .as_str()
110 .and_then(|s| Decimal::from_str(s).ok());
111
112 let ask_qty = data["askQty"]
113 .as_str()
114 .and_then(|s| Decimal::from_str(s).ok());
115
116 let open_price = data["openPrice"]
117 .as_str()
118 .and_then(|s| Decimal::from_str(s).ok());
119
120 let high_price = data["highPrice"]
121 .as_str()
122 .and_then(|s| Decimal::from_str(s).ok());
123
124 let low_price = data["lowPrice"]
125 .as_str()
126 .and_then(|s| Decimal::from_str(s).ok());
127
128 let volume = data["volume"]
129 .as_str()
130 .and_then(|s| Decimal::from_str(s).ok());
131
132 let quote_volume = data["quoteVolume"]
133 .as_str()
134 .and_then(|s| Decimal::from_str(s).ok());
135
136 let open_time = data["openTime"].as_i64();
137 let close_time = data["closeTime"].as_i64();
138 let first_id = data["firstId"].as_i64();
139 let last_id = data["lastId"].as_i64();
140 let count = data["count"].as_i64();
141
142 Ok(Stats24hr {
143 symbol,
144 price_change,
145 price_change_percent,
146 weighted_avg_price,
147 prev_close_price,
148 last_price,
149 last_qty,
150 bid_price,
151 bid_qty,
152 ask_price,
153 ask_qty,
154 open_price,
155 high_price,
156 low_price,
157 volume,
158 quote_volume,
159 open_time,
160 close_time,
161 first_id,
162 last_id,
163 count,
164 info: value_to_hashmap(data),
165 })
166}
167
168pub fn parse_trading_limits(
170 data: &Value,
171 _symbol: String,
172) -> Result<ccxt_core::types::TradingLimits> {
173 let mut price_limits = MinMax::default();
174 let mut amount_limits = MinMax::default();
175 let mut cost_limits = MinMax::default();
176
177 if let Some(filters) = data["filters"].as_array() {
178 for filter in filters {
179 let filter_type = filter["filterType"].as_str().unwrap_or("");
180
181 match filter_type {
182 "PRICE_FILTER" => {
183 price_limits.min = filter["minPrice"]
184 .as_str()
185 .and_then(|s| Decimal::from_str(s).ok());
186 price_limits.max = filter["maxPrice"]
187 .as_str()
188 .and_then(|s| Decimal::from_str(s).ok());
189 }
190 "LOT_SIZE" => {
191 amount_limits.min = filter["minQty"]
192 .as_str()
193 .and_then(|s| Decimal::from_str(s).ok());
194 amount_limits.max = filter["maxQty"]
195 .as_str()
196 .and_then(|s| Decimal::from_str(s).ok());
197 }
198 "MIN_NOTIONAL" | "NOTIONAL" => {
199 cost_limits.min = filter["minNotional"]
200 .as_str()
201 .and_then(|s| Decimal::from_str(s).ok());
202 }
203 _ => {}
204 }
205 }
206 }
207
208 Ok(TradingLimits {
209 min: None,
210 max: None,
211 amount: Some(amount_limits),
212 price: Some(price_limits),
213 cost: Some(cost_limits),
214 })
215}
216
217pub fn parse_account_config(data: &Value) -> Result<AccountConfig> {
219 let multi_assets_margin = data["multiAssetsMargin"].as_bool().unwrap_or(false);
220 let fee_tier = data["feeTier"].as_i64().unwrap_or(0) as i32;
221 let can_trade = data["canTrade"].as_bool().unwrap_or(true);
222 let can_deposit = data["canDeposit"].as_bool().unwrap_or(true);
223 let can_withdraw = data["canWithdraw"].as_bool().unwrap_or(true);
224 let update_time = data["updateTime"].as_i64().unwrap_or(0);
225
226 Ok(AccountConfig {
227 info: Some(data.clone()),
228 multi_assets_margin,
229 fee_tier,
230 can_trade,
231 can_deposit,
232 can_withdraw,
233 update_time,
234 })
235}
236
237pub fn parse_commission_rate(data: &Value, market: &Market) -> Result<CommissionRate> {
239 let maker_commission_rate = data["makerCommissionRate"]
240 .as_str()
241 .and_then(|s| s.parse::<f64>().ok())
242 .unwrap_or(0.0);
243
244 let taker_commission_rate = data["takerCommissionRate"]
245 .as_str()
246 .and_then(|s| s.parse::<f64>().ok())
247 .unwrap_or(0.0);
248
249 Ok(CommissionRate {
250 info: Some(data.clone()),
251 symbol: market.symbol.clone(),
252 maker_commission_rate,
253 taker_commission_rate,
254 })
255}
256
257pub fn parse_open_interest(data: &Value, market: &Market) -> Result<OpenInterest> {
259 let open_interest = data["openInterest"]
260 .as_str()
261 .and_then(|s| s.parse::<f64>().ok())
262 .or_else(|| data["openInterest"].as_f64())
263 .unwrap_or(0.0);
264
265 let timestamp = data["time"]
266 .as_i64()
267 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
268
269 let contract_size = market
270 .contract_size
271 .unwrap_or_else(|| rust_decimal::Decimal::from(1))
272 .to_f64()
273 .unwrap_or(1.0);
274 let open_interest_value = open_interest * contract_size;
275
276 Ok(OpenInterest {
277 info: Some(data.clone()),
278 symbol: market.symbol.clone(),
279 open_interest,
280 open_interest_value,
281 timestamp,
282 })
283}
284
285pub fn parse_open_interest_history(
287 data: &Value,
288 market: &Market,
289) -> Result<Vec<OpenInterestHistory>> {
290 let array = data.as_array().ok_or_else(|| {
291 Error::from(ParseError::invalid_value(
292 "data",
293 "Expected array for open interest history",
294 ))
295 })?;
296
297 let mut result = Vec::new();
298
299 for item in array {
300 let sum_open_interest = item["sumOpenInterest"]
301 .as_str()
302 .and_then(|s| s.parse::<f64>().ok())
303 .or_else(|| item["sumOpenInterest"].as_f64())
304 .unwrap_or(0.0);
305
306 let sum_open_interest_value = item["sumOpenInterestValue"]
307 .as_str()
308 .and_then(|s| s.parse::<f64>().ok())
309 .or_else(|| item["sumOpenInterestValue"].as_f64())
310 .unwrap_or(0.0);
311
312 let timestamp = item["timestamp"]
313 .as_i64()
314 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
315
316 result.push(OpenInterestHistory {
317 info: Some(item.clone()),
318 symbol: market.symbol.clone(),
319 sum_open_interest,
320 sum_open_interest_value,
321 timestamp,
322 });
323 }
324
325 Ok(result)
326}
327
328pub fn parse_max_leverage(data: &Value, market: &Market) -> Result<MaxLeverage> {
330 let target_data = if let Some(array) = data.as_array() {
331 array
332 .iter()
333 .find(|item| item["symbol"].as_str().unwrap_or("") == market.id)
334 .ok_or_else(|| {
335 Error::from(ParseError::invalid_value(
336 "symbol",
337 format!("Symbol {} not found in leverage brackets", market.id),
338 ))
339 })?
340 } else {
341 data
342 };
343
344 let brackets = target_data["brackets"]
345 .as_array()
346 .ok_or_else(|| Error::from(ParseError::missing_field("brackets")))?;
347
348 if brackets.is_empty() {
349 return Err(Error::from(ParseError::invalid_value(
350 "data",
351 "Empty brackets array",
352 )));
353 }
354
355 let first_bracket = &brackets[0];
356 let max_leverage = first_bracket["initialLeverage"].as_i64().unwrap_or(1) as i32;
357
358 let notional = first_bracket["notionalCap"]
359 .as_f64()
360 .or_else(|| {
361 first_bracket["notionalCap"]
362 .as_str()
363 .and_then(|s| s.parse::<f64>().ok())
364 })
365 .unwrap_or(0.0);
366
367 Ok(MaxLeverage {
368 info: Some(data.clone()),
369 symbol: market.symbol.clone(),
370 max_leverage,
371 notional,
372 })
373}
374
375pub fn parse_index_price(data: &Value, market: &Market) -> Result<IndexPrice> {
377 let index_price = data["indexPrice"]
378 .as_f64()
379 .or_else(|| {
380 data["indexPrice"]
381 .as_str()
382 .and_then(|s| s.parse::<f64>().ok())
383 })
384 .ok_or_else(|| Error::from(ParseError::missing_field("indexPrice")))?;
385
386 let timestamp = data["time"]
387 .as_i64()
388 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
389
390 Ok(IndexPrice {
391 info: Some(data.clone()),
392 symbol: market.symbol.clone(),
393 index_price,
394 timestamp,
395 })
396}
397
398pub fn parse_premium_index(data: &Value, market: &Market) -> Result<PremiumIndex> {
400 let mark_price = data["markPrice"]
401 .as_f64()
402 .or_else(|| {
403 data["markPrice"]
404 .as_str()
405 .and_then(|s| s.parse::<f64>().ok())
406 })
407 .ok_or_else(|| Error::from(ParseError::missing_field("markPrice")))?;
408
409 let index_price = data["indexPrice"]
410 .as_f64()
411 .or_else(|| {
412 data["indexPrice"]
413 .as_str()
414 .and_then(|s| s.parse::<f64>().ok())
415 })
416 .ok_or_else(|| Error::from(ParseError::missing_field("indexPrice")))?;
417
418 let estimated_settle_price = data["estimatedSettlePrice"]
419 .as_f64()
420 .or_else(|| {
421 data["estimatedSettlePrice"]
422 .as_str()
423 .and_then(|s| s.parse::<f64>().ok())
424 })
425 .unwrap_or(0.0);
426
427 let last_funding_rate = data["lastFundingRate"]
428 .as_f64()
429 .or_else(|| {
430 data["lastFundingRate"]
431 .as_str()
432 .and_then(|s| s.parse::<f64>().ok())
433 })
434 .unwrap_or(0.0);
435
436 let next_funding_time = data["nextFundingTime"].as_i64().unwrap_or(0);
437
438 let time = data["time"]
439 .as_i64()
440 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
441
442 Ok(PremiumIndex {
443 info: Some(data.clone()),
444 symbol: market.symbol.clone(),
445 mark_price,
446 index_price,
447 estimated_settle_price,
448 last_funding_rate,
449 next_funding_time,
450 time,
451 })
452}
453
454pub fn parse_liquidation(data: &Value, market: &Market) -> Result<Liquidation> {
456 let side = data["side"]
457 .as_str()
458 .ok_or_else(|| Error::from(ParseError::missing_field("side")))?
459 .to_string();
460
461 let order_type = data["type"].as_str().unwrap_or("LIMIT").to_string();
462
463 let time = data["time"]
464 .as_i64()
465 .ok_or_else(|| Error::from(ParseError::missing_field("time")))?;
466
467 let price = data["price"]
468 .as_f64()
469 .or_else(|| data["price"].as_str().and_then(|s| s.parse::<f64>().ok()))
470 .ok_or_else(|| Error::from(ParseError::missing_field("price")))?;
471
472 let quantity = data["origQty"]
473 .as_f64()
474 .or_else(|| data["origQty"].as_str().and_then(|s| s.parse::<f64>().ok()))
475 .ok_or_else(|| Error::from(ParseError::missing_field("origQty")))?;
476
477 let average_price = data["averagePrice"]
478 .as_f64()
479 .or_else(|| {
480 data["averagePrice"]
481 .as_str()
482 .and_then(|s| s.parse::<f64>().ok())
483 })
484 .unwrap_or(price);
485
486 Ok(Liquidation {
487 info: Some(data.clone()),
488 symbol: market.symbol.clone(),
489 side,
490 order_type,
491 time,
492 price,
493 quantity,
494 average_price,
495 })
496}
497
498pub fn parse_mark_price(data: &Value) -> Result<ccxt_core::types::MarkPrice> {
500 let symbol = data["symbol"]
501 .as_str()
502 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
503 .to_string();
504
505 let formatted_symbol = if symbol.len() >= 6 {
506 let quote_currencies = ["USDT", "BUSD", "USDC", "BTC", "ETH", "BNB"];
507 let mut found = false;
508 let mut formatted = symbol.clone();
509
510 for quote in "e_currencies {
511 if symbol.ends_with(quote) {
512 let base = &symbol[..symbol.len() - quote.len()];
513 formatted = format!("{}/{}", base, quote);
514 found = true;
515 break;
516 }
517 }
518
519 if found { formatted } else { symbol.clone() }
520 } else {
521 symbol.clone()
522 };
523
524 let mark_price = parse_decimal(data, "markPrice").unwrap_or(Decimal::ZERO);
525 let index_price = parse_decimal(data, "indexPrice");
526 let estimated_settle_price = parse_decimal(data, "estimatedSettlePrice");
527 let last_funding_rate = parse_decimal(data, "lastFundingRate");
528
529 let next_funding_time = data["nextFundingTime"].as_i64();
530
531 let timestamp = data["time"]
532 .as_i64()
533 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
534
535 Ok(MarkPrice {
536 symbol: formatted_symbol,
537 mark_price,
538 index_price,
539 estimated_settle_price,
540 last_funding_rate,
541 next_funding_time,
542 interest_rate: None,
543 timestamp,
544 })
545}
546
547pub fn parse_mark_prices(data: &Value) -> Result<Vec<ccxt_core::types::MarkPrice>> {
549 if let Some(array) = data.as_array() {
550 array.iter().map(|item| parse_mark_price(item)).collect()
551 } else {
552 Ok(vec![parse_mark_price(data)?])
553 }
554}
555
556pub fn parse_ws_mark_price(data: &Value) -> Result<MarkPrice> {
558 let symbol = data["s"]
559 .as_str()
560 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
561 .to_string();
562
563 let mark_price = data["p"]
564 .as_str()
565 .and_then(|s| s.parse::<Decimal>().ok())
566 .ok_or_else(|| Error::from(ParseError::missing_field("mark_price")))?;
567
568 let index_price = data["i"].as_str().and_then(|s| s.parse::<Decimal>().ok());
569 let estimated_settle_price = data["P"].as_str().and_then(|s| s.parse::<Decimal>().ok());
570 let last_funding_rate = data["r"].as_str().and_then(|s| s.parse::<Decimal>().ok());
571 let next_funding_time = data["T"].as_i64();
572 let interest_rate = None;
573 let timestamp = data["E"].as_i64().unwrap_or(0);
574
575 Ok(MarkPrice {
576 symbol,
577 mark_price,
578 index_price,
579 estimated_settle_price,
580 last_funding_rate,
581 next_funding_time,
582 interest_rate,
583 timestamp,
584 })
585}
586
587pub fn parse_ledger_entry(data: &Value) -> Result<LedgerEntry> {
589 let id = data["tranId"]
590 .as_i64()
591 .or_else(|| data["id"].as_i64())
592 .map(|v| v.to_string())
593 .or_else(|| data["tranId"].as_str().map(ToString::to_string))
594 .or_else(|| data["id"].as_str().map(ToString::to_string))
595 .unwrap_or_default();
596
597 let currency = data["asset"]
598 .as_str()
599 .or_else(|| data["currency"].as_str())
600 .unwrap_or("")
601 .to_string();
602
603 let amount = data["amount"]
604 .as_str()
605 .and_then(|s| s.parse::<f64>().ok())
606 .or_else(|| data["amount"].as_f64())
607 .or_else(|| data["qty"].as_str().and_then(|s| s.parse::<f64>().ok()))
608 .or_else(|| data["qty"].as_f64())
609 .unwrap_or(0.0);
610
611 let timestamp = data["timestamp"]
612 .as_i64()
613 .or_else(|| data["time"].as_i64())
614 .unwrap_or(0);
615
616 let type_str = data["type"].as_str().unwrap_or("");
617 let (direction, entry_type) = match type_str {
618 "DEPOSIT" => (LedgerDirection::In, LedgerEntryType::Deposit),
619 "WITHDRAW" => (LedgerDirection::Out, LedgerEntryType::Withdrawal),
620 "FEE" => (LedgerDirection::Out, LedgerEntryType::Fee),
621 "REBATE" => (LedgerDirection::In, LedgerEntryType::Rebate),
622 "TRANSFER" => (
623 if amount >= 0.0 {
624 LedgerDirection::In
625 } else {
626 LedgerDirection::Out
627 },
628 LedgerEntryType::Transfer,
629 ),
630 _ => (
631 if amount >= 0.0 {
632 LedgerDirection::In
633 } else {
634 LedgerDirection::Out
635 },
636 LedgerEntryType::Trade,
637 ),
638 };
639
640 let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
641 .map(|dt| dt.to_rfc3339())
642 .unwrap_or_default();
643
644 Ok(LedgerEntry {
645 id,
646 currency,
647 account: None,
648 reference_account: None,
649 reference_id: None,
650 type_: entry_type,
651 direction,
652 amount: amount.abs(),
653 timestamp,
654 datetime,
655 before: None,
656 after: None,
657 status: None,
658 fee: None,
659 info: data.clone(),
660 })
661}