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