1use 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct KrakenWsExecutionData {
196 pub exec_type: KrakenExecType,
198 pub order_id: String,
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub cl_ord_id: Option<String>,
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub symbol: Option<String>,
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub side: Option<KrakenOrderSide>,
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub order_type: Option<KrakenOrderType>,
212 #[serde(skip_serializing_if = "Option::is_none")]
214 pub order_qty: Option<f64>,
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub limit_price: Option<f64>,
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub order_status: Option<KrakenWsOrderStatus>,
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub cum_qty: Option<f64>,
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub cum_cost: Option<f64>,
227 #[serde(skip_serializing_if = "Option::is_none")]
229 pub avg_price: Option<f64>,
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub time_in_force: Option<KrakenTimeInForce>,
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub post_only: Option<bool>,
236 #[serde(skip_serializing_if = "Option::is_none")]
238 pub reduce_only: Option<bool>,
239 pub timestamp: String,
241 #[serde(skip_serializing_if = "Option::is_none")]
244 pub exec_id: Option<String>,
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub last_qty: Option<f64>,
248 #[serde(skip_serializing_if = "Option::is_none")]
250 pub last_price: Option<f64>,
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub cost: Option<f64>,
254 #[serde(skip_serializing_if = "Option::is_none")]
256 pub liquidity_ind: Option<KrakenLiquidityInd>,
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub fees: Option<Vec<KrakenWsFee>>,
260 #[serde(skip_serializing_if = "Option::is_none")]
262 pub fee_usd_equiv: Option<f64>,
263 #[serde(skip_serializing_if = "Option::is_none")]
265 pub reason: Option<String>,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct KrakenWsFee {
271 pub asset: String,
273 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}