rustrade_data/exchange/kraken/
message.rs1use super::channel::KrakenChannel;
2use crate::{Identifier, exchange::ExchangeSub};
3use rustrade_integration::subscription::SubscriptionId;
4use serde::{
5 Deserialize, Serialize,
6 de::{self, DeserializeOwned, IgnoredAny, MapAccess, Visitor},
7};
8use smol_str::SmolStr;
9use std::marker::PhantomData;
10
11#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize)]
107#[serde(untagged)]
108pub enum KrakenMessage<T> {
109 Data(T),
110 Event(KrakenEvent),
111}
112
113impl<T> Identifier<Option<SubscriptionId>> for KrakenMessage<T>
114where
115 T: Identifier<Option<SubscriptionId>>,
116{
117 fn id(&self) -> Option<SubscriptionId> {
118 match self {
119 Self::Data(data) => data.id(),
120 Self::Event(_) => None,
121 }
122 }
123}
124
125impl<'de, T> Deserialize<'de> for KrakenMessage<KrakenData<T>>
126where
127 T: KrakenChannelData,
128{
129 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
130 where
131 D: serde::Deserializer<'de>,
132 {
133 deserializer.deserialize_map(KrakenMessageVisitor(PhantomData))
134 }
135}
136
137struct KrakenMessageVisitor<T>(PhantomData<T>);
140
141impl<'de, T> Visitor<'de> for KrakenMessageVisitor<T>
142where
143 T: KrakenChannelData,
144{
145 type Value = KrakenMessage<KrakenData<T>>;
146
147 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 write!(formatter, "a Kraken WebSocket v2 message")
149 }
150
151 fn visit_map<Map>(self, mut map: Map) -> Result<Self::Value, Map::Error>
152 where
153 Map: MapAccess<'de>,
154 {
155 let mut channel: Option<SmolStr> = None;
156 let mut data: Option<Vec<T>> = None;
157 let mut data_unread: Option<serde_json::Value> = None;
160 let mut method: Option<SmolStr> = None;
161 let mut error: Option<String> = None;
162
163 while let Some(key) = map.next_key::<SmolStr>()? {
164 match key.as_str() {
165 "channel" => channel = Some(map.next_value()?),
166 "data" if channel.as_deref() == Some(T::CHANNEL.as_ref()) => {
167 data = Some(map.next_value()?)
168 }
169 "data" if channel.is_none() => data_unread = Some(map.next_value()?),
170 "method" => method = Some(map.next_value()?),
171 "error" => error = Some(map.next_value()?),
172 _ => {
173 map.next_value::<IgnoredAny>()?;
174 }
175 }
176 }
177
178 match (channel, method) {
179 (Some(channel), _) if channel == T::CHANNEL.as_ref() => {
180 let data = match (data, data_unread) {
181 (Some(data), _) => data,
182 (None, Some(unread)) => {
183 Vec::<T>::deserialize(unread).map_err(de::Error::custom)?
184 }
185 (None, None) => return Err(de::Error::missing_field("data")),
186 };
187 KrakenData::new(data)
188 .map(KrakenMessage::Data)
189 .map_err(de::Error::custom)
190 }
191 (Some(channel), _) => match channel.as_str() {
192 "heartbeat" => Ok(KrakenMessage::Event(KrakenEvent::Heartbeat)),
193 "status" => Ok(KrakenMessage::Event(KrakenEvent::Status)),
194 other => Err(de::Error::custom(format_args!(
195 "unexpected Kraken channel {other} on a {} stream",
196 T::CHANNEL.as_ref()
197 ))),
198 },
199 (None, Some(method)) => match error {
200 None => Ok(KrakenMessage::Event(KrakenEvent::Response { method })),
201 Some(error) => Err(de::Error::custom(format_args!(
202 "Kraken refused a request ({method}): {error}"
203 ))),
204 },
205 (None, None) => Err(de::Error::custom(
206 "Kraken message names neither a channel nor a method",
207 )),
208 }
209 }
210}
211
212pub trait KrakenChannelData: DeserializeOwned {
214 const CHANNEL: KrakenChannel;
216
217 fn symbol(&self) -> &str;
219}
220
221#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize)]
229pub struct KrakenData<T> {
230 pub subscription_id: Option<SubscriptionId>,
233 pub data: Vec<T>,
234}
235
236impl<T> KrakenData<T>
237where
238 T: KrakenChannelData,
239{
240 fn new(data: Vec<T>) -> Result<Self, String> {
242 let subscription_id = match data.split_first() {
243 None => None,
244 Some((first, rest)) => {
245 if let Some(other) = rest.iter().find(|item| item.symbol() != first.symbol()) {
246 return Err(format!(
247 "Kraken {} message mixes symbols {} and {}",
248 T::CHANNEL.as_ref(),
249 first.symbol(),
250 other.symbol()
251 ));
252 }
253 Some(ExchangeSub::from((T::CHANNEL, first.symbol())).id())
254 }
255 };
256
257 Ok(Self {
258 subscription_id,
259 data,
260 })
261 }
262}
263
264impl<T> Identifier<Option<SubscriptionId>> for KrakenData<T> {
265 fn id(&self) -> Option<SubscriptionId> {
266 self.subscription_id.clone()
267 }
268}
269
270#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize)]
280pub enum KrakenEvent {
281 Heartbeat,
283 Status,
285 Response { method: SmolStr },
287}
288
289#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
296pub struct KrakenError {
297 #[serde(rename = "error")]
298 pub message: String,
299 #[serde(default)]
300 pub symbol: Option<String>,
301}
302
303#[cfg(test)]
304#[allow(clippy::unwrap_used)] mod tests {
306 use super::*;
307 use crate::exchange::kraken::trade::KrakenTrades;
308
309 const TIMES: &str =
310 r#""time_in":"2018-11-12T21:14:59.545897Z","time_out":"2018-11-12T21:14:59.545901Z""#;
311
312 fn trade(symbol: &str) -> String {
313 format!(
314 r#"{{"symbol":"{symbol}","side":"buy","price":1.5,"qty":2,"ord_type":"limit","trade_id":1,"timestamp":"2018-11-12T21:14:59.545897Z"}}"#
315 )
316 }
317
318 fn parse_error(message: &str) -> String {
319 serde_json::from_str::<KrakenTrades>(message)
320 .unwrap_err()
321 .to_string()
322 }
323
324 #[test]
325 fn heartbeat_and_status_are_events() {
326 let heartbeat: KrakenTrades = serde_json::from_str(r#"{"channel":"heartbeat"}"#).unwrap();
327 assert_eq!(heartbeat, KrakenMessage::Event(KrakenEvent::Heartbeat));
328
329 let status: KrakenTrades = serde_json::from_str(
330 r#"{"channel":"status","type":"update","data":[{"api_version":"v2","connection_id":1,"system":"online","version":"2.0.0"}]}"#,
331 )
332 .unwrap();
333 assert_eq!(status, KrakenMessage::Event(KrakenEvent::Status));
334 }
335
336 #[test]
337 fn a_successful_response_is_an_event() {
338 let pong: KrakenTrades =
339 serde_json::from_str(&format!(r#"{{"method":"pong","req_id":1,{TIMES}}}"#)).unwrap();
340 assert_eq!(
341 pong,
342 KrakenMessage::Event(KrakenEvent::Response {
343 method: "pong".into()
344 })
345 );
346 }
347
348 #[test]
349 fn a_refused_request_is_an_error_quoting_kraken() {
350 let error = parse_error(&format!(
351 r#"{{"error":"Malformed request","method":"error","success":false,{TIMES}}}"#
352 ));
353 assert!(
354 error.contains("Kraken refused a request (error): Malformed request"),
355 "{error}"
356 );
357 }
358
359 #[test]
360 fn a_message_mixing_symbols_is_refused_naming_them() {
361 let error = parse_error(&format!(
362 r#"{{"channel":"trade","type":"update","data":[{},{}]}}"#,
363 trade("BTC/USD"),
364 trade("ETH/USD")
365 ));
366 assert!(
367 error.contains("Kraken trade message mixes symbols BTC/USD and ETH/USD"),
368 "{error}"
369 );
370 }
371
372 #[test]
373 fn another_data_channel_is_refused_naming_it() {
374 let error = parse_error(
375 r#"{"channel":"ticker","type":"update","data":[{"symbol":"BTC/USD","bid":1.0,"bid_qty":1.0,"ask":2.0,"ask_qty":1.0,"timestamp":"2018-11-12T21:14:59.545897Z"}]}"#,
376 );
377 assert!(
378 error.contains("unexpected Kraken channel ticker on a trade stream"),
379 "{error}"
380 );
381 }
382
383 #[test]
384 fn an_item_that_does_not_parse_keeps_its_reason() {
385 let error = parse_error(
386 r#"{"channel":"trade","type":"update","data":[{"symbol":"BTC/USD","side":"buy","price":1.5,"qty":2,"ord_type":"limit","timestamp":"2018-11-12T21:14:59.545897Z"}]}"#,
387 );
388 assert!(error.contains("missing field `trade_id`"), "{error}");
389 }
390
391 #[test]
392 fn a_malformed_envelope_is_refused_naming_why() {
393 let error = parse_error("{}");
394 assert!(
395 error.contains("Kraken message names neither a channel nor a method"),
396 "{error}"
397 );
398 let error = parse_error(r#"{"channel":"trade","type":"update"}"#);
399 assert!(error.contains("missing field `data`"), "{error}");
400 }
401
402 #[test]
403 fn data_before_another_channel_is_not_read_as_items() {
404 let status: KrakenTrades =
405 serde_json::from_str(r#"{"data":[{"system":"online"}],"channel":"status"}"#).unwrap();
406 assert_eq!(status, KrakenMessage::Event(KrakenEvent::Status));
407 }
408
409 #[test]
410 fn data_before_its_channel_still_parses() {
411 let trades: KrakenTrades = serde_json::from_str(&format!(
412 r#"{{"data":[{}],"type":"update","channel":"trade"}}"#,
413 trade("BTC/USD")
414 ))
415 .unwrap();
416
417 let KrakenMessage::Data(trades) = trades else {
418 panic!("expected data, got {trades:?}");
419 };
420 assert_eq!(
421 trades.subscription_id,
422 Some(SubscriptionId::from("trade|BTC/USD"))
423 );
424 assert_eq!(trades.data.len(), 1);
425 assert_eq!(trades.data[0].symbol, "BTC/USD");
426 }
427}