nautilus_kraken/websocket/
messages.rs1use nautilus_model::data::{Data, OrderBookDeltas};
19use serde::{Deserialize, Serialize};
20use serde_json::Value;
21use ustr::Ustr;
22
23use super::enums::{KrakenWsChannel, KrakenWsMessageType, KrakenWsMethod};
24use crate::common::enums::{KrakenOrderSide, KrakenOrderType};
25
26#[derive(Clone, Debug)]
28pub enum NautilusWsMessage {
29 Data(Vec<Data>),
30 Deltas(OrderBookDeltas),
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct KrakenWsRequest {
37 pub method: KrakenWsMethod,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub params: Option<KrakenWsParams>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub req_id: Option<u64>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct KrakenWsParams {
46 pub channel: KrakenWsChannel,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub symbol: Option<Vec<Ustr>>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub snapshot: Option<bool>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub depth: Option<u32>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub token: Option<String>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(tag = "method")]
61pub enum KrakenWsResponse {
62 #[serde(rename = "pong")]
63 Pong(KrakenWsPong),
64 #[serde(rename = "subscribe")]
65 Subscribe(KrakenWsSubscribeResponse),
66 #[serde(rename = "unsubscribe")]
67 Unsubscribe(KrakenWsUnsubscribeResponse),
68 #[serde(other)]
69 Other,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct KrakenWsPong {
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub req_id: Option<u64>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct KrakenWsSubscribeResponse {
80 pub success: bool,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub error: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub req_id: Option<u64>,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub result: Option<KrakenWsSubscriptionResult>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct KrakenWsUnsubscribeResponse {
91 pub success: bool,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub error: Option<String>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub req_id: Option<u64>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct KrakenWsSubscriptionResult {
100 pub channel: KrakenWsChannel,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub snapshot: Option<bool>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct KrakenWsMessage {
109 pub channel: KrakenWsChannel,
110 #[serde(rename = "type")]
111 pub event_type: KrakenWsMessageType,
112 pub data: Vec<Value>,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub symbol: Option<Ustr>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct KrakenWsTickerData {
121 pub symbol: Ustr,
122 pub bid: f64,
123 pub bid_qty: f64,
124 pub ask: f64,
125 pub ask_qty: f64,
126 pub last: f64,
127 pub volume: f64,
128 pub vwap: f64,
129 pub low: f64,
130 pub high: f64,
131 pub change: f64,
132 pub change_pct: f64,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct KrakenWsTradeData {
139 pub symbol: Ustr,
140 pub side: KrakenOrderSide,
141 pub price: f64,
142 pub qty: f64,
143 pub ord_type: KrakenOrderType,
144 pub trade_id: i64,
145 pub timestamp: String,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct KrakenWsBookData {
152 pub symbol: Ustr,
153 #[serde(skip_serializing_if = "Option::is_none")]
154 pub bids: Option<Vec<KrakenWsBookLevel>>,
155 #[serde(skip_serializing_if = "Option::is_none")]
156 pub asks: Option<Vec<KrakenWsBookLevel>>,
157 pub checksum: Option<u32>,
158 pub timestamp: Option<String>,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct KrakenWsBookLevel {
163 pub price: f64,
164 pub qty: f64,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct KrakenWsOhlcData {
171 pub symbol: Ustr,
172 pub interval: u32,
173 pub timestamp: String,
174 pub open: f64,
175 pub high: f64,
176 pub low: f64,
177 pub close: f64,
178 pub volume: f64,
179 pub vwap: f64,
180 pub trades: i64,
181}
182
183#[cfg(test)]
184mod tests {
185 use rstest::rstest;
186
187 use super::*;
188
189 fn load_test_data(filename: &str) -> String {
190 let path = format!("test_data/{filename}");
191 std::fs::read_to_string(&path)
192 .unwrap_or_else(|e| panic!("Failed to load test data from {path}: {e}"))
193 }
194
195 #[rstest]
196 fn test_parse_subscribe_response() {
197 let data = load_test_data("ws_subscribe_response.json");
198 let response: KrakenWsResponse =
199 serde_json::from_str(&data).expect("Failed to parse subscribe response");
200
201 match response {
202 KrakenWsResponse::Subscribe(sub) => {
203 assert!(sub.success);
204 assert_eq!(sub.req_id, Some(1));
205 assert!(sub.result.is_some());
206 let result = sub.result.unwrap();
207 assert_eq!(result.channel, KrakenWsChannel::Ticker);
208 }
209 _ => panic!("Expected Subscribe response"),
210 }
211 }
212
213 #[rstest]
214 fn test_parse_pong() {
215 let data = load_test_data("ws_pong.json");
216 let response: KrakenWsResponse = serde_json::from_str(&data).expect("Failed to parse pong");
217
218 match response {
219 KrakenWsResponse::Pong(pong) => {
220 assert_eq!(pong.req_id, Some(42));
221 }
222 _ => panic!("Expected Pong response"),
223 }
224 }
225
226 #[rstest]
227 fn test_parse_ticker_snapshot() {
228 let data = load_test_data("ws_ticker_snapshot.json");
229 let message: KrakenWsMessage =
230 serde_json::from_str(&data).expect("Failed to parse ticker snapshot");
231
232 assert_eq!(message.channel, KrakenWsChannel::Ticker);
233 assert_eq!(message.event_type, KrakenWsMessageType::Snapshot);
234 assert!(!message.data.is_empty());
235
236 let ticker: KrakenWsTickerData =
237 serde_json::from_value(message.data[0].clone()).expect("Failed to parse ticker data");
238 assert_eq!(ticker.symbol.as_str(), "BTC/USD");
239 assert!(ticker.bid.is_finite() && ticker.bid > 0.0);
240 assert!(ticker.ask.is_finite() && ticker.ask > 0.0);
241 assert!(ticker.last.is_finite() && ticker.last > 0.0);
242 }
243
244 #[rstest]
245 fn test_parse_trade_update() {
246 let data = load_test_data("ws_trade_update.json");
247 let message: KrakenWsMessage =
248 serde_json::from_str(&data).expect("Failed to parse trade update");
249
250 assert_eq!(message.channel, KrakenWsChannel::Trade);
251 assert_eq!(message.event_type, KrakenWsMessageType::Update);
252 assert_eq!(message.data.len(), 2);
253
254 let trade: KrakenWsTradeData =
255 serde_json::from_value(message.data[0].clone()).expect("Failed to parse trade data");
256 assert_eq!(trade.symbol.as_str(), "BTC/USD");
257 assert!(trade.price.is_finite() && trade.price > 0.0);
258 assert!(trade.qty.is_finite() && trade.qty > 0.0);
259 assert!(trade.trade_id > 0);
260 }
261
262 #[rstest]
263 fn test_parse_book_snapshot() {
264 let data = load_test_data("ws_book_snapshot.json");
265 let message: KrakenWsMessage =
266 serde_json::from_str(&data).expect("Failed to parse book snapshot");
267
268 assert_eq!(message.channel, KrakenWsChannel::Book);
269 assert_eq!(message.event_type, KrakenWsMessageType::Snapshot);
270
271 let book: KrakenWsBookData =
272 serde_json::from_value(message.data[0].clone()).expect("Failed to parse book data");
273 assert_eq!(book.symbol.as_str(), "BTC/USD");
274 assert!(book.bids.is_some());
275 assert!(book.asks.is_some());
276 assert!(book.checksum.is_some());
277
278 let bids = book.bids.unwrap();
279 assert_eq!(bids.len(), 3);
280 assert!(bids[0].price.is_finite() && bids[0].price > 0.0);
281 assert!(bids[0].qty.is_finite() && bids[0].qty > 0.0);
282 }
283
284 #[rstest]
285 fn test_parse_book_update() {
286 let data = load_test_data("ws_book_update.json");
287 let message: KrakenWsMessage =
288 serde_json::from_str(&data).expect("Failed to parse book update");
289
290 assert_eq!(message.channel, KrakenWsChannel::Book);
291 assert_eq!(message.event_type, KrakenWsMessageType::Update);
292
293 let book: KrakenWsBookData =
294 serde_json::from_value(message.data[0].clone()).expect("Failed to parse book data");
295 assert!(book.timestamp.is_some());
296 assert!(book.checksum.is_some());
297 }
298
299 #[rstest]
300 fn test_parse_ohlc_update() {
301 let data = load_test_data("ws_ohlc_update.json");
302 let message: KrakenWsMessage =
303 serde_json::from_str(&data).expect("Failed to parse OHLC update");
304
305 assert_eq!(message.channel, KrakenWsChannel::Ohlc);
306 assert_eq!(message.event_type, KrakenWsMessageType::Update);
307
308 let ohlc: KrakenWsOhlcData =
309 serde_json::from_value(message.data[0].clone()).expect("Failed to parse OHLC data");
310 assert_eq!(ohlc.symbol.as_str(), "BTC/USD");
311 assert!(ohlc.open.is_finite() && ohlc.open > 0.0);
312 assert!(ohlc.high.is_finite() && ohlc.high > 0.0);
313 assert!(ohlc.low.is_finite() && ohlc.low > 0.0);
314 assert!(ohlc.close.is_finite() && ohlc.close > 0.0);
315 assert_eq!(ohlc.interval, 1);
316 assert!(ohlc.trades > 0);
317 }
318}