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