1use super::{parse_decimal, parse_decimal_multi, value_to_hashmap};
2use ccxt_core::{
3 Result,
4 error::{Error, ParseError},
5 types::{
6 Market, Ticker,
7 financial::{Amount, Price},
8 },
9};
10use serde_json::Value;
11
12pub fn parse_ticker(data: &Value, market: Option<&Market>) -> Result<Ticker> {
14 let symbol = if let Some(m) = market {
15 m.symbol.clone()
16 } else {
17 data["symbol"]
18 .as_str()
19 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
20 .to_string()
21 };
22
23 let timestamp = data["closeTime"].as_i64();
24
25 Ok(Ticker {
26 symbol,
27 timestamp: timestamp.unwrap_or(0),
28 datetime: timestamp.map(|t| {
29 chrono::DateTime::from_timestamp(t / 1000, 0)
30 .map(|dt| dt.to_rfc3339())
31 .unwrap_or_default()
32 }),
33 high: parse_decimal(data, "highPrice").map(Price::new),
34 low: parse_decimal(data, "lowPrice").map(Price::new),
35 bid: parse_decimal(data, "bidPrice").map(Price::new),
36 bid_volume: parse_decimal(data, "bidQty").map(Amount::new),
37 ask: parse_decimal(data, "askPrice").map(Price::new),
38 ask_volume: parse_decimal(data, "askQty").map(Amount::new),
39 vwap: parse_decimal(data, "weightedAvgPrice").map(Price::new),
40 open: parse_decimal(data, "openPrice").map(Price::new),
41 close: parse_decimal(data, "lastPrice").map(Price::new),
42 last: parse_decimal(data, "lastPrice").map(Price::new),
43 previous_close: parse_decimal(data, "prevClosePrice").map(Price::new),
44 change: parse_decimal(data, "priceChange").map(Price::new),
45 percentage: parse_decimal(data, "priceChangePercent"),
46 average: None,
47 base_volume: parse_decimal(data, "volume").map(Amount::new),
48 quote_volume: parse_decimal(data, "quoteVolume").map(Amount::new),
49 funding_rate: None,
50 open_interest: None,
51 index_price: None,
52 mark_price: None,
53 info: value_to_hashmap(data),
54 })
55}
56
57pub fn parse_ws_ticker(data: &Value, market: Option<&Market>) -> Result<Ticker> {
59 let market_id = data["s"]
60 .as_str()
61 .or_else(|| data["symbol"].as_str())
62 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?;
63
64 let symbol = if let Some(m) = market {
65 m.symbol.clone()
66 } else {
67 market_id.to_string()
68 };
69
70 let event = data["e"].as_str().unwrap_or("bookTicker");
71
72 if event == "markPriceUpdate" {
73 let timestamp = data["E"].as_i64().unwrap_or(0);
74 return Ok(Ticker {
75 symbol,
76 timestamp,
77 datetime: Some(
78 chrono::DateTime::from_timestamp_millis(timestamp)
79 .map(|dt| dt.to_rfc3339())
80 .unwrap_or_default(),
81 ),
82 high: None,
83 low: None,
84 bid: None,
85 bid_volume: None,
86 ask: None,
87 ask_volume: None,
88 vwap: None,
89 open: None,
90 close: parse_decimal(data, "p").map(Price::from),
91 last: parse_decimal(data, "p").map(Price::from),
92 previous_close: None,
93 change: None,
94 percentage: None,
95 average: None,
96 base_volume: None,
97 quote_volume: None,
98 funding_rate: parse_decimal(data, "r"),
99 open_interest: None,
100 index_price: parse_decimal(data, "i").map(Price::from),
101 mark_price: parse_decimal(data, "p").map(Price::from),
102 info: value_to_hashmap(data),
103 });
104 }
105
106 let timestamp = if event == "bookTicker" {
107 data["E"]
108 .as_i64()
109 .or_else(|| data["time"].as_i64())
110 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis())
111 } else {
112 data["C"]
113 .as_i64()
114 .or_else(|| data["E"].as_i64())
115 .or_else(|| data["time"].as_i64())
116 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis())
117 };
118
119 let last = parse_decimal_multi(data, &["c", "price"]);
120
121 Ok(Ticker {
122 symbol,
123 timestamp,
124 datetime: Some(
125 chrono::DateTime::from_timestamp_millis(timestamp)
126 .map(|dt| dt.to_rfc3339())
127 .unwrap_or_default(),
128 ),
129 high: parse_decimal(data, "h").map(Price::from),
130 low: parse_decimal(data, "l").map(Price::from),
131 bid: parse_decimal_multi(data, &["b", "bidPrice"]).map(Price::from),
132 bid_volume: parse_decimal_multi(data, &["B", "bidQty"]).map(Amount::from),
133 ask: parse_decimal_multi(data, &["a", "askPrice"]).map(Price::from),
134 ask_volume: parse_decimal_multi(data, &["A", "askQty"]).map(Amount::from),
135 vwap: parse_decimal(data, "w").map(Price::from),
136 open: parse_decimal(data, "o").map(Price::from),
137 close: last.map(Price::from),
138 last: last.map(Price::from),
139 previous_close: parse_decimal(data, "x").map(Price::from),
140 change: parse_decimal(data, "p").map(Price::from),
141 percentage: parse_decimal(data, "P"),
142 average: None,
143 base_volume: parse_decimal(data, "v").map(Amount::from),
144 quote_volume: parse_decimal(data, "q").map(Amount::from),
145 funding_rate: None,
146 open_interest: None,
147 index_price: None,
148 mark_price: None,
149 info: value_to_hashmap(data),
150 })
151}
152
153pub fn parse_bid_ask(data: &Value) -> Result<ccxt_core::types::BidAsk> {
155 use ccxt_core::types::BidAsk;
156 use rust_decimal::Decimal;
157
158 let symbol = data["symbol"]
159 .as_str()
160 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
161 .to_string();
162
163 let formatted_symbol = if symbol.len() >= 6 {
164 let quote_currencies = ["USDT", "BUSD", "USDC", "BTC", "ETH", "BNB"];
165 let mut found = false;
166 let mut formatted = symbol.clone();
167
168 for quote in "e_currencies {
169 if symbol.ends_with(quote) {
170 let base = &symbol[..symbol.len() - quote.len()];
171 formatted = format!("{}/{}", base, quote);
172 found = true;
173 break;
174 }
175 }
176
177 if found { formatted } else { symbol.clone() }
178 } else {
179 symbol.clone()
180 };
181
182 let bid_price = parse_decimal(data, "bidPrice").unwrap_or(Decimal::ZERO);
183 let bid_quantity = parse_decimal(data, "bidQty").unwrap_or(Decimal::ZERO);
184 let ask_price = parse_decimal(data, "askPrice").unwrap_or(Decimal::ZERO);
185 let ask_quantity = parse_decimal(data, "askQty").unwrap_or(Decimal::ZERO);
186
187 let timestamp = data["time"]
188 .as_i64()
189 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
190
191 Ok(BidAsk {
192 symbol: formatted_symbol,
193 bid_price,
194 bid_quantity,
195 ask_price,
196 ask_quantity,
197 timestamp,
198 })
199}
200
201pub fn parse_bids_asks(data: &Value) -> Result<Vec<ccxt_core::types::BidAsk>> {
203 if let Some(array) = data.as_array() {
204 array.iter().map(|item| parse_bid_ask(item)).collect()
205 } else {
206 Ok(vec![parse_bid_ask(data)?])
207 }
208}
209
210pub fn parse_last_price(data: &Value) -> Result<ccxt_core::types::LastPrice> {
212 use ccxt_core::types::LastPrice;
213 use rust_decimal::Decimal;
214
215 let symbol = data["symbol"]
216 .as_str()
217 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
218 .to_string();
219
220 let formatted_symbol = if symbol.len() >= 6 {
221 let quote_currencies = ["USDT", "BUSD", "USDC", "BTC", "ETH", "BNB"];
222 let mut found = false;
223 let mut formatted = symbol.clone();
224
225 for quote in "e_currencies {
226 if symbol.ends_with(quote) {
227 let base = &symbol[..symbol.len() - quote.len()];
228 formatted = format!("{}/{}", base, quote);
229 found = true;
230 break;
231 }
232 }
233
234 if found { formatted } else { symbol.clone() }
235 } else {
236 symbol.clone()
237 };
238
239 let price = parse_decimal(data, "price").unwrap_or(Decimal::ZERO);
240
241 let timestamp = data["time"]
242 .as_i64()
243 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
244
245 let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
246 .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string())
247 .unwrap_or_default();
248
249 Ok(LastPrice {
250 symbol: formatted_symbol,
251 price,
252 timestamp,
253 datetime,
254 })
255}
256
257pub fn parse_last_prices(data: &Value) -> Result<Vec<ccxt_core::types::LastPrice>> {
259 if let Some(array) = data.as_array() {
260 array.iter().map(|item| parse_last_price(item)).collect()
261 } else {
262 Ok(vec![parse_last_price(data)?])
263 }
264}
265
266pub fn parse_ws_bid_ask(data: &Value) -> Result<ccxt_core::types::BidAsk> {
268 use ccxt_core::types::BidAsk;
269 use rust_decimal::Decimal;
270 use rust_decimal::prelude::FromStr;
271
272 let symbol = data["s"]
273 .as_str()
274 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
275 .to_string();
276
277 let bid_price = data["b"]
278 .as_str()
279 .and_then(|s| Decimal::from_str(s).ok())
280 .ok_or_else(|| Error::from(ParseError::missing_field("bid_price")))?;
281
282 let bid_quantity = data["B"]
283 .as_str()
284 .and_then(|s| Decimal::from_str(s).ok())
285 .ok_or_else(|| Error::from(ParseError::missing_field("bid_quantity")))?;
286
287 let ask_price = data["a"]
288 .as_str()
289 .and_then(|s| Decimal::from_str(s).ok())
290 .ok_or_else(|| Error::from(ParseError::missing_field("ask_price")))?;
291
292 let ask_quantity = data["A"]
293 .as_str()
294 .and_then(|s| Decimal::from_str(s).ok())
295 .ok_or_else(|| Error::from(ParseError::missing_field("ask_quantity")))?;
296
297 let timestamp = data["E"].as_i64().unwrap_or(0);
298
299 Ok(BidAsk {
300 symbol,
301 bid_price,
302 bid_quantity,
303 ask_price,
304 ask_quantity,
305 timestamp,
306 })
307}