nautilus_kraken/websocket/futures/
messages.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Data models for Kraken Futures WebSocket v1 API messages.
17
18use nautilus_model::{
19    data::{IndexPriceUpdate, MarkPriceUpdate, OrderBookDeltas, QuoteTick, TradeTick},
20    events::{OrderAccepted, OrderCanceled, OrderExpired, OrderUpdated},
21    reports::{FillReport, OrderStatusReport},
22};
23use serde::{Deserialize, Serialize};
24use serde_json::Value;
25use strum::{AsRefStr, EnumString};
26use ustr::Ustr;
27
28use crate::common::enums::KrakenOrderSide;
29
30/// Output message types from the Futures WebSocket handler.
31#[derive(Clone, Debug)]
32pub enum KrakenFuturesWsMessage {
33    BookDeltas(OrderBookDeltas),
34    Quote(QuoteTick),
35    Trade(TradeTick),
36    MarkPrice(MarkPriceUpdate),
37    IndexPrice(IndexPriceUpdate),
38    OrderAccepted(OrderAccepted),
39    OrderCanceled(OrderCanceled),
40    OrderExpired(OrderExpired),
41    OrderUpdated(OrderUpdated),
42    OrderStatusReport(Box<OrderStatusReport>),
43    FillReport(Box<FillReport>),
44    Reconnected,
45}
46
47/// Kraken Futures WebSocket feed types.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumString, AsRefStr)]
49#[serde(rename_all = "snake_case")]
50#[strum(serialize_all = "snake_case")]
51pub enum KrakenFuturesFeed {
52    Ticker,
53    Trade,
54    TradeSnapshot,
55    Book,
56    BookSnapshot,
57    Heartbeat,
58    OpenOrders,
59    OpenOrdersSnapshot,
60    Fills,
61    FillsSnapshot,
62}
63
64/// Kraken Futures WebSocket subscription channel types.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, AsRefStr)]
66#[strum(serialize_all = "snake_case")]
67pub enum KrakenFuturesChannel {
68    Book,
69    Trades,
70    Quotes,
71    Mark,
72    Index,
73}
74
75/// Kraken Futures WebSocket event types.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78pub enum KrakenFuturesEvent {
79    Subscribe,
80    Unsubscribe,
81    Subscribed,
82    Unsubscribed,
83    Info,
84    Error,
85    Alert,
86    Challenge,
87}
88
89/// Message type classification for efficient routing.
90/// Used to classify incoming WebSocket messages without full deserialization.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum KrakenFuturesMessageType {
93    // Private feeds (execution)
94    OpenOrdersSnapshot,
95    OpenOrdersCancel,
96    OpenOrdersDelta,
97    FillsSnapshot,
98    FillsDelta,
99    // Public feeds (market data)
100    Ticker,
101    TradeSnapshot,
102    Trade,
103    BookSnapshot,
104    BookDelta,
105    // Control messages
106    Info,
107    Pong,
108    Subscribed,
109    Unsubscribed,
110    Challenge,
111    Heartbeat,
112    Unknown,
113}
114
115#[must_use]
116pub fn classify_futures_message(value: &Value) -> KrakenFuturesMessageType {
117    if let Some(event) = value.get("event").and_then(|v| v.as_str()) {
118        return match event {
119            "info" => KrakenFuturesMessageType::Info,
120            "pong" => KrakenFuturesMessageType::Pong,
121            "subscribed" => KrakenFuturesMessageType::Subscribed,
122            "unsubscribed" => KrakenFuturesMessageType::Unsubscribed,
123            "challenge" => KrakenFuturesMessageType::Challenge,
124            _ => KrakenFuturesMessageType::Unknown,
125        };
126    }
127
128    if let Some(feed) = value.get("feed").and_then(|v| v.as_str()) {
129        return match feed {
130            "heartbeat" => KrakenFuturesMessageType::Heartbeat,
131            "open_orders_snapshot" => KrakenFuturesMessageType::OpenOrdersSnapshot,
132            "open_orders" => {
133                // Cancel messages have is_cancel=true but no "order" object
134                if value.get("is_cancel").and_then(|v| v.as_bool()) == Some(true) {
135                    if value.get("order").is_some() {
136                        KrakenFuturesMessageType::OpenOrdersDelta
137                    } else {
138                        KrakenFuturesMessageType::OpenOrdersCancel
139                    }
140                } else {
141                    KrakenFuturesMessageType::OpenOrdersDelta
142                }
143            }
144            "fills_snapshot" => KrakenFuturesMessageType::FillsSnapshot,
145            "fills" => KrakenFuturesMessageType::FillsDelta,
146            "ticker" => KrakenFuturesMessageType::Ticker,
147            "trade_snapshot" => KrakenFuturesMessageType::TradeSnapshot,
148            "trade" => KrakenFuturesMessageType::Trade,
149            "book_snapshot" => KrakenFuturesMessageType::BookSnapshot,
150            "book" => KrakenFuturesMessageType::BookDelta,
151            _ => KrakenFuturesMessageType::Unknown,
152        };
153    }
154
155    KrakenFuturesMessageType::Unknown
156}
157
158/// Subscribe/unsubscribe request for Kraken Futures WebSocket.
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct KrakenFuturesRequest {
161    pub event: KrakenFuturesEvent,
162    pub feed: KrakenFuturesFeed,
163    pub product_ids: Vec<String>,
164}
165
166/// Response to a subscription request.
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct KrakenFuturesSubscriptionResponse {
169    pub event: KrakenFuturesEvent,
170    pub feed: KrakenFuturesFeed,
171    pub product_ids: Vec<String>,
172}
173
174/// Error response from Kraken Futures WebSocket.
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct KrakenFuturesErrorResponse {
177    pub event: KrakenFuturesEvent,
178    #[serde(default)]
179    pub message: Option<String>,
180}
181
182/// Info message from Kraken Futures WebSocket (sent on connection).
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct KrakenFuturesInfoMessage {
185    pub event: KrakenFuturesEvent,
186    pub version: i32,
187}
188
189/// Heartbeat message from Kraken Futures WebSocket.
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct KrakenFuturesHeartbeat {
192    pub feed: KrakenFuturesFeed,
193    pub time: i64,
194}
195
196/// Ticker data from Kraken Futures WebSocket (uses snake_case).
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct KrakenFuturesTickerData {
199    pub feed: KrakenFuturesFeed,
200    pub product_id: Ustr,
201    #[serde(default)]
202    pub time: Option<i64>,
203    #[serde(default)]
204    pub bid: Option<f64>,
205    #[serde(default)]
206    pub ask: Option<f64>,
207    #[serde(default)]
208    pub bid_size: Option<f64>,
209    #[serde(default)]
210    pub ask_size: Option<f64>,
211    #[serde(default)]
212    pub last: Option<f64>,
213    #[serde(default)]
214    pub volume: Option<f64>,
215    #[serde(default)]
216    pub volume_quote: Option<f64>,
217    #[serde(default, rename = "openInterest")]
218    pub open_interest: Option<f64>,
219    #[serde(default)]
220    pub index: Option<f64>,
221    #[serde(default, rename = "markPrice")]
222    pub mark_price: Option<f64>,
223    #[serde(default)]
224    pub change: Option<f64>,
225    #[serde(default)]
226    pub open: Option<f64>,
227    #[serde(default)]
228    pub high: Option<f64>,
229    #[serde(default)]
230    pub low: Option<f64>,
231    #[serde(default)]
232    pub funding_rate: Option<f64>,
233    #[serde(default)]
234    pub funding_rate_prediction: Option<f64>,
235    #[serde(default)]
236    pub relative_funding_rate: Option<f64>,
237    #[serde(default)]
238    pub relative_funding_rate_prediction: Option<f64>,
239    #[serde(default)]
240    pub next_funding_rate_time: Option<f64>,
241    #[serde(default)]
242    pub tag: Option<String>,
243    #[serde(default)]
244    pub pair: Option<String>,
245    #[serde(default)]
246    pub leverage: Option<String>,
247    #[serde(default)]
248    pub dtm: Option<i64>,
249    #[serde(default, rename = "maturityTime")]
250    pub maturity_time: Option<i64>,
251    #[serde(default)]
252    pub suspended: Option<bool>,
253    #[serde(default)]
254    pub post_only: Option<bool>,
255}
256
257/// Trade data from Kraken Futures WebSocket (uses snake_case).
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct KrakenFuturesTradeData {
260    pub feed: KrakenFuturesFeed,
261    pub product_id: Ustr,
262    #[serde(default)]
263    pub uid: Option<String>,
264    pub side: KrakenOrderSide,
265    #[serde(rename = "type", default)]
266    pub trade_type: Option<String>,
267    pub seq: i64,
268    pub time: i64,
269    pub qty: f64,
270    pub price: f64,
271}
272
273/// Trade snapshot from Kraken Futures WebSocket (sent on subscription).
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct KrakenFuturesTradeSnapshot {
276    pub feed: KrakenFuturesFeed,
277    pub product_id: Ustr,
278    pub trades: Vec<KrakenFuturesTradeData>,
279}
280
281/// Book snapshot from Kraken Futures WebSocket (uses snake_case).
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct KrakenFuturesBookSnapshot {
284    pub feed: KrakenFuturesFeed,
285    pub product_id: Ustr,
286    pub timestamp: i64,
287    pub seq: i64,
288    #[serde(default)]
289    pub tick_size: Option<f64>,
290    pub bids: Vec<KrakenFuturesBookLevel>,
291    pub asks: Vec<KrakenFuturesBookLevel>,
292}
293
294/// Book delta from Kraken Futures WebSocket (uses snake_case).
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct KrakenFuturesBookDelta {
297    pub feed: KrakenFuturesFeed,
298    pub product_id: Ustr,
299    pub side: KrakenOrderSide,
300    pub seq: i64,
301    pub price: f64,
302    pub qty: f64,
303    pub timestamp: i64,
304}
305
306/// Price level in order book.
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct KrakenFuturesBookLevel {
309    pub price: f64,
310    pub qty: f64,
311}
312
313/// Challenge request for WebSocket authentication.
314#[derive(Debug, Clone, Serialize)]
315pub struct KrakenFuturesChallengeRequest {
316    pub event: KrakenFuturesEvent,
317    pub api_key: String,
318}
319
320/// Challenge response from WebSocket.
321#[derive(Debug, Clone, Deserialize)]
322pub struct KrakenFuturesChallengeResponse {
323    pub event: KrakenFuturesEvent,
324    pub message: String,
325}
326
327/// Authenticated subscription request for private feeds.
328#[derive(Debug, Clone, Serialize)]
329pub struct KrakenFuturesPrivateSubscribeRequest {
330    pub event: KrakenFuturesEvent,
331    pub feed: KrakenFuturesFeed,
332    pub api_key: String,
333    pub original_challenge: String,
334    pub signed_challenge: String,
335}
336
337/// Open order from Kraken Futures WebSocket.
338#[derive(Debug, Clone, Deserialize)]
339pub struct KrakenFuturesOpenOrder {
340    pub instrument: Ustr,
341    pub time: i64,
342    pub last_update_time: i64,
343    pub qty: f64,
344    pub filled: f64,
345    /// Limit price. Optional for stop/trigger orders which only have stop_price.
346    #[serde(default)]
347    pub limit_price: Option<f64>,
348    #[serde(default)]
349    pub stop_price: Option<f64>,
350    #[serde(rename = "type")]
351    pub order_type: String,
352    pub order_id: String,
353    #[serde(default)]
354    pub cli_ord_id: Option<String>,
355    /// 0 = buy, 1 = sell
356    pub direction: i32,
357    #[serde(default)]
358    pub reduce_only: bool,
359    #[serde(default, rename = "triggerSignal")]
360    pub trigger_signal: Option<String>,
361}
362
363/// Open orders snapshot from Kraken Futures WebSocket.
364#[derive(Debug, Clone, Deserialize)]
365pub struct KrakenFuturesOpenOrdersSnapshot {
366    pub feed: KrakenFuturesFeed,
367    #[serde(default)]
368    pub account: Option<String>,
369    pub orders: Vec<KrakenFuturesOpenOrder>,
370}
371
372/// Open orders delta/update from Kraken Futures WebSocket.
373/// Used when full order details are provided (new orders, updates).
374#[derive(Debug, Clone, Deserialize)]
375pub struct KrakenFuturesOpenOrdersDelta {
376    pub feed: KrakenFuturesFeed,
377    pub order: KrakenFuturesOpenOrder,
378    pub is_cancel: bool,
379    #[serde(default)]
380    pub reason: Option<String>,
381}
382
383/// Open orders cancel notification from Kraken Futures WebSocket.
384/// Used when an order is canceled - contains only order identifiers.
385#[derive(Debug, Clone, Deserialize)]
386pub struct KrakenFuturesOpenOrdersCancel {
387    pub feed: KrakenFuturesFeed,
388    pub order_id: String,
389    pub cli_ord_id: Option<String>,
390    pub is_cancel: bool,
391    #[serde(default)]
392    pub reason: Option<String>,
393}
394
395/// Fill from Kraken Futures WebSocket.
396#[derive(Debug, Clone, Deserialize)]
397pub struct KrakenFuturesFill {
398    #[serde(alias = "product_id")]
399    pub instrument: Option<Ustr>,
400    pub time: i64,
401    pub price: f64,
402    pub qty: f64,
403    pub order_id: String,
404    #[serde(default)]
405    pub cli_ord_id: Option<String>,
406    pub fill_id: String,
407    pub fill_type: String,
408    /// true = buy, false = sell
409    pub buy: bool,
410    #[serde(default)]
411    pub fee_paid: Option<f64>,
412    #[serde(default)]
413    pub fee_currency: Option<String>,
414}
415
416/// Fills snapshot from Kraken Futures WebSocket.
417#[derive(Debug, Clone, Deserialize)]
418pub struct KrakenFuturesFillsSnapshot {
419    pub feed: KrakenFuturesFeed,
420    #[serde(default)]
421    pub account: Option<String>,
422    pub fills: Vec<KrakenFuturesFill>,
423}
424
425/// Fills delta/update from Kraken Futures WebSocket.
426/// Note: Kraken sends fills updates in array format (same as snapshot).
427#[derive(Debug, Clone, Deserialize)]
428pub struct KrakenFuturesFillsDelta {
429    pub feed: KrakenFuturesFeed,
430    #[serde(default)]
431    pub username: Option<String>,
432    pub fills: Vec<KrakenFuturesFill>,
433}
434
435#[cfg(test)]
436mod tests {
437    use rstest::rstest;
438
439    use super::*;
440
441    #[rstest]
442    fn test_deserialize_ticker_data() {
443        // Kraken Futures WebSocket uses snake_case (unlike the REST API which uses camelCase)
444        let json = r#"{
445            "feed": "ticker",
446            "product_id": "PI_XBTUSD",
447            "time": 1700000000000,
448            "bid": 90650.5,
449            "ask": 90651.0,
450            "bid_size": 10.5,
451            "ask_size": 8.2,
452            "last": 90650.8,
453            "volume": 1234567.89,
454            "index": 90648.5,
455            "markPrice": 90649.2,
456            "funding_rate": 0.0001,
457            "openInterest": 50000000.0
458        }"#;
459
460        let ticker: KrakenFuturesTickerData = serde_json::from_str(json).unwrap();
461        assert_eq!(ticker.feed, KrakenFuturesFeed::Ticker);
462        assert_eq!(ticker.product_id, Ustr::from("PI_XBTUSD"));
463        assert_eq!(ticker.bid, Some(90650.5));
464        assert_eq!(ticker.ask, Some(90651.0));
465        assert_eq!(ticker.index, Some(90648.5));
466        assert_eq!(ticker.mark_price, Some(90649.2));
467        assert_eq!(ticker.funding_rate, Some(0.0001));
468    }
469
470    #[rstest]
471    fn test_serialize_subscribe_request() {
472        let request = KrakenFuturesRequest {
473            event: KrakenFuturesEvent::Subscribe,
474            feed: KrakenFuturesFeed::Ticker,
475            product_ids: vec!["PI_XBTUSD".to_string()],
476        };
477
478        let json = serde_json::to_string(&request).unwrap();
479        assert!(json.contains("\"event\":\"subscribe\""));
480        assert!(json.contains("\"feed\":\"ticker\""));
481        assert!(json.contains("PI_XBTUSD"));
482    }
483
484    #[rstest]
485    fn test_deserialize_ticker_from_fixture() {
486        let json = include_str!("../../../test_data/ws_futures_ticker.json");
487        let ticker: KrakenFuturesTickerData = serde_json::from_str(json).unwrap();
488
489        assert_eq!(ticker.feed, KrakenFuturesFeed::Ticker);
490        assert_eq!(ticker.product_id, Ustr::from("PI_XBTUSD"));
491        assert_eq!(ticker.bid, Some(21978.5));
492        assert_eq!(ticker.ask, Some(21987.0));
493        assert_eq!(ticker.bid_size, Some(2536.0));
494        assert_eq!(ticker.ask_size, Some(13948.0));
495        assert_eq!(ticker.index, Some(21984.54));
496        assert_eq!(ticker.mark_price, Some(21979.68641534714));
497        assert!(ticker.funding_rate.is_some());
498    }
499
500    #[rstest]
501    fn test_deserialize_trade_from_fixture() {
502        let json = include_str!("../../../test_data/ws_futures_trade.json");
503        let trade: KrakenFuturesTradeData = serde_json::from_str(json).unwrap();
504
505        assert_eq!(trade.feed, KrakenFuturesFeed::Trade);
506        assert_eq!(trade.product_id, Ustr::from("PI_XBTUSD"));
507        assert_eq!(trade.side, KrakenOrderSide::Sell);
508        assert_eq!(trade.qty, 15000.0);
509        assert_eq!(trade.price, 34969.5);
510        assert_eq!(trade.seq, 653355);
511    }
512
513    #[rstest]
514    fn test_deserialize_trade_snapshot_from_fixture() {
515        let json = include_str!("../../../test_data/ws_futures_trade_snapshot.json");
516        let snapshot: KrakenFuturesTradeSnapshot = serde_json::from_str(json).unwrap();
517
518        assert_eq!(snapshot.feed, KrakenFuturesFeed::TradeSnapshot);
519        assert_eq!(snapshot.product_id, Ustr::from("PI_XBTUSD"));
520        assert_eq!(snapshot.trades.len(), 2);
521        assert_eq!(snapshot.trades[0].price, 34893.0);
522        assert_eq!(snapshot.trades[1].price, 34891.0);
523    }
524
525    #[rstest]
526    fn test_deserialize_book_snapshot_from_fixture() {
527        let json = include_str!("../../../test_data/ws_futures_book_snapshot.json");
528        let snapshot: KrakenFuturesBookSnapshot = serde_json::from_str(json).unwrap();
529
530        assert_eq!(snapshot.feed, KrakenFuturesFeed::BookSnapshot);
531        assert_eq!(snapshot.product_id, Ustr::from("PI_XBTUSD"));
532        assert_eq!(snapshot.bids.len(), 2);
533        assert_eq!(snapshot.asks.len(), 2);
534        assert_eq!(snapshot.bids[0].price, 34892.5);
535        assert_eq!(snapshot.asks[0].price, 34911.5);
536    }
537
538    #[rstest]
539    fn test_deserialize_book_delta_from_fixture() {
540        let json = include_str!("../../../test_data/ws_futures_book_delta.json");
541        let delta: KrakenFuturesBookDelta = serde_json::from_str(json).unwrap();
542
543        assert_eq!(delta.feed, KrakenFuturesFeed::Book);
544        assert_eq!(delta.product_id, Ustr::from("PI_XBTUSD"));
545        assert_eq!(delta.side, KrakenOrderSide::Sell);
546        assert_eq!(delta.price, 34981.0);
547        assert_eq!(delta.qty, 0.0); // Delete action
548    }
549
550    #[rstest]
551    fn test_deserialize_open_orders_snapshot_from_fixture() {
552        let json = include_str!("../../../test_data/ws_futures_open_orders_snapshot.json");
553        let snapshot: KrakenFuturesOpenOrdersSnapshot = serde_json::from_str(json).unwrap();
554
555        assert_eq!(snapshot.feed, KrakenFuturesFeed::OpenOrdersSnapshot);
556        assert_eq!(snapshot.orders.len(), 1);
557        assert_eq!(snapshot.orders[0].instrument, Ustr::from("PI_XBTUSD"));
558        assert_eq!(snapshot.orders[0].qty, 1000.0);
559        assert_eq!(snapshot.orders[0].order_type, "stop");
560    }
561
562    #[rstest]
563    fn test_deserialize_open_orders_delta_from_fixture() {
564        let json = include_str!("../../../test_data/ws_futures_open_orders_delta.json");
565        let delta: KrakenFuturesOpenOrdersDelta = serde_json::from_str(json).unwrap();
566
567        assert_eq!(delta.feed, KrakenFuturesFeed::OpenOrders);
568        assert!(!delta.is_cancel);
569        assert_eq!(delta.order.instrument, Ustr::from("PI_XBTUSD"));
570        assert_eq!(delta.order.qty, 304.0);
571        assert_eq!(delta.order.limit_price, Some(10640.0));
572    }
573
574    #[rstest]
575    fn test_deserialize_open_orders_cancel_from_fixture() {
576        let json = include_str!("../../../test_data/ws_futures_open_orders_cancel.json");
577        let cancel: KrakenFuturesOpenOrdersCancel = serde_json::from_str(json).unwrap();
578
579        assert_eq!(cancel.feed, KrakenFuturesFeed::OpenOrders);
580        assert!(cancel.is_cancel);
581        assert_eq!(cancel.order_id, "660c6b23-8007-48c1-a7c9-4893f4572e8c");
582        assert_eq!(cancel.reason, Some("cancelled_by_user".to_string()));
583        assert!(cancel.cli_ord_id.is_none()); // Not in docs example
584    }
585
586    #[rstest]
587    fn test_deserialize_fills_snapshot_from_fixture() {
588        let json = include_str!("../../../test_data/ws_futures_fills_snapshot.json");
589        let snapshot: KrakenFuturesFillsSnapshot = serde_json::from_str(json).unwrap();
590
591        assert_eq!(snapshot.feed, KrakenFuturesFeed::FillsSnapshot);
592        assert_eq!(snapshot.fills.len(), 2);
593        assert_eq!(
594            snapshot.fills[0].instrument,
595            Some(Ustr::from("FI_XBTUSD_200925"))
596        );
597        assert!(snapshot.fills[0].buy);
598        assert_eq!(snapshot.fills[0].fill_type, "maker");
599    }
600
601    #[rstest]
602    fn test_classify_ticker_message() {
603        let json = include_str!("../../../test_data/ws_futures_ticker.json");
604        let value: Value = serde_json::from_str(json).unwrap();
605        assert_eq!(
606            classify_futures_message(&value),
607            KrakenFuturesMessageType::Ticker
608        );
609    }
610
611    #[rstest]
612    fn test_classify_trade_message() {
613        let json = include_str!("../../../test_data/ws_futures_trade.json");
614        let value: Value = serde_json::from_str(json).unwrap();
615        assert_eq!(
616            classify_futures_message(&value),
617            KrakenFuturesMessageType::Trade
618        );
619    }
620
621    #[rstest]
622    fn test_classify_trade_snapshot_message() {
623        let json = include_str!("../../../test_data/ws_futures_trade_snapshot.json");
624        let value: Value = serde_json::from_str(json).unwrap();
625        assert_eq!(
626            classify_futures_message(&value),
627            KrakenFuturesMessageType::TradeSnapshot
628        );
629    }
630
631    #[rstest]
632    fn test_classify_book_snapshot_message() {
633        let json = include_str!("../../../test_data/ws_futures_book_snapshot.json");
634        let value: Value = serde_json::from_str(json).unwrap();
635        assert_eq!(
636            classify_futures_message(&value),
637            KrakenFuturesMessageType::BookSnapshot
638        );
639    }
640
641    #[rstest]
642    fn test_classify_book_delta_message() {
643        let json = include_str!("../../../test_data/ws_futures_book_delta.json");
644        let value: Value = serde_json::from_str(json).unwrap();
645        assert_eq!(
646            classify_futures_message(&value),
647            KrakenFuturesMessageType::BookDelta
648        );
649    }
650
651    #[rstest]
652    fn test_classify_open_orders_delta_message() {
653        let json = include_str!("../../../test_data/ws_futures_open_orders_delta.json");
654        let value: Value = serde_json::from_str(json).unwrap();
655        assert_eq!(
656            classify_futures_message(&value),
657            KrakenFuturesMessageType::OpenOrdersDelta
658        );
659    }
660
661    #[rstest]
662    fn test_classify_open_orders_cancel_message() {
663        let json = include_str!("../../../test_data/ws_futures_open_orders_cancel.json");
664        let value: Value = serde_json::from_str(json).unwrap();
665        assert_eq!(
666            classify_futures_message(&value),
667            KrakenFuturesMessageType::OpenOrdersCancel
668        );
669    }
670
671    #[rstest]
672    fn test_classify_heartbeat_message() {
673        let json = r#"{"feed":"heartbeat","time":1700000000000}"#;
674        let value: Value = serde_json::from_str(json).unwrap();
675        assert_eq!(
676            classify_futures_message(&value),
677            KrakenFuturesMessageType::Heartbeat
678        );
679    }
680
681    #[rstest]
682    fn test_classify_info_event() {
683        let json = r#"{"event":"info","version":1}"#;
684        let value: Value = serde_json::from_str(json).unwrap();
685        assert_eq!(
686            classify_futures_message(&value),
687            KrakenFuturesMessageType::Info
688        );
689    }
690
691    #[rstest]
692    fn test_classify_subscribed_event() {
693        let json = r#"{"event":"subscribed","feed":"ticker","product_ids":["PI_XBTUSD"]}"#;
694        let value: Value = serde_json::from_str(json).unwrap();
695        assert_eq!(
696            classify_futures_message(&value),
697            KrakenFuturesMessageType::Subscribed
698        );
699    }
700}