nautilus_kraken/websocket/
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 WebSocket v2 API messages.
17
18use nautilus_model::data::{Data, OrderBookDeltas};
19use serde::{Deserialize, Serialize};
20use serde_json::Value;
21use ustr::Ustr;
22
23use super::enums::{KrakenWsChannel, KrakenWsMessageType, KrakenWsMethod};
24use crate::common::enums::{KrakenOrderSide, KrakenOrderType};
25
26/// Nautilus WebSocket message types for Kraken adapter.
27#[derive(Clone, Debug)]
28pub enum NautilusWsMessage {
29    Data(Vec<Data>),
30    Deltas(OrderBookDeltas),
31}
32
33// Request messages
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct KrakenWsRequest {
37    pub method: KrakenWsMethod,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub params: Option<KrakenWsParams>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub req_id: Option<u64>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct KrakenWsParams {
46    pub channel: KrakenWsChannel,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub symbol: Option<Vec<Ustr>>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub snapshot: Option<bool>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub depth: Option<u32>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub token: Option<String>,
55}
56
57// Response messages
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(tag = "method")]
61pub enum KrakenWsResponse {
62    #[serde(rename = "pong")]
63    Pong(KrakenWsPong),
64    #[serde(rename = "subscribe")]
65    Subscribe(KrakenWsSubscribeResponse),
66    #[serde(rename = "unsubscribe")]
67    Unsubscribe(KrakenWsUnsubscribeResponse),
68    #[serde(other)]
69    Other,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct KrakenWsPong {
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub req_id: Option<u64>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct KrakenWsSubscribeResponse {
80    pub success: bool,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub error: Option<String>,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub req_id: Option<u64>,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub result: Option<KrakenWsSubscriptionResult>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct KrakenWsUnsubscribeResponse {
91    pub success: bool,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub error: Option<String>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub req_id: Option<u64>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct KrakenWsSubscriptionResult {
100    pub channel: KrakenWsChannel,
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub snapshot: Option<bool>,
103}
104
105// Data messages
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct KrakenWsMessage {
109    pub channel: KrakenWsChannel,
110    #[serde(rename = "type")]
111    pub event_type: KrakenWsMessageType,
112    pub data: Vec<Value>,
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub symbol: Option<Ustr>,
115}
116
117// Ticker data
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct KrakenWsTickerData {
121    pub symbol: Ustr,
122    pub bid: f64,
123    pub bid_qty: f64,
124    pub ask: f64,
125    pub ask_qty: f64,
126    pub last: f64,
127    pub volume: f64,
128    pub vwap: f64,
129    pub low: f64,
130    pub high: f64,
131    pub change: f64,
132    pub change_pct: f64,
133}
134
135// Trade data
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct KrakenWsTradeData {
139    pub symbol: Ustr,
140    pub side: KrakenOrderSide,
141    pub price: f64,
142    pub qty: f64,
143    pub ord_type: KrakenOrderType,
144    pub trade_id: i64,
145    pub timestamp: String,
146}
147
148// Order book data
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct KrakenWsBookData {
152    pub symbol: Ustr,
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub bids: Option<Vec<KrakenWsBookLevel>>,
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub asks: Option<Vec<KrakenWsBookLevel>>,
157    pub checksum: Option<u32>,
158    pub timestamp: Option<String>,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct KrakenWsBookLevel {
163    pub price: f64,
164    pub qty: f64,
165}
166
167// OHLC data
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct KrakenWsOhlcData {
171    pub symbol: Ustr,
172    pub interval: u32,
173    pub timestamp: String,
174    pub open: f64,
175    pub high: f64,
176    pub low: f64,
177    pub close: f64,
178    pub volume: f64,
179    pub vwap: f64,
180    pub trades: i64,
181}
182
183#[cfg(test)]
184mod tests {
185    use rstest::rstest;
186
187    use super::*;
188
189    fn load_test_data(filename: &str) -> String {
190        let path = format!("test_data/{filename}");
191        std::fs::read_to_string(&path)
192            .unwrap_or_else(|e| panic!("Failed to load test data from {path}: {e}"))
193    }
194
195    #[rstest]
196    fn test_parse_subscribe_response() {
197        let data = load_test_data("ws_subscribe_response.json");
198        let response: KrakenWsResponse =
199            serde_json::from_str(&data).expect("Failed to parse subscribe response");
200
201        match response {
202            KrakenWsResponse::Subscribe(sub) => {
203                assert!(sub.success);
204                assert_eq!(sub.req_id, Some(1));
205                assert!(sub.result.is_some());
206                let result = sub.result.unwrap();
207                assert_eq!(result.channel, KrakenWsChannel::Ticker);
208            }
209            _ => panic!("Expected Subscribe response"),
210        }
211    }
212
213    #[rstest]
214    fn test_parse_pong() {
215        let data = load_test_data("ws_pong.json");
216        let response: KrakenWsResponse = serde_json::from_str(&data).expect("Failed to parse pong");
217
218        match response {
219            KrakenWsResponse::Pong(pong) => {
220                assert_eq!(pong.req_id, Some(42));
221            }
222            _ => panic!("Expected Pong response"),
223        }
224    }
225
226    #[rstest]
227    fn test_parse_ticker_snapshot() {
228        let data = load_test_data("ws_ticker_snapshot.json");
229        let message: KrakenWsMessage =
230            serde_json::from_str(&data).expect("Failed to parse ticker snapshot");
231
232        assert_eq!(message.channel, KrakenWsChannel::Ticker);
233        assert_eq!(message.event_type, KrakenWsMessageType::Snapshot);
234        assert!(!message.data.is_empty());
235
236        let ticker: KrakenWsTickerData =
237            serde_json::from_value(message.data[0].clone()).expect("Failed to parse ticker data");
238        assert_eq!(ticker.symbol.as_str(), "BTC/USD");
239        assert!(ticker.bid.is_finite() && ticker.bid > 0.0);
240        assert!(ticker.ask.is_finite() && ticker.ask > 0.0);
241        assert!(ticker.last.is_finite() && ticker.last > 0.0);
242    }
243
244    #[rstest]
245    fn test_parse_trade_update() {
246        let data = load_test_data("ws_trade_update.json");
247        let message: KrakenWsMessage =
248            serde_json::from_str(&data).expect("Failed to parse trade update");
249
250        assert_eq!(message.channel, KrakenWsChannel::Trade);
251        assert_eq!(message.event_type, KrakenWsMessageType::Update);
252        assert_eq!(message.data.len(), 2);
253
254        let trade: KrakenWsTradeData =
255            serde_json::from_value(message.data[0].clone()).expect("Failed to parse trade data");
256        assert_eq!(trade.symbol.as_str(), "BTC/USD");
257        assert!(trade.price.is_finite() && trade.price > 0.0);
258        assert!(trade.qty.is_finite() && trade.qty > 0.0);
259        assert!(trade.trade_id > 0);
260    }
261
262    #[rstest]
263    fn test_parse_book_snapshot() {
264        let data = load_test_data("ws_book_snapshot.json");
265        let message: KrakenWsMessage =
266            serde_json::from_str(&data).expect("Failed to parse book snapshot");
267
268        assert_eq!(message.channel, KrakenWsChannel::Book);
269        assert_eq!(message.event_type, KrakenWsMessageType::Snapshot);
270
271        let book: KrakenWsBookData =
272            serde_json::from_value(message.data[0].clone()).expect("Failed to parse book data");
273        assert_eq!(book.symbol.as_str(), "BTC/USD");
274        assert!(book.bids.is_some());
275        assert!(book.asks.is_some());
276        assert!(book.checksum.is_some());
277
278        let bids = book.bids.unwrap();
279        assert_eq!(bids.len(), 3);
280        assert!(bids[0].price.is_finite() && bids[0].price > 0.0);
281        assert!(bids[0].qty.is_finite() && bids[0].qty > 0.0);
282    }
283
284    #[rstest]
285    fn test_parse_book_update() {
286        let data = load_test_data("ws_book_update.json");
287        let message: KrakenWsMessage =
288            serde_json::from_str(&data).expect("Failed to parse book update");
289
290        assert_eq!(message.channel, KrakenWsChannel::Book);
291        assert_eq!(message.event_type, KrakenWsMessageType::Update);
292
293        let book: KrakenWsBookData =
294            serde_json::from_value(message.data[0].clone()).expect("Failed to parse book data");
295        assert!(book.timestamp.is_some());
296        assert!(book.checksum.is_some());
297    }
298
299    #[rstest]
300    fn test_parse_ohlc_update() {
301        let data = load_test_data("ws_ohlc_update.json");
302        let message: KrakenWsMessage =
303            serde_json::from_str(&data).expect("Failed to parse OHLC update");
304
305        assert_eq!(message.channel, KrakenWsChannel::Ohlc);
306        assert_eq!(message.event_type, KrakenWsMessageType::Update);
307
308        let ohlc: KrakenWsOhlcData =
309            serde_json::from_value(message.data[0].clone()).expect("Failed to parse OHLC data");
310        assert_eq!(ohlc.symbol.as_str(), "BTC/USD");
311        assert!(ohlc.open.is_finite() && ohlc.open > 0.0);
312        assert!(ohlc.high.is_finite() && ohlc.high > 0.0);
313        assert!(ohlc.low.is_finite() && ohlc.low > 0.0);
314        assert!(ohlc.close.is_finite() && ohlc.close > 0.0);
315        assert_eq!(ohlc.interval, 1);
316        assert!(ohlc.trades > 0);
317    }
318}