Skip to main content

bothan_bitfinex/api/msg/ticker/
spot.rs

1use std::fmt;
2
3use serde::de::{MapAccess, SeqAccess, Visitor};
4use serde::{Deserialize, Deserializer, Serialize, de};
5
6/// Represents spot ticker data from the Bitfinex API.
7///
8/// `Ticker` struct contains fields matching those returned by the Bitfinex API
9/// for spot ticker events. It serves as an interface for JSON deserialization
10/// of spot trading market data, supporting both array and object-based responses.
11#[derive(Debug, Clone, PartialEq, Serialize)]
12pub struct Ticker {
13    /// The symbol of the requested ticker data (e.g., "tBTCUSD").
14    pub symbol: String,
15    /// Price of last highest bid.
16    pub bid: f64,
17    /// Sum of the 25 highest bid sizes.
18    pub bid_size: f64,
19    /// Price of last lowest ask.
20    pub ask: f64,
21    /// Sum of the 25 lowest ask sizes.
22    pub ask_size: f64,
23    /// The amount that the last price has changed since yesterday.
24    pub daily_change: f64,
25    /// Relative price change since yesterday (*100 for percentage change).
26    pub daily_change_relative: f64,
27    /// Price of the last trade.
28    pub last_price: f64,
29    /// Daily volume.
30    pub volume: f64,
31    /// Daily high.
32    pub high: f64,
33    /// Daily low.
34    pub low: f64,
35}
36
37impl<'de> Deserialize<'de> for Ticker {
38    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
39    where
40        D: Deserializer<'de>,
41    {
42        #[derive(Deserialize)]
43        #[serde(field_identifier, rename_all = "snake_case")]
44        enum Field {
45            Symbol,
46            Bid,
47            BidSize,
48            Ask,
49            AskSize,
50            DailyChange,
51            DailyChangeRelative,
52            LastPrice,
53            Volume,
54            High,
55            Low,
56        }
57
58        struct SpotTickerVisitor {}
59        impl<'de> Visitor<'de> for SpotTickerVisitor {
60            type Value = Ticker;
61
62            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
63                formatter.write_str("tuple with length 11")
64            }
65
66            fn visit_seq<V>(self, mut seq: V) -> Result<Ticker, V::Error>
67            where
68                V: SeqAccess<'de>,
69            {
70                let symbol = seq
71                    .next_element()?
72                    .ok_or_else(|| de::Error::invalid_length(0, &self))?;
73                let bid = seq
74                    .next_element()?
75                    .ok_or_else(|| de::Error::invalid_length(1, &self))?;
76                let bid_size = seq
77                    .next_element()?
78                    .ok_or_else(|| de::Error::invalid_length(2, &self))?;
79                let ask = seq
80                    .next_element()?
81                    .ok_or_else(|| de::Error::invalid_length(3, &self))?;
82                let ask_size = seq
83                    .next_element()?
84                    .ok_or_else(|| de::Error::invalid_length(4, &self))?;
85                let daily_change = seq
86                    .next_element()?
87                    .ok_or_else(|| de::Error::invalid_length(5, &self))?;
88                let daily_change_relative = seq
89                    .next_element()?
90                    .ok_or_else(|| de::Error::invalid_length(6, &self))?;
91                let last_price = seq
92                    .next_element()?
93                    .ok_or_else(|| de::Error::invalid_length(7, &self))?;
94                let volume = seq
95                    .next_element()?
96                    .ok_or_else(|| de::Error::invalid_length(8, &self))?;
97                let high = seq
98                    .next_element()?
99                    .ok_or_else(|| de::Error::invalid_length(9, &self))?;
100                let low = seq
101                    .next_element()?
102                    .ok_or_else(|| de::Error::invalid_length(10, &self))?;
103
104                let spot_ticker = Ticker {
105                    symbol,
106                    bid,
107                    bid_size,
108                    ask,
109                    ask_size,
110                    daily_change,
111                    daily_change_relative,
112                    last_price,
113                    volume,
114                    high,
115                    low,
116                };
117                Ok(spot_ticker)
118            }
119
120            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
121            where
122                A: MapAccess<'de>,
123            {
124                let mut symbol = None;
125                let mut bid = None;
126                let mut bid_size = None;
127                let mut ask = None;
128                let mut ask_size = None;
129                let mut daily_change = None;
130                let mut daily_change_relative = None;
131                let mut last_price = None;
132                let mut volume = None;
133                let mut high = None;
134                let mut low = None;
135
136                while let Some(key) = map.next_key()? {
137                    match key {
138                        Field::Symbol => {
139                            if symbol.is_some() {
140                                return Err(de::Error::duplicate_field("symbol"));
141                            }
142                            symbol = Some(map.next_value()?);
143                        }
144                        Field::Bid => {
145                            if bid.is_some() {
146                                return Err(de::Error::duplicate_field("bid"));
147                            }
148                            bid = Some(map.next_value()?);
149                        }
150                        Field::BidSize => {
151                            if bid_size.is_some() {
152                                return Err(de::Error::duplicate_field("bid_size"));
153                            }
154                            bid_size = Some(map.next_value()?);
155                        }
156                        Field::Ask => {
157                            if ask.is_some() {
158                                return Err(de::Error::duplicate_field("ask"));
159                            }
160                            ask = Some(map.next_value()?);
161                        }
162                        Field::AskSize => {
163                            if ask_size.is_some() {
164                                return Err(de::Error::duplicate_field("ask_size"));
165                            }
166                            ask_size = Some(map.next_value()?);
167                        }
168                        Field::DailyChange => {
169                            if daily_change.is_some() {
170                                return Err(de::Error::duplicate_field("daily_change"));
171                            }
172                            daily_change = Some(map.next_value()?);
173                        }
174                        Field::DailyChangeRelative => {
175                            if daily_change_relative.is_some() {
176                                return Err(de::Error::duplicate_field("daily_change_relative"));
177                            }
178                            daily_change_relative = Some(map.next_value()?);
179                        }
180                        Field::LastPrice => {
181                            if last_price.is_some() {
182                                return Err(de::Error::duplicate_field("last_price"));
183                            }
184                            last_price = Some(map.next_value()?);
185                        }
186                        Field::Volume => {
187                            if volume.is_some() {
188                                return Err(de::Error::duplicate_field("volume"));
189                            }
190                            volume = Some(map.next_value()?);
191                        }
192                        Field::High => {
193                            if high.is_some() {
194                                return Err(de::Error::duplicate_field("high"));
195                            }
196                            high = Some(map.next_value()?);
197                        }
198                        Field::Low => {
199                            if low.is_some() {
200                                return Err(de::Error::duplicate_field("low"));
201                            }
202                            low = Some(map.next_value()?);
203                        }
204                    }
205                }
206
207                let symbol = symbol.ok_or_else(|| de::Error::missing_field("symbol"))?;
208                let bid = bid.ok_or_else(|| de::Error::missing_field("bid"))?;
209                let bid_size = bid_size.ok_or_else(|| de::Error::missing_field("bid_size"))?;
210                let ask = ask.ok_or_else(|| de::Error::missing_field("ask"))?;
211                let ask_size = ask_size.ok_or_else(|| de::Error::missing_field("ask_size"))?;
212                let daily_change =
213                    daily_change.ok_or_else(|| de::Error::missing_field("daily_change"))?;
214                let daily_change_relative = daily_change_relative
215                    .ok_or_else(|| de::Error::missing_field("daily_change_relative"))?;
216                let last_price =
217                    last_price.ok_or_else(|| de::Error::missing_field("last_price"))?;
218                let volume = volume.ok_or_else(|| de::Error::missing_field("volume"))?;
219                let high = high.ok_or_else(|| de::Error::missing_field("high"))?;
220                let low = low.ok_or_else(|| de::Error::missing_field("low"))?;
221
222                let ticker = Ticker {
223                    symbol,
224                    bid,
225                    bid_size,
226                    ask,
227                    ask_size,
228                    daily_change,
229                    daily_change_relative,
230                    last_price,
231                    volume,
232                    high,
233                    low,
234                };
235                Ok(ticker)
236            }
237        }
238
239        const FIELDS: &[&str] = &[
240            "symbol",
241            "bid",
242            "bid_size",
243            "ask",
244            "ask_size",
245            "daily_change",
246            "daily_change_relative",
247            "last_price",
248            "volume",
249            "high",
250            "low",
251        ];
252        deserializer.deserialize_struct("SpotTicker", FIELDS, SpotTickerVisitor {})
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_deserialize_spot_ticker_from_array() {
262        let json = r#"["tBTCUSD",101740,93.86022424,101750,38.06413103,2132,0.02140175,101750,663.27534767,102760,98740]"#;
263        let spot_ticker: Ticker = serde_json::from_str(json).unwrap();
264
265        let expected = Ticker {
266            symbol: "tBTCUSD".to_string(),
267            bid: 101740.0,
268            bid_size: 93.86022424,
269            ask: 101750.0,
270            ask_size: 38.06413103,
271            daily_change: 2132.0,
272            daily_change_relative: 0.02140175,
273            last_price: 101750.0,
274            volume: 663.27534767,
275            high: 102760.0,
276            low: 98740.0,
277        };
278
279        assert_eq!(spot_ticker, expected);
280    }
281
282    #[test]
283    fn test_deserialize_spot_ticker_from_array_with_invalid_length() {
284        let json = r#"["tBTCUSD",101740,93.86022424,101750,38.06413103,2132,0.02140175,101750,663.27534767]"#;
285        let spot_ticker: Result<Ticker, _> = serde_json::from_str(json);
286
287        assert_eq!(
288            spot_ticker.err().unwrap().to_string(),
289            "invalid length 9, expected tuple with length 11 at line 1 column 85"
290        );
291    }
292
293    #[test]
294    fn test_deserialize_spot_ticker_from_array_with_invalid_value() {
295        let json =
296            r#"[100000,101740,93.86022424,101750,38.06413103,2132,0.02140175,101750,663.27534767]"#;
297        let spot_ticker: Result<Ticker, _> = serde_json::from_str(json);
298
299        assert_eq!(
300            spot_ticker.err().unwrap().to_string(),
301            "invalid type: integer `100000`, expected a string at line 1 column 7"
302        );
303    }
304
305    #[test]
306    fn test_deserialize_spot_ticker_from_map() {
307        let json = r#"{"symbol":"tBTCUSD","bid":101740,"bid_size":93.86022424,"ask":101750,"ask_size":38.06413103,"daily_change":2132,"daily_change_relative":0.02140175,"last_price":101750,"volume":663.27534767,"high":102760,"low":98740.0}"#;
308        let spot_ticker: Ticker = serde_json::from_str(json).unwrap();
309
310        let expected = Ticker {
311            symbol: "tBTCUSD".to_string(),
312            bid: 101740.0,
313            bid_size: 93.86022424,
314            ask: 101750.0,
315            ask_size: 38.06413103,
316            daily_change: 2132.0,
317            daily_change_relative: 0.02140175,
318            last_price: 101750.0,
319            volume: 663.27534767,
320            high: 102760.0,
321            low: 98740.0,
322        };
323
324        assert_eq!(spot_ticker, expected);
325    }
326
327    #[test]
328    fn test_deserialize_spot_ticker_from_map_with_missing_field() {
329        let json = r#"{"bid":101740,"bid_size":93.86022424,"ask":101750,"ask_size":38.06413103,"daily_change":2132,"daily_change_relative":0.02140175,"last_price":101750,"volume":663.27534767,"high":102760,"low":98740.0}"#;
330        let spot_ticker: Result<Ticker, _> = serde_json::from_str(json);
331
332        assert_eq!(
333            spot_ticker.err().unwrap().to_string(),
334            "missing field `symbol` at line 1 column 198"
335        );
336    }
337
338    #[test]
339    fn test_deserialize_spot_ticker_from_map_with_invalid_key() {
340        let json = r#"{"abc":"tBTCUSD","bid":101740,"bid_size":93.86022424,"ask":101750,"ask_size":38.06413103,"daily_change":2132,"daily_change_relative":0.02140175,"last_price":101750,"volume":663.27534767,"high":102760,"low":98740.0}"#;
341        let spot_ticker: Result<Ticker, _> = serde_json::from_str(json);
342
343        assert_eq!(
344            spot_ticker.err().unwrap().to_string(),
345            "unknown field `abc`, expected one of `symbol`, `bid`, `bid_size`, `ask`, `ask_size`, `daily_change`, `daily_change_relative`, `last_price`, `volume`, `high`, `low` at line 1 column 6"
346        );
347    }
348
349    #[test]
350    fn test_deserialize_spot_ticker_from_map_with_invalid_value() {
351        let json = r#"{"symbol": -1000,"bid":101740,"bid_size":93.86022424,"ask":101750,"ask_size":38.06413103,"daily_change":2132,"daily_change_relative":0.02140175,"last_price":101750,"volume":663.27534767,"high":102760,"low":98740.0}"#;
352        let spot_ticker: Result<Ticker, _> = serde_json::from_str(json);
353
354        assert_eq!(
355            spot_ticker.err().unwrap().to_string(),
356            "invalid type: integer `-1000`, expected a string at line 1 column 16"
357        );
358    }
359}