Skip to main content

nautilus_kraken/websocket/futures/
messages.rs

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