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::{
28 common::enums::{KrakenOrderSide, KrakenOrderType, KrakenSpotTrigger, KrakenTimeInForce},
29 websocket::spot_v2::level_3::messages::{KrakenL3Snapshot, KrakenL3UpdateData},
30};
31
32#[derive(Clone, Debug)]
34pub enum KrakenSpotWsMessage {
35 Ticker(Vec<KrakenWsTickerData>),
36 Trade(Vec<KrakenWsTradeData>),
37 Book {
38 data: Vec<KrakenWsBookData>,
39 is_snapshot: bool,
40 },
41 Ohlc(Vec<KrakenWsOhlcData>),
42 Execution(Vec<KrakenWsExecutionData>),
43 OrderResponse(KrakenWsOrderResponse),
44 L3Snapshot(KrakenL3Snapshot),
45 L3Update(KrakenL3UpdateData),
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)]
60#[serde(untagged)]
61pub enum KrakenWsParams {
62 Channel(KrakenWsChannelParams),
64 AddOrder(KrakenWsAddOrderParams),
66 AmendOrder(KrakenWsAmendOrderParams),
68 CancelOrder(KrakenWsCancelOrderParams),
70 BatchAdd(KrakenWsBatchAddParams),
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct KrakenWsChannelParams {
77 pub channel: KrakenWsChannel,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub symbol: Option<Vec<Ustr>>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub snapshot: Option<bool>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub depth: Option<u32>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub interval: Option<u32>,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub event_trigger: Option<String>,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub token: Option<String>,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub snap_orders: Option<bool>,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub snap_trades: Option<bool>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct KrakenWsAddOrderParams {
108 pub order_type: KrakenOrderType,
110 pub side: KrakenOrderSide,
112 pub order_qty: f64,
114 pub symbol: String,
116 pub token: String,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub limit_price: Option<f64>,
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub time_in_force: Option<KrakenTimeInForce>,
124 #[serde(skip_serializing_if = "Option::is_none")]
128 pub expire_time: Option<String>,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub cl_ord_id: Option<String>,
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub post_only: Option<bool>,
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub reduce_only: Option<bool>,
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub trigger: Option<KrakenWsTriggerParams>,
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub leverage: Option<u16>,
144 #[serde(skip_serializing_if = "Option::is_none")]
146 pub conditional: Option<KrakenWsConditionalParams>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct KrakenWsAmendOrderParams {
152 pub token: String,
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub order_id: Option<String>,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub cl_ord_id: Option<String>,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub order_qty: Option<f64>,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub limit_price: Option<f64>,
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub trigger_price: Option<f64>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct KrakenWsCancelOrderParams {
174 pub token: String,
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub order_id: Option<Vec<String>>,
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub cl_ord_id: Option<Vec<String>>,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct KrakenWsBatchAddParams {
187 pub symbol: String,
189 pub orders: Vec<KrakenWsBatchAddOrder>,
191 pub token: String,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct KrakenWsBatchAddOrder {
198 pub order_type: KrakenOrderType,
200 pub side: KrakenOrderSide,
202 pub order_qty: f64,
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub limit_price: Option<f64>,
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub cl_ord_id: Option<String>,
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub time_in_force: Option<KrakenTimeInForce>,
213 #[serde(skip_serializing_if = "Option::is_none")]
216 pub expire_time: Option<String>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub post_only: Option<bool>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub reduce_only: Option<bool>,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub leverage: Option<u16>,
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub trigger: Option<KrakenWsTriggerParams>,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct KrakenWsTriggerParams {
234 pub reference: KrakenSpotTrigger,
236 pub price: f64,
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub price_type: Option<String>,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct KrakenWsConditionalParams {
246 pub order_type: KrakenOrderType,
248 #[serde(skip_serializing_if = "Option::is_none")]
250 pub limit_price: Option<f64>,
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub trigger_price: Option<f64>,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct KrakenWsOrderResponse {
259 pub method: KrakenWsMethod,
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub req_id: Option<u64>,
264 pub success: bool,
266 #[serde(skip_serializing_if = "Option::is_none")]
268 pub time_in: Option<String>,
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub time_out: Option<String>,
272 #[serde(skip_serializing_if = "Option::is_none")]
274 pub error: Option<String>,
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub result: Option<KrakenWsOrderResult>,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct KrakenWsOrderResult {
283 #[serde(skip_serializing_if = "Option::is_none")]
285 pub order_id: Option<String>,
286 #[serde(skip_serializing_if = "Option::is_none")]
288 pub cl_ord_id: Option<String>,
289 #[serde(skip_serializing_if = "Option::is_none")]
291 pub order_userref: Option<i64>,
292 #[serde(skip_serializing_if = "Option::is_none")]
294 pub warning: Option<Vec<String>>,
295 #[serde(skip_serializing_if = "Option::is_none")]
297 pub orders: Option<Vec<KrakenWsBatchOrderResult>>,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct KrakenWsBatchOrderResult {
303 pub success: bool,
305 #[serde(skip_serializing_if = "Option::is_none")]
307 pub order_id: Option<String>,
308 #[serde(skip_serializing_if = "Option::is_none")]
310 pub cl_ord_id: Option<String>,
311 #[serde(skip_serializing_if = "Option::is_none")]
313 pub error: Option<String>,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
317#[serde(tag = "method")]
318pub enum KrakenWsResponse {
319 #[serde(rename = "pong")]
320 Pong(KrakenWsPong),
321 #[serde(rename = "subscribe")]
322 Subscribe(KrakenWsSubscribeResponse),
323 #[serde(rename = "unsubscribe")]
324 Unsubscribe(KrakenWsUnsubscribeResponse),
325 #[serde(other)]
326 Other,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct KrakenWsPong {
331 #[serde(skip_serializing_if = "Option::is_none")]
332 pub req_id: Option<u64>,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct KrakenWsSubscribeResponse {
337 pub success: bool,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub error: Option<String>,
340 #[serde(skip_serializing_if = "Option::is_none")]
341 pub req_id: Option<u64>,
342 #[serde(skip_serializing_if = "Option::is_none")]
343 pub result: Option<KrakenWsSubscriptionResult>,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct KrakenWsUnsubscribeResponse {
348 pub success: bool,
349 #[serde(skip_serializing_if = "Option::is_none")]
350 pub error: Option<String>,
351 #[serde(skip_serializing_if = "Option::is_none")]
352 pub req_id: Option<u64>,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct KrakenWsSubscriptionResult {
357 pub channel: KrakenWsChannel,
358 #[serde(skip_serializing_if = "Option::is_none")]
359 pub snapshot: Option<bool>,
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct KrakenWsMessage {
364 pub channel: KrakenWsChannel,
365 #[serde(rename = "type")]
366 pub event_type: KrakenWsMessageType,
367 pub data: Vec<Value>,
368 #[serde(skip_serializing_if = "Option::is_none")]
369 pub symbol: Option<Ustr>,
370 #[serde(skip_serializing_if = "Option::is_none")]
371 pub timestamp: Option<DateTime<Utc>>,
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct KrakenWsTickerData {
376 pub symbol: Ustr,
377 pub bid: f64,
378 pub bid_qty: f64,
379 pub ask: f64,
380 pub ask_qty: f64,
381 pub last: f64,
382 pub volume: f64,
383 pub vwap: f64,
384 pub low: f64,
385 pub high: f64,
386 pub change: f64,
387 pub change_pct: f64,
388 pub timestamp: DateTime<Utc>,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct KrakenWsTradeData {
393 pub symbol: Ustr,
394 pub side: KrakenOrderSide,
395 pub price: f64,
396 pub qty: f64,
397 pub ord_type: KrakenOrderType,
398 pub trade_id: i64,
399 pub timestamp: DateTime<Utc>,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct KrakenWsBookData {
404 pub symbol: Ustr,
405 #[serde(skip_serializing_if = "Option::is_none")]
406 pub bids: Option<Vec<KrakenWsBookLevel>>,
407 #[serde(skip_serializing_if = "Option::is_none")]
408 pub asks: Option<Vec<KrakenWsBookLevel>>,
409 pub checksum: Option<u32>,
410 pub timestamp: DateTime<Utc>,
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct KrakenWsBookLevel {
415 pub price: f64,
416 pub qty: f64,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct KrakenWsOhlcData {
421 pub symbol: Ustr,
422 pub interval: u32,
423 pub interval_begin: DateTime<Utc>,
424 pub open: f64,
425 pub high: f64,
426 pub low: f64,
427 pub close: f64,
428 pub volume: f64,
429 pub vwap: f64,
430 pub trades: i64,
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
435pub struct KrakenWsExecutionData {
436 pub exec_type: KrakenExecType,
438 pub order_id: String,
440 #[serde(skip_serializing_if = "Option::is_none")]
442 pub cl_ord_id: Option<String>,
443 #[serde(skip_serializing_if = "Option::is_none")]
445 pub symbol: Option<String>,
446 #[serde(skip_serializing_if = "Option::is_none")]
448 pub side: Option<KrakenOrderSide>,
449 #[serde(skip_serializing_if = "Option::is_none")]
451 pub order_type: Option<KrakenOrderType>,
452 #[serde(skip_serializing_if = "Option::is_none")]
454 pub order_qty: Option<f64>,
455 #[serde(skip_serializing_if = "Option::is_none")]
457 pub limit_price: Option<f64>,
458 #[serde(skip_serializing_if = "Option::is_none")]
460 pub order_status: Option<KrakenWsOrderStatus>,
461 #[serde(skip_serializing_if = "Option::is_none")]
463 pub cum_qty: Option<f64>,
464 #[serde(skip_serializing_if = "Option::is_none")]
466 pub cum_cost: Option<f64>,
467 #[serde(skip_serializing_if = "Option::is_none")]
469 pub avg_price: Option<f64>,
470 #[serde(skip_serializing_if = "Option::is_none")]
472 pub time_in_force: Option<KrakenTimeInForce>,
473 #[serde(skip_serializing_if = "Option::is_none")]
475 pub post_only: Option<bool>,
476 #[serde(skip_serializing_if = "Option::is_none")]
478 pub reduce_only: Option<bool>,
479 pub timestamp: DateTime<Utc>,
481 #[serde(skip_serializing_if = "Option::is_none")]
483 pub exec_id: Option<String>,
484 #[serde(skip_serializing_if = "Option::is_none")]
486 pub last_qty: Option<f64>,
487 #[serde(skip_serializing_if = "Option::is_none")]
489 pub last_price: Option<f64>,
490 #[serde(skip_serializing_if = "Option::is_none")]
492 pub cost: Option<f64>,
493 #[serde(skip_serializing_if = "Option::is_none")]
495 pub liquidity_ind: Option<KrakenLiquidityInd>,
496 #[serde(skip_serializing_if = "Option::is_none")]
498 pub fees: Option<Vec<KrakenWsFee>>,
499 #[serde(skip_serializing_if = "Option::is_none")]
501 pub fee_usd_equiv: Option<f64>,
502 #[serde(skip_serializing_if = "Option::is_none")]
504 pub reason: Option<String>,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize)]
509pub struct KrakenWsFee {
510 pub asset: String,
512 pub qty: f64,
514}
515
516#[cfg(test)]
517mod tests {
518 use rstest::rstest;
519
520 use super::*;
521
522 fn load_test_data(filename: &str) -> String {
523 let path = format!("test_data/{filename}");
524 std::fs::read_to_string(&path)
525 .unwrap_or_else(|e| panic!("Failed to load test data from {path}: {e}"))
526 }
527
528 #[rstest]
529 fn test_parse_subscribe_response() {
530 let data = load_test_data("ws_subscribe_response.json");
531 let response: KrakenWsResponse =
532 serde_json::from_str(&data).expect("Failed to parse subscribe response");
533
534 match response {
535 KrakenWsResponse::Subscribe(sub) => {
536 assert!(sub.success);
537 assert_eq!(sub.req_id, Some(1));
538 assert!(sub.result.is_some());
539 let result = sub.result.unwrap();
540 assert_eq!(result.channel, KrakenWsChannel::Ticker);
541 }
542 _ => panic!("Expected Subscribe response"),
543 }
544 }
545
546 #[rstest]
547 fn test_parse_pong() {
548 let data = load_test_data("ws_pong.json");
549 let response: KrakenWsResponse = serde_json::from_str(&data).expect("Failed to parse pong");
550
551 match response {
552 KrakenWsResponse::Pong(pong) => {
553 assert_eq!(pong.req_id, Some(42));
554 }
555 _ => panic!("Expected Pong response"),
556 }
557 }
558
559 #[rstest]
560 fn test_parse_ticker_snapshot() {
561 let data = load_test_data("ws_ticker_snapshot.json");
562 let message: KrakenWsMessage =
563 serde_json::from_str(&data).expect("Failed to parse ticker snapshot");
564
565 assert_eq!(message.channel, KrakenWsChannel::Ticker);
566 assert_eq!(message.event_type, KrakenWsMessageType::Snapshot);
567 assert!(!message.data.is_empty());
568
569 let ticker: KrakenWsTickerData =
570 serde_json::from_value(message.data[0].clone()).expect("Failed to parse ticker data");
571 assert_eq!(ticker.symbol.as_str(), "BTC/USD");
572 assert!(ticker.bid.is_finite() && ticker.bid > 0.0);
573 assert!(ticker.ask.is_finite() && ticker.ask > 0.0);
574 assert!(ticker.last.is_finite() && ticker.last > 0.0);
575 assert_eq!(
576 ticker.timestamp.timestamp_nanos_opt().unwrap(),
577 1_671_960_659_123_456_000
578 );
579 }
580
581 #[rstest]
582 fn test_parse_trade_update() {
583 let data = load_test_data("ws_trade_update.json");
584 let message: KrakenWsMessage =
585 serde_json::from_str(&data).expect("Failed to parse trade update");
586
587 assert_eq!(message.channel, KrakenWsChannel::Trade);
588 assert_eq!(message.event_type, KrakenWsMessageType::Update);
589 assert_eq!(message.data.len(), 2);
590
591 let trade: KrakenWsTradeData =
592 serde_json::from_value(message.data[0].clone()).expect("Failed to parse trade data");
593 assert_eq!(trade.symbol.as_str(), "BTC/USD");
594 assert!(trade.price.is_finite() && trade.price > 0.0);
595 assert!(trade.qty.is_finite() && trade.qty > 0.0);
596 assert!(trade.trade_id > 0);
597 }
598
599 #[rstest]
600 fn test_parse_book_snapshot() {
601 let data = load_test_data("ws_book_snapshot.json");
602 let message: KrakenWsMessage =
603 serde_json::from_str(&data).expect("Failed to parse book snapshot");
604
605 assert_eq!(message.channel, KrakenWsChannel::Book);
606 assert_eq!(message.event_type, KrakenWsMessageType::Snapshot);
607
608 let book: KrakenWsBookData =
609 serde_json::from_value(message.data[0].clone()).expect("Failed to parse book data");
610 assert_eq!(book.symbol.as_str(), "BTC/USD");
611 assert!(book.bids.is_some());
612 assert!(book.asks.is_some());
613 assert!(book.checksum.is_some());
614 assert_eq!(
615 book.timestamp.timestamp_nanos_opt().unwrap(),
616 1_696_613_755_440_295_000
617 );
618
619 let bids = book.bids.unwrap();
620 assert_eq!(bids.len(), 3);
621 assert!(bids[0].price.is_finite() && bids[0].price > 0.0);
622 assert!(bids[0].qty.is_finite() && bids[0].qty > 0.0);
623 }
624
625 #[rstest]
626 fn test_parse_book_update() {
627 let data = load_test_data("ws_book_update.json");
628 let message: KrakenWsMessage =
629 serde_json::from_str(&data).expect("Failed to parse book update");
630
631 assert_eq!(message.channel, KrakenWsChannel::Book);
632 assert_eq!(message.event_type, KrakenWsMessageType::Update);
633
634 let book: KrakenWsBookData =
635 serde_json::from_value(message.data[0].clone()).expect("Failed to parse book data");
636 assert_eq!(
637 book.timestamp.timestamp_nanos_opt().unwrap(),
638 1_696_613_755_440_295_000
639 );
640 assert!(book.checksum.is_some());
641 }
642
643 #[rstest]
644 fn test_parse_ohlc_update() {
645 let data = load_test_data("ws_ohlc_update.json");
646 let message: KrakenWsMessage =
647 serde_json::from_str(&data).expect("Failed to parse OHLC update");
648
649 assert_eq!(message.channel, KrakenWsChannel::Ohlc);
650 assert_eq!(message.event_type, KrakenWsMessageType::Update);
651
652 let ohlc: KrakenWsOhlcData =
653 serde_json::from_value(message.data[0].clone()).expect("Failed to parse OHLC data");
654 assert_eq!(ohlc.symbol.as_str(), "BTC/USD");
655 assert!(ohlc.open.is_finite() && ohlc.open > 0.0);
656 assert!(ohlc.high.is_finite() && ohlc.high > 0.0);
657 assert!(ohlc.low.is_finite() && ohlc.low > 0.0);
658 assert!(ohlc.close.is_finite() && ohlc.close > 0.0);
659 assert_eq!(ohlc.interval, 1);
660 assert!(ohlc.trades > 0);
661 }
662
663 #[rstest]
664 fn test_serialize_add_order_request() {
665 let request = KrakenWsRequest {
666 method: KrakenWsMethod::AddOrder,
667 params: Some(KrakenWsParams::AddOrder(KrakenWsAddOrderParams {
668 order_type: KrakenOrderType::Limit,
669 side: KrakenOrderSide::Buy,
670 order_qty: 0.01,
671 symbol: "BTC/USD".to_string(),
672 limit_price: Some(30000.0),
673 time_in_force: Some(KrakenTimeInForce::GoodTilCancelled),
674 expire_time: None,
675 cl_ord_id: Some("O-20260505-000001".to_string()),
676 post_only: Some(true),
677 reduce_only: None,
678 leverage: None,
679 trigger: None,
680 conditional: None,
681 token: "TESTTOKEN".to_string(),
682 })),
683 req_id: Some(42),
684 };
685
686 let serialized = serde_json::to_string(&request).expect("Failed to serialize");
687 let expected: serde_json::Value =
688 serde_json::from_str(&load_test_data("ws_add_order_request.json"))
689 .expect("Failed to parse fixture");
690 let actual: serde_json::Value =
691 serde_json::from_str(&serialized).expect("Failed to parse serialized");
692 assert_eq!(actual, expected);
693 }
694
695 #[rstest]
696 fn test_serialize_amend_order_request() {
697 let request = KrakenWsRequest {
698 method: KrakenWsMethod::AmendOrder,
699 params: Some(KrakenWsParams::AmendOrder(KrakenWsAmendOrderParams {
700 order_id: Some("OABCDE-12345-FGHIJ".to_string()),
701 cl_ord_id: None,
702 order_qty: Some(0.005),
703 limit_price: None,
704 trigger_price: None,
705 token: "TESTTOKEN".to_string(),
706 })),
707 req_id: Some(43),
708 };
709
710 let serialized = serde_json::to_string(&request).expect("Failed to serialize");
711 let expected: serde_json::Value =
712 serde_json::from_str(&load_test_data("ws_amend_order_request.json"))
713 .expect("Failed to parse fixture");
714 let actual: serde_json::Value =
715 serde_json::from_str(&serialized).expect("Failed to parse serialized");
716 assert_eq!(actual, expected);
717 }
718
719 #[rstest]
720 fn test_serialize_cancel_order_request() {
721 let request = KrakenWsRequest {
722 method: KrakenWsMethod::CancelOrder,
723 params: Some(KrakenWsParams::CancelOrder(KrakenWsCancelOrderParams {
724 order_id: Some(vec!["OABCDE-12345-FGHIJ".to_string()]),
725 cl_ord_id: None,
726 token: "TESTTOKEN".to_string(),
727 })),
728 req_id: Some(44),
729 };
730
731 let serialized = serde_json::to_string(&request).expect("Failed to serialize");
732 let expected: serde_json::Value =
733 serde_json::from_str(&load_test_data("ws_cancel_order_request.json"))
734 .expect("Failed to parse fixture");
735 let actual: serde_json::Value =
736 serde_json::from_str(&serialized).expect("Failed to parse serialized");
737 assert_eq!(actual, expected);
738 }
739
740 #[rstest]
741 fn test_serialize_batch_add_request() {
742 let request = KrakenWsRequest {
743 method: KrakenWsMethod::BatchAdd,
744 params: Some(KrakenWsParams::BatchAdd(KrakenWsBatchAddParams {
745 symbol: "BTC/USD".to_string(),
746 orders: vec![
747 KrakenWsBatchAddOrder {
748 order_type: KrakenOrderType::Limit,
749 side: KrakenOrderSide::Buy,
750 order_qty: 0.01,
751 limit_price: Some(30000.0),
752 cl_ord_id: Some("O-A".to_string()),
753 time_in_force: None,
754 expire_time: None,
755 post_only: None,
756 reduce_only: None,
757 leverage: None,
758 trigger: None,
759 },
760 KrakenWsBatchAddOrder {
761 order_type: KrakenOrderType::Limit,
762 side: KrakenOrderSide::Sell,
763 order_qty: 0.01,
764 limit_price: Some(31000.0),
765 cl_ord_id: Some("O-B".to_string()),
766 time_in_force: None,
767 expire_time: None,
768 post_only: None,
769 reduce_only: None,
770 leverage: None,
771 trigger: None,
772 },
773 ],
774 token: "TESTTOKEN".to_string(),
775 })),
776 req_id: Some(45),
777 };
778
779 let serialized = serde_json::to_string(&request).expect("Failed to serialize");
780 let expected: serde_json::Value =
781 serde_json::from_str(&load_test_data("ws_batch_add_request.json"))
782 .expect("Failed to parse fixture");
783 let actual: serde_json::Value =
784 serde_json::from_str(&serialized).expect("Failed to parse serialized");
785 assert_eq!(actual, expected);
786 }
787
788 #[rstest]
789 fn test_add_order_params_serializes_expire_time_for_gtd() {
790 let params = KrakenWsAddOrderParams {
791 order_type: KrakenOrderType::Limit,
792 side: KrakenOrderSide::Buy,
793 order_qty: 0.01,
794 symbol: "BTC/USD".to_string(),
795 token: "TKN".to_string(),
796 limit_price: Some(30000.0),
797 time_in_force: Some(KrakenTimeInForce::GoodTilDate),
798 expire_time: Some("2026-12-31T23:59:59+00:00".to_string()),
799 cl_ord_id: None,
800 post_only: None,
801 reduce_only: None,
802 leverage: None,
803 trigger: None,
804 conditional: None,
805 };
806 let value: serde_json::Value =
807 serde_json::from_str(&serde_json::to_string(¶ms).expect("serialize"))
808 .expect("json");
809
810 assert_eq!(value["time_in_force"], "GTD");
811 assert_eq!(value["expire_time"], "2026-12-31T23:59:59+00:00");
812 }
813
814 #[rstest]
815 fn test_add_order_params_omits_expire_time_when_absent() {
816 let params = KrakenWsAddOrderParams {
817 order_type: KrakenOrderType::Limit,
818 side: KrakenOrderSide::Buy,
819 order_qty: 0.01,
820 symbol: "BTC/USD".to_string(),
821 token: "TKN".to_string(),
822 limit_price: Some(30000.0),
823 time_in_force: None,
824 expire_time: None,
825 cl_ord_id: None,
826 post_only: None,
827 reduce_only: None,
828 leverage: None,
829 trigger: None,
830 conditional: None,
831 };
832 let value: serde_json::Value =
833 serde_json::from_str(&serde_json::to_string(¶ms).expect("serialize"))
834 .expect("json");
835
836 assert!(value.get("expire_time").is_none());
837 }
838
839 #[rstest]
840 fn test_batch_add_order_serializes_leverage_and_trigger() {
841 let order = KrakenWsBatchAddOrder {
842 order_type: KrakenOrderType::StopLossLimit,
843 side: KrakenOrderSide::Buy,
844 order_qty: 0.01,
845 limit_price: Some(31000.0),
846 cl_ord_id: Some("O-CONDITIONAL".to_string()),
847 time_in_force: None,
848 expire_time: None,
849 post_only: None,
850 reduce_only: None,
851 leverage: Some(2),
852 trigger: Some(KrakenWsTriggerParams {
853 reference: KrakenSpotTrigger::Last,
854 price: 30500.0,
855 price_type: None,
856 }),
857 };
858 let value: serde_json::Value =
859 serde_json::from_str(&serde_json::to_string(&order).expect("serialize")).expect("json");
860
861 assert_eq!(
862 value["leverage"], 2,
863 "leverage must be serialized for margin batch legs",
864 );
865 assert!(
866 value.get("trigger").is_some(),
867 "trigger must be serialized for conditional batch legs",
868 );
869 assert_eq!(value["trigger"]["reference"], "last");
870 assert_eq!(value["trigger"]["price"], 30500.0);
871 }
872
873 #[rstest]
874 fn test_batch_add_order_omits_leverage_and_trigger_when_absent() {
875 let order = KrakenWsBatchAddOrder {
876 order_type: KrakenOrderType::Limit,
877 side: KrakenOrderSide::Buy,
878 order_qty: 0.01,
879 limit_price: Some(30000.0),
880 cl_ord_id: Some("O-PLAIN".to_string()),
881 time_in_force: None,
882 expire_time: None,
883 post_only: None,
884 reduce_only: None,
885 leverage: None,
886 trigger: None,
887 };
888 let value: serde_json::Value =
889 serde_json::from_str(&serde_json::to_string(&order).expect("serialize")).expect("json");
890
891 assert!(
892 value.get("leverage").is_none(),
893 "leverage must be omitted when None"
894 );
895 assert!(
896 value.get("trigger").is_none(),
897 "trigger must be omitted when None"
898 );
899 }
900
901 #[rstest]
902 fn test_deserialize_add_order_response_success() {
903 let data = load_test_data("ws_add_order_response_success.json");
904 let response: KrakenWsOrderResponse =
905 serde_json::from_str(&data).expect("Failed to parse add_order success response");
906
907 assert_eq!(response.method, KrakenWsMethod::AddOrder);
908 assert_eq!(response.req_id, Some(42));
909 assert!(response.success);
910 assert!(response.error.is_none());
911
912 let result = response.result.expect("Expected result");
913 assert_eq!(result.order_id.as_deref(), Some("OABCDE-12345-FGHIJ"));
914 assert_eq!(result.cl_ord_id.as_deref(), Some("O-20260505-000001"));
915 assert_eq!(result.order_userref, Some(0));
916 }
917
918 #[rstest]
919 fn test_deserialize_add_order_response_failure() {
920 let data = load_test_data("ws_add_order_response_failure.json");
921 let response: KrakenWsOrderResponse =
922 serde_json::from_str(&data).expect("Failed to parse add_order failure response");
923
924 assert_eq!(response.method, KrakenWsMethod::AddOrder);
925 assert_eq!(response.req_id, Some(99));
926 assert!(!response.success);
927 assert_eq!(response.error.as_deref(), Some("EOrder:Insufficient funds"));
928 assert!(response.result.is_none());
929 }
930
931 #[rstest]
932 fn test_deserialize_batch_add_response_partial() {
933 let data = load_test_data("ws_batch_add_response_partial.json");
934 let response: KrakenWsOrderResponse =
935 serde_json::from_str(&data).expect("Failed to parse batch_add partial response");
936
937 assert_eq!(response.method, KrakenWsMethod::BatchAdd);
938 assert_eq!(response.req_id, Some(45));
939 assert!(response.success);
940
941 let result = response.result.expect("Expected result");
942 let orders = result.orders.expect("Expected orders");
943 assert_eq!(orders.len(), 2);
944
945 assert!(orders[0].success);
946 assert_eq!(orders[0].order_id.as_deref(), Some("O1"));
947 assert_eq!(orders[0].cl_ord_id.as_deref(), Some("O-A"));
948 assert!(orders[0].error.is_none());
949
950 assert!(!orders[1].success);
951 assert!(orders[1].order_id.is_none());
952 assert_eq!(orders[1].cl_ord_id.as_deref(), Some("O-B"));
953 assert_eq!(orders[1].error.as_deref(), Some("EOrder:Invalid price"));
954 }
955}