1use jiff::Timestamp;
19#[cfg(test)]
20use nautilus_core::string::secret::REDACTED;
21use nautilus_core::string::secret::SecretString;
22use rust_decimal::Decimal;
23use serde::{Deserialize, Serialize};
24use serde_json::{Value, value::RawValue};
25use ustr::Ustr;
26
27use super::enums::{
28 KrakenExecType, KrakenLiquidityInd, KrakenWsChannel, KrakenWsMessageType, KrakenWsMethod,
29 KrakenWsOrderStatus,
30};
31use crate::{
32 common::{
33 enums::{KrakenOrderSide, KrakenOrderType, KrakenSpotTrigger, KrakenTimeInForce},
34 serialization::{decimal, optional_decimal},
35 },
36 websocket::spot_v2::level_3::messages::{KrakenL3Snapshot, KrakenL3UpdateData},
37};
38
39#[derive(Clone, Debug)]
41pub enum KrakenSpotWsMessage {
42 Ticker(Vec<KrakenWsTickerData>),
43 Trade(Vec<KrakenWsTradeData>),
44 Book {
45 data: Vec<KrakenWsBookData>,
46 is_snapshot: bool,
47 },
48 Ohlc(Vec<KrakenWsOhlcData>),
49 Execution(Vec<KrakenWsExecutionData>),
50 OrderResponse(KrakenWsOrderResponse),
51 L3Snapshot(KrakenL3Snapshot),
52 L3Update(KrakenL3UpdateData),
53 SubscriptionAck {
58 req_id: Option<u64>,
59 symbol: Option<Ustr>,
60 success: bool,
61 error: Option<String>,
62 },
63 Reconnected,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct KrakenWsRequest {
68 pub method: KrakenWsMethod,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub params: Option<KrakenWsParams>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub req_id: Option<u64>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(untagged)]
78pub enum KrakenWsParams {
79 Channel(KrakenWsChannelParams),
81 AddOrder(KrakenWsAddOrderParams),
83 AmendOrder(KrakenWsAmendOrderParams),
85 CancelOrder(KrakenWsCancelOrderParams),
87 BatchAdd(KrakenWsBatchAddParams),
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct KrakenWsChannelParams {
94 pub channel: KrakenWsChannel,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub symbol: Option<Vec<Ustr>>,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub snapshot: Option<bool>,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub depth: Option<u32>,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub interval: Option<u32>,
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub event_trigger: Option<String>,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub token: Option<SecretString>,
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub snap_orders: Option<bool>,
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub snap_trades: Option<bool>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct KrakenWsAddOrderParams {
125 pub order_type: KrakenOrderType,
127 pub side: KrakenOrderSide,
129 #[serde(with = "decimal")]
131 pub order_qty: Decimal,
132 pub symbol: String,
134 pub token: SecretString,
136 #[serde(
138 default,
139 skip_serializing_if = "Option::is_none",
140 with = "optional_decimal"
141 )]
142 pub limit_price: Option<Decimal>,
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub time_in_force: Option<KrakenTimeInForce>,
146 #[serde(skip_serializing_if = "Option::is_none")]
150 pub expire_time: Option<String>,
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub cl_ord_id: Option<String>,
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub post_only: Option<bool>,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub reduce_only: Option<bool>,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub trigger: Option<KrakenWsTriggerParams>,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub leverage: Option<u16>,
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub conditional: Option<KrakenWsConditionalParams>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct KrakenWsAmendOrderParams {
174 pub token: SecretString,
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub order_id: Option<String>,
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub cl_ord_id: Option<String>,
182 #[serde(
184 default,
185 skip_serializing_if = "Option::is_none",
186 with = "optional_decimal"
187 )]
188 pub order_qty: Option<Decimal>,
189 #[serde(
191 default,
192 skip_serializing_if = "Option::is_none",
193 with = "optional_decimal"
194 )]
195 pub limit_price: Option<Decimal>,
196 #[serde(
198 default,
199 skip_serializing_if = "Option::is_none",
200 with = "optional_decimal"
201 )]
202 pub trigger_price: Option<Decimal>,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct KrakenWsCancelOrderParams {
208 pub token: SecretString,
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub order_id: Option<Vec<String>>,
213 #[serde(skip_serializing_if = "Option::is_none")]
215 pub cl_ord_id: Option<Vec<String>>,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct KrakenWsBatchAddParams {
221 pub symbol: String,
223 pub orders: Vec<KrakenWsBatchAddOrder>,
225 pub token: SecretString,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct KrakenWsBatchAddOrder {
232 pub order_type: KrakenOrderType,
234 pub side: KrakenOrderSide,
236 #[serde(with = "decimal")]
238 pub order_qty: Decimal,
239 #[serde(
241 default,
242 skip_serializing_if = "Option::is_none",
243 with = "optional_decimal"
244 )]
245 pub limit_price: Option<Decimal>,
246 #[serde(skip_serializing_if = "Option::is_none")]
248 pub cl_ord_id: Option<String>,
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub time_in_force: Option<KrakenTimeInForce>,
252 #[serde(skip_serializing_if = "Option::is_none")]
255 pub expire_time: Option<String>,
256 #[serde(skip_serializing_if = "Option::is_none")]
258 pub post_only: Option<bool>,
259 #[serde(skip_serializing_if = "Option::is_none")]
261 pub reduce_only: Option<bool>,
262 #[serde(skip_serializing_if = "Option::is_none")]
264 pub leverage: Option<u16>,
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub trigger: Option<KrakenWsTriggerParams>,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct KrakenWsTriggerParams {
273 pub reference: KrakenSpotTrigger,
275 #[serde(with = "decimal")]
277 pub price: Decimal,
278 #[serde(skip_serializing_if = "Option::is_none")]
280 pub price_type: Option<String>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct KrakenWsConditionalParams {
286 pub order_type: KrakenOrderType,
288 #[serde(
290 default,
291 skip_serializing_if = "Option::is_none",
292 with = "optional_decimal"
293 )]
294 pub limit_price: Option<Decimal>,
295 #[serde(
297 default,
298 skip_serializing_if = "Option::is_none",
299 with = "optional_decimal"
300 )]
301 pub trigger_price: Option<Decimal>,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct KrakenWsOrderResponse {
307 pub method: KrakenWsMethod,
309 #[serde(skip_serializing_if = "Option::is_none")]
311 pub req_id: Option<u64>,
312 pub success: bool,
314 #[serde(skip_serializing_if = "Option::is_none")]
316 pub time_in: Option<String>,
317 #[serde(skip_serializing_if = "Option::is_none")]
319 pub time_out: Option<String>,
320 #[serde(skip_serializing_if = "Option::is_none")]
322 pub error: Option<String>,
323 #[serde(skip_serializing_if = "Option::is_none")]
325 pub result: Option<KrakenWsOrderResult>,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct KrakenWsOrderResult {
331 #[serde(skip_serializing_if = "Option::is_none")]
333 pub order_id: Option<String>,
334 #[serde(skip_serializing_if = "Option::is_none")]
336 pub cl_ord_id: Option<String>,
337 #[serde(skip_serializing_if = "Option::is_none")]
339 pub order_userref: Option<i64>,
340 #[serde(skip_serializing_if = "Option::is_none")]
342 pub warning: Option<Vec<String>>,
343 #[serde(skip_serializing_if = "Option::is_none")]
345 pub orders: Option<Vec<KrakenWsBatchOrderResult>>,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct KrakenWsBatchOrderResult {
351 pub success: bool,
353 #[serde(skip_serializing_if = "Option::is_none")]
355 pub order_id: Option<String>,
356 #[serde(skip_serializing_if = "Option::is_none")]
358 pub cl_ord_id: Option<String>,
359 #[serde(skip_serializing_if = "Option::is_none")]
361 pub error: Option<String>,
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize)]
365#[serde(tag = "method")]
366pub enum KrakenWsResponse {
367 #[serde(rename = "pong")]
368 Pong(KrakenWsPong),
369 #[serde(rename = "subscribe")]
370 Subscribe(KrakenWsSubscribeResponse),
371 #[serde(rename = "unsubscribe")]
372 Unsubscribe(KrakenWsUnsubscribeResponse),
373 #[serde(other)]
374 Other,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize)]
378pub struct KrakenWsPong {
379 #[serde(skip_serializing_if = "Option::is_none")]
380 pub req_id: Option<u64>,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct KrakenWsSubscribeResponse {
385 pub success: bool,
386 #[serde(skip_serializing_if = "Option::is_none")]
387 pub error: Option<String>,
388 #[serde(skip_serializing_if = "Option::is_none")]
389 pub req_id: Option<u64>,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub symbol: Option<Ustr>,
393 #[serde(skip_serializing_if = "Option::is_none")]
394 pub result: Option<KrakenWsSubscriptionResult>,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize)]
398pub struct KrakenWsUnsubscribeResponse {
399 pub success: bool,
400 #[serde(skip_serializing_if = "Option::is_none")]
401 pub error: Option<String>,
402 #[serde(skip_serializing_if = "Option::is_none")]
403 pub req_id: Option<u64>,
404 #[serde(default, skip_serializing_if = "Option::is_none")]
406 pub symbol: Option<Ustr>,
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct KrakenWsSubscriptionResult {
411 pub channel: KrakenWsChannel,
412 #[serde(skip_serializing_if = "Option::is_none")]
413 pub snapshot: Option<bool>,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct KrakenWsMessage {
418 pub channel: KrakenWsChannel,
419 #[serde(rename = "type")]
420 pub event_type: KrakenWsMessageType,
421 pub data: Vec<Value>,
422 #[serde(skip_serializing_if = "Option::is_none")]
423 pub symbol: Option<Ustr>,
424 #[serde(skip_serializing_if = "Option::is_none")]
425 pub timestamp: Option<Timestamp>,
426}
427
428#[derive(Debug, Clone, Deserialize)]
429pub(crate) struct KrakenWsRawMessage {
430 pub channel: KrakenWsChannel,
431 #[serde(rename = "type")]
432 pub event_type: KrakenWsMessageType,
433 pub data: Vec<Box<RawValue>>,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct KrakenWsTickerData {
438 pub symbol: Ustr,
439 #[serde(with = "decimal")]
440 pub bid: Decimal,
441 #[serde(with = "decimal")]
442 pub bid_qty: Decimal,
443 #[serde(with = "decimal")]
444 pub ask: Decimal,
445 #[serde(with = "decimal")]
446 pub ask_qty: Decimal,
447 #[serde(with = "decimal")]
448 pub last: Decimal,
449 #[serde(with = "decimal")]
450 pub volume: Decimal,
451 #[serde(with = "decimal")]
452 pub vwap: Decimal,
453 #[serde(with = "decimal")]
454 pub low: Decimal,
455 #[serde(with = "decimal")]
456 pub high: Decimal,
457 #[serde(with = "decimal")]
458 pub change: Decimal,
459 #[serde(with = "decimal")]
460 pub change_pct: Decimal,
461 pub timestamp: Timestamp,
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize)]
465pub struct KrakenWsTradeData {
466 pub symbol: Ustr,
467 pub side: KrakenOrderSide,
468 #[serde(with = "decimal")]
469 pub price: Decimal,
470 #[serde(with = "decimal")]
471 pub qty: Decimal,
472 pub ord_type: KrakenOrderType,
473 pub trade_id: i64,
474 pub timestamp: Timestamp,
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize)]
478pub struct KrakenWsBookData {
479 pub symbol: Ustr,
480 #[serde(skip_serializing_if = "Option::is_none")]
481 pub bids: Option<Vec<KrakenWsBookLevel>>,
482 #[serde(skip_serializing_if = "Option::is_none")]
483 pub asks: Option<Vec<KrakenWsBookLevel>>,
484 pub checksum: Option<u32>,
485 pub timestamp: Timestamp,
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct KrakenWsBookLevel {
490 #[serde(with = "decimal")]
491 pub price: Decimal,
492 #[serde(with = "decimal")]
493 pub qty: Decimal,
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct KrakenWsOhlcData {
498 pub symbol: Ustr,
499 pub interval: u32,
500 pub interval_begin: Timestamp,
501 #[serde(with = "decimal")]
502 pub open: Decimal,
503 #[serde(with = "decimal")]
504 pub high: Decimal,
505 #[serde(with = "decimal")]
506 pub low: Decimal,
507 #[serde(with = "decimal")]
508 pub close: Decimal,
509 #[serde(with = "decimal")]
510 pub volume: Decimal,
511 #[serde(with = "decimal")]
512 pub vwap: Decimal,
513 pub trades: i64,
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize)]
518pub struct KrakenWsExecutionData {
519 pub exec_type: KrakenExecType,
521 pub order_id: String,
523 #[serde(skip_serializing_if = "Option::is_none")]
525 pub cl_ord_id: Option<String>,
526 #[serde(skip_serializing_if = "Option::is_none")]
528 pub symbol: Option<String>,
529 #[serde(skip_serializing_if = "Option::is_none")]
531 pub side: Option<KrakenOrderSide>,
532 #[serde(skip_serializing_if = "Option::is_none")]
534 pub order_type: Option<KrakenOrderType>,
535 #[serde(
537 default,
538 skip_serializing_if = "Option::is_none",
539 with = "optional_decimal"
540 )]
541 pub order_qty: Option<Decimal>,
542 #[serde(
544 default,
545 skip_serializing_if = "Option::is_none",
546 with = "optional_decimal"
547 )]
548 pub limit_price: Option<Decimal>,
549 #[serde(skip_serializing_if = "Option::is_none")]
551 pub order_status: Option<KrakenWsOrderStatus>,
552 #[serde(
554 default,
555 skip_serializing_if = "Option::is_none",
556 with = "optional_decimal"
557 )]
558 pub cum_qty: Option<Decimal>,
559 #[serde(
561 default,
562 skip_serializing_if = "Option::is_none",
563 with = "optional_decimal"
564 )]
565 pub cum_cost: Option<Decimal>,
566 #[serde(
568 default,
569 skip_serializing_if = "Option::is_none",
570 with = "optional_decimal"
571 )]
572 pub avg_price: Option<Decimal>,
573 #[serde(skip_serializing_if = "Option::is_none")]
575 pub time_in_force: Option<KrakenTimeInForce>,
576 #[serde(skip_serializing_if = "Option::is_none")]
578 pub post_only: Option<bool>,
579 #[serde(skip_serializing_if = "Option::is_none")]
581 pub reduce_only: Option<bool>,
582 pub timestamp: Timestamp,
584 #[serde(skip_serializing_if = "Option::is_none")]
586 pub exec_id: Option<String>,
587 #[serde(
589 default,
590 skip_serializing_if = "Option::is_none",
591 with = "optional_decimal"
592 )]
593 pub last_qty: Option<Decimal>,
594 #[serde(
596 default,
597 skip_serializing_if = "Option::is_none",
598 with = "optional_decimal"
599 )]
600 pub last_price: Option<Decimal>,
601 #[serde(
603 default,
604 skip_serializing_if = "Option::is_none",
605 with = "optional_decimal"
606 )]
607 pub cost: Option<Decimal>,
608 #[serde(skip_serializing_if = "Option::is_none")]
610 pub liquidity_ind: Option<KrakenLiquidityInd>,
611 #[serde(skip_serializing_if = "Option::is_none")]
613 pub fees: Option<Vec<KrakenWsFee>>,
614 #[serde(
616 default,
617 skip_serializing_if = "Option::is_none",
618 with = "optional_decimal"
619 )]
620 pub fee_usd_equiv: Option<Decimal>,
621 #[serde(skip_serializing_if = "Option::is_none")]
623 pub reason: Option<String>,
624}
625
626#[derive(Debug, Clone, Serialize, Deserialize)]
628pub struct KrakenWsFee {
629 pub asset: String,
631 #[serde(with = "decimal")]
633 pub qty: Decimal,
634}
635
636#[cfg(test)]
637mod tests {
638 use rstest::rstest;
639 use rust_decimal_macros::dec;
640
641 use super::*;
642
643 #[rstest]
644 fn test_private_request_debug_redacts_tokens() {
645 let channel = KrakenWsChannelParams {
646 channel: KrakenWsChannel::Executions,
647 symbol: None,
648 snapshot: None,
649 depth: None,
650 interval: None,
651 event_trigger: None,
652 token: Some(SecretString::from("channel-token-value")),
653 snap_orders: Some(true),
654 snap_trades: Some(true),
655 };
656 let cancel = KrakenWsCancelOrderParams {
657 token: SecretString::from("cancel-token-value"),
658 order_id: Some(vec!["ORDER-1".to_string()]),
659 cl_ord_id: None,
660 };
661
662 let channel_json = serde_json::to_value(&channel).unwrap();
663 let cancel_json = serde_json::to_value(&cancel).unwrap();
664 let formatted = format!("{channel:?} {cancel:?}");
665
666 assert_eq!(channel_json["token"], "channel-token-value");
667 assert_eq!(cancel_json["token"], "cancel-token-value");
668 assert_eq!(formatted.matches(REDACTED).count(), 2);
669 assert!(!formatted.contains("channel-token-value"));
670 assert!(!formatted.contains("cancel-token-value"));
671 }
672
673 fn load_test_data(filename: &str) -> String {
674 let path = format!("test_data/{filename}");
675 std::fs::read_to_string(&path)
676 .unwrap_or_else(|e| panic!("Failed to load test data from {path}: {e}"))
677 }
678
679 #[rstest]
680 fn test_parse_subscribe_response() {
681 let data = load_test_data("ws_subscribe_response.json");
682 let response: KrakenWsResponse =
683 serde_json::from_str(&data).expect("Failed to parse subscribe response");
684
685 match response {
686 KrakenWsResponse::Subscribe(sub) => {
687 assert!(sub.success);
688 assert_eq!(sub.req_id, Some(1));
689 assert!(sub.result.is_some());
690 let result = sub.result.unwrap();
691 assert_eq!(result.channel, KrakenWsChannel::Ticker);
692 }
693 _ => panic!("Expected Subscribe response"),
694 }
695 }
696
697 #[rstest]
699 fn test_parse_subscribe_rejection_names_the_symbol() {
700 let data = r#"{"error":"Currency pair not supported BOGUS/NOPE","method":"subscribe","req_id":7,"success":false,"symbol":"BOGUS/NOPE","time_in":"2024-01-01T00:00:00.000000Z","time_out":"2024-01-01T00:00:00.000100Z"}"#;
701 let response: KrakenWsResponse =
702 serde_json::from_str(data).expect("Failed to parse subscribe rejection");
703
704 let KrakenWsResponse::Subscribe(sub) = response else {
705 panic!("Expected Subscribe response");
706 };
707 assert!(!sub.success);
708 assert_eq!(sub.req_id, Some(7));
709 assert_eq!(sub.symbol, Some(Ustr::from("BOGUS/NOPE")));
710 assert_eq!(
711 sub.error.as_deref(),
712 Some("Currency pair not supported BOGUS/NOPE")
713 );
714 assert!(sub.result.is_none());
715 }
716
717 #[rstest]
718 fn test_parse_pong() {
719 let data = load_test_data("ws_pong.json");
720 let response: KrakenWsResponse = serde_json::from_str(&data).expect("Failed to parse pong");
721
722 match response {
723 KrakenWsResponse::Pong(pong) => {
724 assert_eq!(pong.req_id, Some(42));
725 }
726 _ => panic!("Expected Pong response"),
727 }
728 }
729
730 #[rstest]
731 fn test_parse_ticker_snapshot() {
732 let data = load_test_data("ws_ticker_snapshot.json");
733 let message: KrakenWsRawMessage =
734 serde_json::from_str(&data).expect("Failed to parse ticker snapshot");
735
736 assert_eq!(message.channel, KrakenWsChannel::Ticker);
737 assert_eq!(message.event_type, KrakenWsMessageType::Snapshot);
738 assert!(!message.data.is_empty());
739
740 let ticker: KrakenWsTickerData =
741 serde_json::from_str(message.data[0].get()).expect("Failed to parse ticker data");
742 assert_eq!(ticker.symbol, "BTC/USD");
743 assert_eq!(ticker.bid, dec!(105944.20));
744 assert_eq!(ticker.ask, dec!(105944.30));
745 assert_eq!(ticker.last, dec!(105899.40));
746 assert_eq!(ticker.timestamp.as_nanosecond(), 1_671_960_659_123_456_000);
747 }
748
749 #[rstest]
750 fn test_optional_decimal_fields_default_when_missing() {
751 let execution: KrakenWsExecutionData = serde_json::from_str(&load_test_data(
752 "ws_execution_missing_optional_decimals.json",
753 ))
754 .unwrap();
755 let amend: KrakenWsAmendOrderParams = serde_json::from_str(&load_test_data(
756 "ws_amend_order_missing_optional_decimals.json",
757 ))
758 .unwrap();
759
760 assert_eq!(
761 (
762 execution.order_qty,
763 execution.limit_price,
764 execution.cum_qty,
765 execution.cum_cost,
766 execution.avg_price,
767 execution.last_qty,
768 execution.last_price,
769 execution.cost,
770 execution.fee_usd_equiv,
771 ),
772 (None, None, None, None, None, None, None, None, None)
773 );
774 assert_eq!(
775 (amend.order_qty, amend.limit_price, amend.trigger_price),
776 (None, None, None)
777 );
778 }
779
780 #[rstest]
781 fn test_parse_trade_update() {
782 let data = load_test_data("ws_trade_update.json");
783 let message: KrakenWsRawMessage =
784 serde_json::from_str(&data).expect("Failed to parse trade update");
785
786 assert_eq!(message.channel, KrakenWsChannel::Trade);
787 assert_eq!(message.event_type, KrakenWsMessageType::Update);
788 assert_eq!(message.data.len(), 2);
789
790 let trade: KrakenWsTradeData =
791 serde_json::from_str(message.data[0].get()).expect("Failed to parse trade data");
792 assert_eq!(trade.symbol, "BTC/USD");
793 assert_eq!(trade.price, dec!(105944.20));
794 assert_eq!(trade.qty, dec!(0.00027625));
795 assert!(trade.trade_id > 0);
796 }
797
798 #[rstest]
799 fn test_parse_book_snapshot() {
800 let data = load_test_data("ws_book_snapshot.json");
801 let message: KrakenWsRawMessage =
802 serde_json::from_str(&data).expect("Failed to parse book snapshot");
803
804 assert_eq!(message.channel, KrakenWsChannel::Book);
805 assert_eq!(message.event_type, KrakenWsMessageType::Snapshot);
806
807 let book: KrakenWsBookData =
808 serde_json::from_str(message.data[0].get()).expect("Failed to parse book data");
809 assert_eq!(book.symbol, "BTC/USD");
810 assert!(book.bids.is_some());
811 assert!(book.asks.is_some());
812 assert!(book.checksum.is_some());
813 assert_eq!(book.timestamp.as_nanosecond(), 1_696_613_755_440_295_000);
814
815 let bids = book.bids.unwrap();
816 assert_eq!(bids.len(), 10);
817 assert_eq!(bids[0].price, dec!(45283.5));
818 assert_eq!(bids[0].qty, dec!(0.10000000));
819 }
820
821 #[rstest]
822 fn test_parse_book_update() {
823 let data = load_test_data("ws_book_update.json");
824 let message: KrakenWsRawMessage =
825 serde_json::from_str(&data).expect("Failed to parse book update");
826
827 assert_eq!(message.channel, KrakenWsChannel::Book);
828 assert_eq!(message.event_type, KrakenWsMessageType::Update);
829
830 let book: KrakenWsBookData =
831 serde_json::from_str(message.data[0].get()).expect("Failed to parse book data");
832 assert_eq!(book.timestamp.as_nanosecond(), 1_696_613_755_440_295_000);
833 assert!(book.checksum.is_some());
834 }
835
836 #[rstest]
837 fn test_parse_ohlc_update() {
838 let data = load_test_data("ws_ohlc_update.json");
839 let message: KrakenWsRawMessage =
840 serde_json::from_str(&data).expect("Failed to parse OHLC update");
841
842 assert_eq!(message.channel, KrakenWsChannel::Ohlc);
843 assert_eq!(message.event_type, KrakenWsMessageType::Update);
844
845 let ohlc: KrakenWsOhlcData =
846 serde_json::from_str(message.data[0].get()).expect("Failed to parse OHLC data");
847 assert_eq!(ohlc.symbol, "BTC/USD");
848 assert_eq!(ohlc.open, dec!(106038.2));
849 assert_eq!(ohlc.high, dec!(106044.3));
850 assert_eq!(ohlc.low, dec!(106038.1));
851 assert_eq!(ohlc.close, dec!(106040.1));
852 assert_eq!(ohlc.interval, 1);
853 assert!(ohlc.trades > 0);
854 }
855
856 #[rstest]
857 fn test_serialize_add_order_request() {
858 let request = KrakenWsRequest {
859 method: KrakenWsMethod::AddOrder,
860 params: Some(KrakenWsParams::AddOrder(KrakenWsAddOrderParams {
861 order_type: KrakenOrderType::Limit,
862 side: KrakenOrderSide::Buy,
863 order_qty: dec!(0.01),
864 symbol: "BTC/USD".to_string(),
865 limit_price: Some(dec!(30000.0)),
866 time_in_force: Some(KrakenTimeInForce::GoodTilCancelled),
867 expire_time: None,
868 cl_ord_id: Some("O-20260505-000001".to_string()),
869 post_only: Some(true),
870 reduce_only: None,
871 leverage: None,
872 trigger: None,
873 conditional: None,
874 token: SecretString::from("TESTTOKEN"),
875 })),
876 req_id: Some(42),
877 };
878
879 let serialized = serde_json::to_string(&request).expect("Failed to serialize");
880 let expected: serde_json::Value =
881 serde_json::from_str(&load_test_data("ws_add_order_request.json"))
882 .expect("Failed to parse fixture");
883 let actual: serde_json::Value =
884 serde_json::from_str(&serialized).expect("Failed to parse serialized");
885 assert_eq!(actual, expected);
886 }
887
888 #[rstest]
889 fn test_serialize_add_order_request_preserves_decimal_precision() {
890 let request = KrakenWsAddOrderParams {
891 order_type: KrakenOrderType::Limit,
892 side: KrakenOrderSide::Buy,
893 order_qty: dec!(0.1234567890123456789012345678),
894 symbol: "BTC/USD".to_string(),
895 token: SecretString::from("TESTTOKEN"),
896 limit_price: Some(dec!(123456789.123456789)),
897 time_in_force: None,
898 expire_time: None,
899 cl_ord_id: None,
900 post_only: None,
901 reduce_only: None,
902 leverage: None,
903 trigger: None,
904 conditional: None,
905 };
906
907 let serialized = serde_json::to_string(&request).unwrap();
908
909 assert!(serialized.contains("\"order_qty\":0.1234567890123456789012345678"));
910 assert!(serialized.contains("\"limit_price\":123456789.123456789"));
911 }
912
913 #[rstest]
914 fn test_serialize_amend_order_request() {
915 let request = KrakenWsRequest {
916 method: KrakenWsMethod::AmendOrder,
917 params: Some(KrakenWsParams::AmendOrder(KrakenWsAmendOrderParams {
918 order_id: Some("OABCDE-12345-FGHIJ".to_string()),
919 cl_ord_id: None,
920 order_qty: Some(dec!(0.005)),
921 limit_price: None,
922 trigger_price: None,
923 token: SecretString::from("TESTTOKEN"),
924 })),
925 req_id: Some(43),
926 };
927
928 let serialized = serde_json::to_string(&request).expect("Failed to serialize");
929 let expected: serde_json::Value =
930 serde_json::from_str(&load_test_data("ws_amend_order_request.json"))
931 .expect("Failed to parse fixture");
932 let actual: serde_json::Value =
933 serde_json::from_str(&serialized).expect("Failed to parse serialized");
934 assert_eq!(actual, expected);
935 }
936
937 #[rstest]
938 fn test_serialize_cancel_order_request() {
939 let request = KrakenWsRequest {
940 method: KrakenWsMethod::CancelOrder,
941 params: Some(KrakenWsParams::CancelOrder(KrakenWsCancelOrderParams {
942 order_id: Some(vec!["OABCDE-12345-FGHIJ".to_string()]),
943 cl_ord_id: None,
944 token: SecretString::from("TESTTOKEN"),
945 })),
946 req_id: Some(44),
947 };
948
949 let serialized = serde_json::to_string(&request).expect("Failed to serialize");
950 let expected: serde_json::Value =
951 serde_json::from_str(&load_test_data("ws_cancel_order_request.json"))
952 .expect("Failed to parse fixture");
953 let actual: serde_json::Value =
954 serde_json::from_str(&serialized).expect("Failed to parse serialized");
955 assert_eq!(actual, expected);
956 }
957
958 #[rstest]
959 fn test_serialize_batch_add_request() {
960 let request = KrakenWsRequest {
961 method: KrakenWsMethod::BatchAdd,
962 params: Some(KrakenWsParams::BatchAdd(KrakenWsBatchAddParams {
963 symbol: "BTC/USD".to_string(),
964 orders: vec![
965 KrakenWsBatchAddOrder {
966 order_type: KrakenOrderType::Limit,
967 side: KrakenOrderSide::Buy,
968 order_qty: dec!(0.01),
969 limit_price: Some(dec!(30000.0)),
970 cl_ord_id: Some("O-A".to_string()),
971 time_in_force: None,
972 expire_time: None,
973 post_only: None,
974 reduce_only: None,
975 leverage: None,
976 trigger: None,
977 },
978 KrakenWsBatchAddOrder {
979 order_type: KrakenOrderType::Limit,
980 side: KrakenOrderSide::Sell,
981 order_qty: dec!(0.01),
982 limit_price: Some(dec!(31000.0)),
983 cl_ord_id: Some("O-B".to_string()),
984 time_in_force: None,
985 expire_time: None,
986 post_only: None,
987 reduce_only: None,
988 leverage: None,
989 trigger: None,
990 },
991 ],
992 token: SecretString::from("TESTTOKEN"),
993 })),
994 req_id: Some(45),
995 };
996
997 let serialized = serde_json::to_string(&request).expect("Failed to serialize");
998 let expected: serde_json::Value =
999 serde_json::from_str(&load_test_data("ws_batch_add_request.json"))
1000 .expect("Failed to parse fixture");
1001 let actual: serde_json::Value =
1002 serde_json::from_str(&serialized).expect("Failed to parse serialized");
1003 assert_eq!(actual, expected);
1004 }
1005
1006 #[rstest]
1007 fn test_add_order_params_serializes_expire_time_for_gtd() {
1008 let params = KrakenWsAddOrderParams {
1009 order_type: KrakenOrderType::Limit,
1010 side: KrakenOrderSide::Buy,
1011 order_qty: dec!(0.01),
1012 symbol: "BTC/USD".to_string(),
1013 token: SecretString::from("TKN"),
1014 limit_price: Some(dec!(30000.0)),
1015 time_in_force: Some(KrakenTimeInForce::GoodTilDate),
1016 expire_time: Some("2026-12-31T23:59:59+00:00".to_string()),
1017 cl_ord_id: None,
1018 post_only: None,
1019 reduce_only: None,
1020 leverage: None,
1021 trigger: None,
1022 conditional: None,
1023 };
1024 let value: serde_json::Value =
1025 serde_json::from_str(&serde_json::to_string(¶ms).expect("serialize"))
1026 .expect("json");
1027
1028 assert_eq!(value["time_in_force"], "GTD");
1029 assert_eq!(value["expire_time"], "2026-12-31T23:59:59+00:00");
1030 }
1031
1032 #[rstest]
1033 fn test_add_order_params_omits_expire_time_when_absent() {
1034 let params = KrakenWsAddOrderParams {
1035 order_type: KrakenOrderType::Limit,
1036 side: KrakenOrderSide::Buy,
1037 order_qty: dec!(0.01),
1038 symbol: "BTC/USD".to_string(),
1039 token: SecretString::from("TKN"),
1040 limit_price: Some(dec!(30000.0)),
1041 time_in_force: None,
1042 expire_time: None,
1043 cl_ord_id: None,
1044 post_only: None,
1045 reduce_only: None,
1046 leverage: None,
1047 trigger: None,
1048 conditional: None,
1049 };
1050 let value: serde_json::Value =
1051 serde_json::from_str(&serde_json::to_string(¶ms).expect("serialize"))
1052 .expect("json");
1053
1054 assert!(value.get("expire_time").is_none());
1055 }
1056
1057 #[rstest]
1058 fn test_batch_add_order_serializes_leverage_and_trigger() {
1059 let order = KrakenWsBatchAddOrder {
1060 order_type: KrakenOrderType::StopLossLimit,
1061 side: KrakenOrderSide::Buy,
1062 order_qty: dec!(0.01),
1063 limit_price: Some(dec!(31000.0)),
1064 cl_ord_id: Some("O-CONDITIONAL".to_string()),
1065 time_in_force: None,
1066 expire_time: None,
1067 post_only: None,
1068 reduce_only: None,
1069 leverage: Some(2),
1070 trigger: Some(KrakenWsTriggerParams {
1071 reference: KrakenSpotTrigger::Last,
1072 price: dec!(30500.0),
1073 price_type: None,
1074 }),
1075 };
1076 let value: serde_json::Value =
1077 serde_json::from_str(&serde_json::to_string(&order).expect("serialize")).expect("json");
1078
1079 assert_eq!(
1080 value["leverage"], 2,
1081 "leverage must be serialized for margin batch legs",
1082 );
1083 assert!(
1084 value.get("trigger").is_some(),
1085 "trigger must be serialized for conditional batch legs",
1086 );
1087 assert_eq!(value["trigger"]["reference"], "last");
1088 assert_eq!(value["trigger"]["price"].to_string(), "30500.0");
1089 }
1090
1091 #[rstest]
1092 fn test_batch_add_order_omits_leverage_and_trigger_when_absent() {
1093 let order = KrakenWsBatchAddOrder {
1094 order_type: KrakenOrderType::Limit,
1095 side: KrakenOrderSide::Buy,
1096 order_qty: dec!(0.01),
1097 limit_price: Some(dec!(30000.0)),
1098 cl_ord_id: Some("O-PLAIN".to_string()),
1099 time_in_force: None,
1100 expire_time: None,
1101 post_only: None,
1102 reduce_only: None,
1103 leverage: None,
1104 trigger: None,
1105 };
1106 let value: serde_json::Value =
1107 serde_json::from_str(&serde_json::to_string(&order).expect("serialize")).expect("json");
1108
1109 assert!(
1110 value.get("leverage").is_none(),
1111 "leverage must be omitted when None"
1112 );
1113 assert!(
1114 value.get("trigger").is_none(),
1115 "trigger must be omitted when None"
1116 );
1117 }
1118
1119 #[rstest]
1120 fn test_deserialize_add_order_response_success() {
1121 let data = load_test_data("ws_add_order_response_success.json");
1122 let response: KrakenWsOrderResponse =
1123 serde_json::from_str(&data).expect("Failed to parse add_order success response");
1124
1125 assert_eq!(response.method, KrakenWsMethod::AddOrder);
1126 assert_eq!(response.req_id, Some(42));
1127 assert!(response.success);
1128 assert!(response.error.is_none());
1129
1130 let result = response.result.expect("Expected result");
1131 assert_eq!(result.order_id.as_deref(), Some("OABCDE-12345-FGHIJ"));
1132 assert_eq!(result.cl_ord_id.as_deref(), Some("O-20260505-000001"));
1133 assert_eq!(result.order_userref, Some(0));
1134 }
1135
1136 #[rstest]
1137 fn test_deserialize_add_order_response_failure() {
1138 let data = load_test_data("ws_add_order_response_failure.json");
1139 let response: KrakenWsOrderResponse =
1140 serde_json::from_str(&data).expect("Failed to parse add_order failure response");
1141
1142 assert_eq!(response.method, KrakenWsMethod::AddOrder);
1143 assert_eq!(response.req_id, Some(99));
1144 assert!(!response.success);
1145 assert_eq!(response.error.as_deref(), Some("EOrder:Insufficient funds"));
1146 assert!(response.result.is_none());
1147 }
1148
1149 #[rstest]
1150 fn test_deserialize_batch_add_response_partial() {
1151 let data = load_test_data("ws_batch_add_response_partial.json");
1152 let response: KrakenWsOrderResponse =
1153 serde_json::from_str(&data).expect("Failed to parse batch_add partial response");
1154
1155 assert_eq!(response.method, KrakenWsMethod::BatchAdd);
1156 assert_eq!(response.req_id, Some(45));
1157 assert!(response.success);
1158
1159 let result = response.result.expect("Expected result");
1160 let orders = result.orders.expect("Expected orders");
1161 assert_eq!(orders.len(), 2);
1162
1163 assert!(orders[0].success);
1164 assert_eq!(orders[0].order_id.as_deref(), Some("O1"));
1165 assert_eq!(orders[0].cl_ord_id.as_deref(), Some("O-A"));
1166 assert!(orders[0].error.is_none());
1167
1168 assert!(!orders[1].success);
1169 assert!(orders[1].order_id.is_none());
1170 assert_eq!(orders[1].cl_ord_id.as_deref(), Some("O-B"));
1171 assert_eq!(orders[1].error.as_deref(), Some("EOrder:Invalid price"));
1172 }
1173}