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