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