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