Skip to main content

nautilus_kraken/websocket/spot_v2/
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 WebSocket v2 API messages.
17
18use chrono::{DateTime, Utc};
19use serde::{Deserialize, Serialize};
20use serde_json::Value;
21use ustr::Ustr;
22
23use super::enums::{
24    KrakenExecType, KrakenLiquidityInd, KrakenWsChannel, KrakenWsMessageType, KrakenWsMethod,
25    KrakenWsOrderStatus,
26};
27use crate::common::enums::{KrakenOrderSide, KrakenOrderType, KrakenTimeInForce};
28
29/// Output message types from the Kraken Spot v2 WebSocket handler.
30#[derive(Clone, Debug)]
31pub enum KrakenSpotWsMessage {
32    Ticker(Vec<KrakenWsTickerData>),
33    Trade(Vec<KrakenWsTradeData>),
34    Book {
35        data: Vec<KrakenWsBookData>,
36        is_snapshot: bool,
37    },
38    Ohlc(Vec<KrakenWsOhlcData>),
39    Execution(Vec<KrakenWsExecutionData>),
40    Reconnected,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct KrakenWsRequest {
45    pub method: KrakenWsMethod,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub params: Option<KrakenWsParams>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub req_id: Option<u64>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct KrakenWsParams {
54    pub channel: KrakenWsChannel,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub symbol: Option<Vec<Ustr>>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub snapshot: Option<bool>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub depth: Option<u32>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub interval: Option<u32>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub event_trigger: Option<String>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub token: Option<String>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub snap_orders: Option<bool>,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub snap_trades: Option<bool>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(tag = "method")]
75pub enum KrakenWsResponse {
76    #[serde(rename = "pong")]
77    Pong(KrakenWsPong),
78    #[serde(rename = "subscribe")]
79    Subscribe(KrakenWsSubscribeResponse),
80    #[serde(rename = "unsubscribe")]
81    Unsubscribe(KrakenWsUnsubscribeResponse),
82    #[serde(other)]
83    Other,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct KrakenWsPong {
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub req_id: Option<u64>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct KrakenWsSubscribeResponse {
94    pub success: bool,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub error: Option<String>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub req_id: Option<u64>,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub result: Option<KrakenWsSubscriptionResult>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct KrakenWsUnsubscribeResponse {
105    pub success: bool,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub error: Option<String>,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub req_id: Option<u64>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct KrakenWsSubscriptionResult {
114    pub channel: KrakenWsChannel,
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub snapshot: Option<bool>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct KrakenWsMessage {
121    pub channel: KrakenWsChannel,
122    #[serde(rename = "type")]
123    pub event_type: KrakenWsMessageType,
124    pub data: Vec<Value>,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub symbol: Option<Ustr>,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub timestamp: Option<DateTime<Utc>>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct KrakenWsTickerData {
133    pub symbol: Ustr,
134    pub bid: f64,
135    pub bid_qty: f64,
136    pub ask: f64,
137    pub ask_qty: f64,
138    pub last: f64,
139    pub volume: f64,
140    pub vwap: f64,
141    pub low: f64,
142    pub high: f64,
143    pub change: f64,
144    pub change_pct: f64,
145    pub timestamp: DateTime<Utc>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct KrakenWsTradeData {
150    pub symbol: Ustr,
151    pub side: KrakenOrderSide,
152    pub price: f64,
153    pub qty: f64,
154    pub ord_type: KrakenOrderType,
155    pub trade_id: i64,
156    pub timestamp: DateTime<Utc>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct KrakenWsBookData {
161    pub symbol: Ustr,
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub bids: Option<Vec<KrakenWsBookLevel>>,
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub asks: Option<Vec<KrakenWsBookLevel>>,
166    pub checksum: Option<u32>,
167    pub timestamp: DateTime<Utc>,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct KrakenWsBookLevel {
172    pub price: f64,
173    pub qty: f64,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct KrakenWsOhlcData {
178    pub symbol: Ustr,
179    pub interval: u32,
180    pub interval_begin: DateTime<Utc>,
181    pub open: f64,
182    pub high: f64,
183    pub low: f64,
184    pub close: f64,
185    pub volume: f64,
186    pub vwap: f64,
187    pub trades: i64,
188}
189
190/// Execution message from the Kraken executions channel.
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct KrakenWsExecutionData {
193    /// Execution type.
194    pub exec_type: KrakenExecType,
195    /// Kraken order ID.
196    pub order_id: String,
197    /// Client order ID (if provided when order was submitted).
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub cl_ord_id: Option<String>,
200    /// Trading pair symbol.
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub symbol: Option<String>,
203    /// Order side.
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub side: Option<KrakenOrderSide>,
206    /// Order type.
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub order_type: Option<KrakenOrderType>,
209    /// Order quantity.
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub order_qty: Option<f64>,
212    /// Limit price.
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub limit_price: Option<f64>,
215    /// Order status.
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub order_status: Option<KrakenWsOrderStatus>,
218    /// Cumulative filled quantity.
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub cum_qty: Option<f64>,
221    /// Cumulative cost.
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub cum_cost: Option<f64>,
224    /// Average fill price.
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub avg_price: Option<f64>,
227    /// Time in force.
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub time_in_force: Option<KrakenTimeInForce>,
230    /// Post only flag.
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub post_only: Option<bool>,
233    /// Reduce only flag.
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub reduce_only: Option<bool>,
236    /// Event timestamp.
237    pub timestamp: DateTime<Utc>,
238    // Trade-specific fields (present when exec_type is Trade)
239    /// Execution/trade ID.
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub exec_id: Option<String>,
242    /// Last fill quantity.
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub last_qty: Option<f64>,
245    /// Last fill price.
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub last_price: Option<f64>,
248    /// Trade cost.
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub cost: Option<f64>,
251    /// Liquidity indicator.
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub liquidity_ind: Option<KrakenLiquidityInd>,
254    /// Fees array.
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub fees: Option<Vec<KrakenWsFee>>,
257    /// Fee in USD equivalent.
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub fee_usd_equiv: Option<f64>,
260    /// Cancel reason (when exec_type is Canceled/Expired).
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub reason: Option<String>,
263}
264
265/// Fee information from execution messages.
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct KrakenWsFee {
268    /// Fee asset.
269    pub asset: String,
270    /// Fee quantity.
271    pub qty: f64,
272}
273
274#[cfg(test)]
275mod tests {
276    use rstest::rstest;
277
278    use super::*;
279
280    fn load_test_data(filename: &str) -> String {
281        let path = format!("test_data/{filename}");
282        std::fs::read_to_string(&path)
283            .unwrap_or_else(|e| panic!("Failed to load test data from {path}: {e}"))
284    }
285
286    #[rstest]
287    fn test_parse_subscribe_response() {
288        let data = load_test_data("ws_subscribe_response.json");
289        let response: KrakenWsResponse =
290            serde_json::from_str(&data).expect("Failed to parse subscribe response");
291
292        match response {
293            KrakenWsResponse::Subscribe(sub) => {
294                assert!(sub.success);
295                assert_eq!(sub.req_id, Some(1));
296                assert!(sub.result.is_some());
297                let result = sub.result.unwrap();
298                assert_eq!(result.channel, KrakenWsChannel::Ticker);
299            }
300            _ => panic!("Expected Subscribe response"),
301        }
302    }
303
304    #[rstest]
305    fn test_parse_pong() {
306        let data = load_test_data("ws_pong.json");
307        let response: KrakenWsResponse = serde_json::from_str(&data).expect("Failed to parse pong");
308
309        match response {
310            KrakenWsResponse::Pong(pong) => {
311                assert_eq!(pong.req_id, Some(42));
312            }
313            _ => panic!("Expected Pong response"),
314        }
315    }
316
317    #[rstest]
318    fn test_parse_ticker_snapshot() {
319        let data = load_test_data("ws_ticker_snapshot.json");
320        let message: KrakenWsMessage =
321            serde_json::from_str(&data).expect("Failed to parse ticker snapshot");
322
323        assert_eq!(message.channel, KrakenWsChannel::Ticker);
324        assert_eq!(message.event_type, KrakenWsMessageType::Snapshot);
325        assert!(!message.data.is_empty());
326
327        let ticker: KrakenWsTickerData =
328            serde_json::from_value(message.data[0].clone()).expect("Failed to parse ticker data");
329        assert_eq!(ticker.symbol.as_str(), "BTC/USD");
330        assert!(ticker.bid.is_finite() && ticker.bid > 0.0);
331        assert!(ticker.ask.is_finite() && ticker.ask > 0.0);
332        assert!(ticker.last.is_finite() && ticker.last > 0.0);
333        assert_eq!(
334            ticker.timestamp.timestamp_nanos_opt().unwrap(),
335            1_671_960_659_123_456_000
336        );
337    }
338
339    #[rstest]
340    fn test_parse_trade_update() {
341        let data = load_test_data("ws_trade_update.json");
342        let message: KrakenWsMessage =
343            serde_json::from_str(&data).expect("Failed to parse trade update");
344
345        assert_eq!(message.channel, KrakenWsChannel::Trade);
346        assert_eq!(message.event_type, KrakenWsMessageType::Update);
347        assert_eq!(message.data.len(), 2);
348
349        let trade: KrakenWsTradeData =
350            serde_json::from_value(message.data[0].clone()).expect("Failed to parse trade data");
351        assert_eq!(trade.symbol.as_str(), "BTC/USD");
352        assert!(trade.price.is_finite() && trade.price > 0.0);
353        assert!(trade.qty.is_finite() && trade.qty > 0.0);
354        assert!(trade.trade_id > 0);
355    }
356
357    #[rstest]
358    fn test_parse_book_snapshot() {
359        let data = load_test_data("ws_book_snapshot.json");
360        let message: KrakenWsMessage =
361            serde_json::from_str(&data).expect("Failed to parse book snapshot");
362
363        assert_eq!(message.channel, KrakenWsChannel::Book);
364        assert_eq!(message.event_type, KrakenWsMessageType::Snapshot);
365
366        let book: KrakenWsBookData =
367            serde_json::from_value(message.data[0].clone()).expect("Failed to parse book data");
368        assert_eq!(book.symbol.as_str(), "BTC/USD");
369        assert!(book.bids.is_some());
370        assert!(book.asks.is_some());
371        assert!(book.checksum.is_some());
372        assert_eq!(
373            book.timestamp.timestamp_nanos_opt().unwrap(),
374            1_696_613_755_440_295_000
375        );
376
377        let bids = book.bids.unwrap();
378        assert_eq!(bids.len(), 3);
379        assert!(bids[0].price.is_finite() && bids[0].price > 0.0);
380        assert!(bids[0].qty.is_finite() && bids[0].qty > 0.0);
381    }
382
383    #[rstest]
384    fn test_parse_book_update() {
385        let data = load_test_data("ws_book_update.json");
386        let message: KrakenWsMessage =
387            serde_json::from_str(&data).expect("Failed to parse book update");
388
389        assert_eq!(message.channel, KrakenWsChannel::Book);
390        assert_eq!(message.event_type, KrakenWsMessageType::Update);
391
392        let book: KrakenWsBookData =
393            serde_json::from_value(message.data[0].clone()).expect("Failed to parse book data");
394        assert_eq!(
395            book.timestamp.timestamp_nanos_opt().unwrap(),
396            1_696_613_755_440_295_000
397        );
398        assert!(book.checksum.is_some());
399    }
400
401    #[rstest]
402    fn test_parse_ohlc_update() {
403        let data = load_test_data("ws_ohlc_update.json");
404        let message: KrakenWsMessage =
405            serde_json::from_str(&data).expect("Failed to parse OHLC update");
406
407        assert_eq!(message.channel, KrakenWsChannel::Ohlc);
408        assert_eq!(message.event_type, KrakenWsMessageType::Update);
409
410        let ohlc: KrakenWsOhlcData =
411            serde_json::from_value(message.data[0].clone()).expect("Failed to parse OHLC data");
412        assert_eq!(ohlc.symbol.as_str(), "BTC/USD");
413        assert!(ohlc.open.is_finite() && ohlc.open > 0.0);
414        assert!(ohlc.high.is_finite() && ohlc.high > 0.0);
415        assert!(ohlc.low.is_finite() && ohlc.low > 0.0);
416        assert!(ohlc.close.is_finite() && ohlc.close > 0.0);
417        assert_eq!(ohlc.interval, 1);
418        assert!(ohlc.trades > 0);
419    }
420}