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 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#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct KrakenWsExecutionData {
198 pub exec_type: KrakenExecType,
200 pub order_id: String,
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub cl_ord_id: Option<String>,
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub symbol: Option<String>,
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub side: Option<KrakenOrderSide>,
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub order_type: Option<KrakenOrderType>,
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub order_qty: Option<f64>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub limit_price: Option<f64>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub order_status: Option<KrakenWsOrderStatus>,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub cum_qty: Option<f64>,
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub cum_cost: Option<f64>,
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub avg_price: Option<f64>,
232 #[serde(skip_serializing_if = "Option::is_none")]
234 pub time_in_force: Option<KrakenTimeInForce>,
235 #[serde(skip_serializing_if = "Option::is_none")]
237 pub post_only: Option<bool>,
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub reduce_only: Option<bool>,
241 pub timestamp: String,
243 #[serde(skip_serializing_if = "Option::is_none")]
246 pub exec_id: Option<String>,
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub last_qty: Option<f64>,
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub last_price: Option<f64>,
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub cost: Option<f64>,
256 #[serde(skip_serializing_if = "Option::is_none")]
258 pub liquidity_ind: Option<KrakenLiquidityInd>,
259 #[serde(skip_serializing_if = "Option::is_none")]
261 pub fees: Option<Vec<KrakenWsFee>>,
262 #[serde(skip_serializing_if = "Option::is_none")]
264 pub fee_usd_equiv: Option<f64>,
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub reason: Option<String>,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct KrakenWsFee {
273 pub asset: String,
275 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}