Skip to main content

ccxt_exchanges/binance/parser/
ticker.rs

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
12/// Parse ticker data from Binance 24hr ticker response.
13pub 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
57/// Parse WebSocket ticker data from Binance streams.
58pub 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
153/// Parse bid/ask price data from Binance ticker API.
154pub 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 &quote_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
201/// Parse multiple bid/ask price data entries from Binance ticker API.
202pub 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
210/// Parse latest price data from Binance ticker API.
211pub 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 &quote_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
257/// Parse multiple latest price data entries from Binance ticker API.
258pub 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
266/// Parse WebSocket BidAsk (best bid/ask prices) data from Binance streams.
267pub 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}