1#[cfg(test)]
19use nautilus_core::string::secret::REDACTED;
20use nautilus_core::string::secret::SecretString;
21use rust_decimal::Decimal;
22use serde::{Deserialize, Deserializer, Serialize};
23use serde_json::Value;
24use strum::{AsRefStr, EnumString};
25use ustr::Ustr;
26use zeroize::Zeroize;
27
28use crate::common::{
29 enums::{KrakenFillType, KrakenFuturesOrderType, KrakenOrderSide},
30 serialization::{decimal, optional_decimal},
31};
32
33fn deserialize_optional_price_zero_as_none<'de, D>(
40 deserializer: D,
41) -> Result<Option<Decimal>, D::Error>
42where
43 D: Deserializer<'de>,
44{
45 let value = optional_decimal::deserialize(deserializer)?;
46 Ok(value.filter(|v| !v.is_zero()))
47}
48
49#[derive(Clone, Debug)]
51#[expect(
52 clippy::large_enum_variant,
53 reason = "Messages are ephemeral and immediately consumed"
54)]
55pub enum KrakenFuturesWsMessage {
56 Ticker(KrakenFuturesTickerData),
57 Trade(KrakenFuturesTradeData),
58 BookSnapshot(KrakenFuturesBookSnapshot),
59 BookDelta(KrakenFuturesBookDelta),
60 OpenOrdersCancel(KrakenFuturesOpenOrdersCancel),
61 OpenOrdersDelta(KrakenFuturesOpenOrdersDelta),
62 FillsDelta(KrakenFuturesFillsDelta),
63 Challenge(String),
64 Reconnected,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumString, AsRefStr)]
69#[serde(rename_all = "snake_case")]
70#[strum(serialize_all = "snake_case")]
71pub enum KrakenFuturesFeed {
72 Ticker,
73 Trade,
74 TradeSnapshot,
75 Book,
76 BookSnapshot,
77 Heartbeat,
78 OpenOrders,
79 OpenOrdersSnapshot,
80 Fills,
81 FillsSnapshot,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, AsRefStr)]
86#[strum(serialize_all = "snake_case")]
87pub enum KrakenFuturesChannel {
88 Book,
89 Deltas,
90 Trades,
91 Quotes,
92 Mark,
93 Index,
94 Funding,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub enum KrakenFuturesEvent {
101 Subscribe,
102 Unsubscribe,
103 Subscribed,
104 Unsubscribed,
105 Info,
106 Error,
107 Alert,
108 Challenge,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum KrakenFuturesMessageType {
115 OpenOrdersSnapshot,
117 OpenOrdersCancel,
118 OpenOrdersDelta,
119 FillsSnapshot,
120 FillsDelta,
121 Ticker,
123 TradeSnapshot,
124 Trade,
125 BookSnapshot,
126 BookDelta,
127 Info,
129 Pong,
130 Subscribed,
131 Unsubscribed,
132 Challenge,
133 Heartbeat,
134 Error,
135 Alert,
136 Unknown,
137}
138
139#[must_use]
140pub fn classify_futures_message(value: &Value) -> KrakenFuturesMessageType {
141 if let Some(event) = value.get("event").and_then(|v| v.as_str()) {
142 return match event {
143 "info" => KrakenFuturesMessageType::Info,
144 "pong" => KrakenFuturesMessageType::Pong,
145 "subscribed" => KrakenFuturesMessageType::Subscribed,
146 "unsubscribed" => KrakenFuturesMessageType::Unsubscribed,
147 "challenge" => KrakenFuturesMessageType::Challenge,
148 "error" => KrakenFuturesMessageType::Error,
149 "alert" => KrakenFuturesMessageType::Alert,
150 _ => KrakenFuturesMessageType::Unknown,
151 };
152 }
153
154 if let Some(feed) = value.get("feed").and_then(|v| v.as_str()) {
155 return match feed {
156 "heartbeat" => KrakenFuturesMessageType::Heartbeat,
157 "open_orders_snapshot" => KrakenFuturesMessageType::OpenOrdersSnapshot,
158 "open_orders" => {
159 if value.get("is_cancel").and_then(|v| v.as_bool()) == Some(true) {
161 if value.get("order").is_some() {
162 KrakenFuturesMessageType::OpenOrdersDelta
163 } else {
164 KrakenFuturesMessageType::OpenOrdersCancel
165 }
166 } else {
167 KrakenFuturesMessageType::OpenOrdersDelta
168 }
169 }
170 "fills_snapshot" => KrakenFuturesMessageType::FillsSnapshot,
171 "fills" => KrakenFuturesMessageType::FillsDelta,
172 "ticker" => KrakenFuturesMessageType::Ticker,
173 "trade_snapshot" => KrakenFuturesMessageType::TradeSnapshot,
174 "trade" => KrakenFuturesMessageType::Trade,
175 "book_snapshot" => KrakenFuturesMessageType::BookSnapshot,
176 "book" => KrakenFuturesMessageType::BookDelta,
177 _ => KrakenFuturesMessageType::Unknown,
178 };
179 }
180
181 KrakenFuturesMessageType::Unknown
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct KrakenFuturesRequest {
187 pub event: KrakenFuturesEvent,
188 pub feed: KrakenFuturesFeed,
189 pub product_ids: Vec<String>,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct KrakenFuturesSubscriptionResponse {
195 pub event: KrakenFuturesEvent,
196 pub feed: KrakenFuturesFeed,
197 pub product_ids: Vec<String>,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct KrakenFuturesErrorResponse {
203 pub event: KrakenFuturesEvent,
204 #[serde(default)]
205 pub message: Option<String>,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct KrakenFuturesInfoMessage {
211 pub event: KrakenFuturesEvent,
212 pub version: i32,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct KrakenFuturesHeartbeat {
218 pub feed: KrakenFuturesFeed,
219 pub time: i64,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct KrakenFuturesTickerData {
225 pub feed: KrakenFuturesFeed,
226 pub product_id: Ustr,
227 #[serde(default)]
228 pub time: Option<i64>,
229 #[serde(default, with = "optional_decimal")]
230 pub bid: Option<Decimal>,
231 #[serde(default, with = "optional_decimal")]
232 pub ask: Option<Decimal>,
233 #[serde(default, with = "optional_decimal")]
234 pub bid_size: Option<Decimal>,
235 #[serde(default, with = "optional_decimal")]
236 pub ask_size: Option<Decimal>,
237 #[serde(default, with = "optional_decimal")]
238 pub last: Option<Decimal>,
239 #[serde(default, with = "optional_decimal")]
240 pub volume: Option<Decimal>,
241 #[serde(default, with = "optional_decimal")]
242 pub volume_quote: Option<Decimal>,
243 #[serde(default, rename = "openInterest", with = "optional_decimal")]
244 pub open_interest: Option<Decimal>,
245 #[serde(default, with = "optional_decimal")]
246 pub index: Option<Decimal>,
247 #[serde(default, rename = "markPrice", with = "optional_decimal")]
248 pub mark_price: Option<Decimal>,
249 #[serde(default, with = "optional_decimal")]
250 pub change: Option<Decimal>,
251 #[serde(default, with = "optional_decimal")]
252 pub open: Option<Decimal>,
253 #[serde(default, with = "optional_decimal")]
254 pub high: Option<Decimal>,
255 #[serde(default, with = "optional_decimal")]
256 pub low: Option<Decimal>,
257 #[serde(default, with = "optional_decimal")]
258 pub funding_rate: Option<Decimal>,
259 #[serde(default, with = "optional_decimal")]
260 pub funding_rate_prediction: Option<Decimal>,
261 #[serde(default, with = "optional_decimal")]
262 pub relative_funding_rate: Option<Decimal>,
263 #[serde(default, with = "optional_decimal")]
264 pub relative_funding_rate_prediction: Option<Decimal>,
265 #[serde(default)]
266 pub next_funding_rate_time: Option<f64>,
267 #[serde(default)]
268 pub tag: Option<String>,
269 #[serde(default)]
270 pub pair: Option<String>,
271 #[serde(default)]
272 pub leverage: Option<String>,
273 #[serde(default)]
274 pub dtm: Option<i64>,
275 #[serde(default, rename = "maturityTime")]
276 pub maturity_time: Option<i64>,
277 #[serde(default)]
278 pub suspended: Option<bool>,
279 #[serde(default)]
280 pub post_only: Option<bool>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct KrakenFuturesTradeData {
286 pub feed: KrakenFuturesFeed,
287 pub product_id: Ustr,
288 #[serde(default)]
289 pub uid: Option<String>,
290 pub side: KrakenOrderSide,
291 #[serde(rename = "type", default)]
292 pub trade_type: Option<String>,
293 pub seq: i64,
294 pub time: i64,
295 #[serde(with = "decimal")]
296 pub qty: Decimal,
297 #[serde(with = "decimal")]
298 pub price: Decimal,
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct KrakenFuturesTradeSnapshot {
304 pub feed: KrakenFuturesFeed,
305 pub product_id: Ustr,
306 pub trades: Vec<KrakenFuturesTradeData>,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct KrakenFuturesBookSnapshot {
312 pub feed: KrakenFuturesFeed,
313 pub product_id: Ustr,
314 pub timestamp: i64,
315 pub seq: i64,
316 #[serde(default, rename = "tickSize", with = "optional_decimal")]
317 pub tick_size: Option<Decimal>,
318 pub bids: Vec<KrakenFuturesBookLevel>,
319 pub asks: Vec<KrakenFuturesBookLevel>,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct KrakenFuturesBookDelta {
325 pub feed: KrakenFuturesFeed,
326 pub product_id: Ustr,
327 pub side: KrakenOrderSide,
328 pub seq: i64,
329 #[serde(with = "decimal")]
330 pub price: Decimal,
331 #[serde(with = "decimal")]
332 pub qty: Decimal,
333 pub timestamp: i64,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct KrakenFuturesBookLevel {
339 #[serde(with = "decimal")]
340 pub price: Decimal,
341 #[serde(with = "decimal")]
342 pub qty: Decimal,
343}
344
345#[derive(Debug, Clone, Serialize)]
347pub struct KrakenFuturesChallengeRequest {
348 pub event: KrakenFuturesEvent,
349 pub api_key: SecretString,
350}
351
352impl Zeroize for KrakenFuturesChallengeRequest {
353 fn zeroize(&mut self) {
354 self.api_key.zeroize();
355 }
356}
357
358#[derive(Debug, Clone, Deserialize)]
360pub struct KrakenFuturesChallengeResponse {
361 pub event: KrakenFuturesEvent,
362 pub message: String,
363}
364
365#[derive(Debug, Clone, Serialize)]
367pub struct KrakenFuturesPrivateSubscribeRequest {
368 pub event: KrakenFuturesEvent,
369 pub feed: KrakenFuturesFeed,
370 pub api_key: SecretString,
371 pub original_challenge: SecretString,
372 pub signed_challenge: SecretString,
373}
374
375impl Zeroize for KrakenFuturesPrivateSubscribeRequest {
376 fn zeroize(&mut self) {
377 self.api_key.zeroize();
378 self.original_challenge.zeroize();
379 self.signed_challenge.zeroize();
380 }
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct KrakenFuturesOpenOrder {
386 pub instrument: Ustr,
387 pub time: i64,
388 pub last_update_time: i64,
389 #[serde(with = "decimal")]
390 pub qty: Decimal,
391 #[serde(with = "decimal")]
392 pub filled: Decimal,
393 #[serde(
395 default,
396 serialize_with = "optional_decimal::serialize",
397 deserialize_with = "deserialize_optional_price_zero_as_none"
398 )]
399 pub limit_price: Option<Decimal>,
400 #[serde(
401 default,
402 serialize_with = "optional_decimal::serialize",
403 deserialize_with = "deserialize_optional_price_zero_as_none"
404 )]
405 pub stop_price: Option<Decimal>,
406 #[serde(rename = "type")]
407 pub order_type: KrakenFuturesOrderType,
408 pub order_id: String,
409 #[serde(default)]
410 pub cli_ord_id: Option<String>,
411 pub direction: i32,
413 #[serde(default)]
414 pub reduce_only: bool,
415 #[serde(default, rename = "triggerSignal")]
416 pub trigger_signal: Option<String>,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct KrakenFuturesOpenOrdersSnapshot {
422 pub feed: KrakenFuturesFeed,
423 #[serde(default)]
424 pub account: Option<String>,
425 pub orders: Vec<KrakenFuturesOpenOrder>,
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
431pub struct KrakenFuturesOpenOrdersDelta {
432 pub feed: KrakenFuturesFeed,
433 pub order: KrakenFuturesOpenOrder,
434 pub is_cancel: bool,
435 #[serde(default)]
436 pub reason: Option<String>,
437}
438
439impl KrakenFuturesOpenOrdersDelta {
440 #[must_use]
448 pub fn is_fill_driven_cancel(&self) -> bool {
449 self.is_cancel && matches!(self.reason.as_deref(), Some("full_fill" | "partial_fill"))
450 }
451
452 #[must_use]
457 pub fn is_partial_fill_removal(&self) -> bool {
458 self.is_cancel && self.reason.as_deref() == Some("partial_fill")
459 }
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize)]
465pub struct KrakenFuturesOpenOrdersCancel {
466 pub feed: KrakenFuturesFeed,
467 pub order_id: String,
468 pub cli_ord_id: Option<String>,
469 pub is_cancel: bool,
470 #[serde(default)]
471 pub reason: Option<String>,
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct KrakenFuturesFill {
477 #[serde(alias = "product_id")]
478 pub instrument: Option<Ustr>,
479 pub time: i64,
480 #[serde(with = "decimal")]
481 pub price: Decimal,
482 #[serde(with = "decimal")]
483 pub qty: Decimal,
484 pub order_id: String,
485 #[serde(default)]
486 pub cli_ord_id: Option<String>,
487 pub fill_id: String,
488 pub fill_type: KrakenFillType,
489 pub buy: bool,
491 #[serde(default, with = "optional_decimal")]
492 pub fee_paid: Option<Decimal>,
493 #[serde(default)]
494 pub fee_currency: Option<String>,
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct KrakenFuturesFillsSnapshot {
500 pub feed: KrakenFuturesFeed,
501 #[serde(default)]
502 pub account: Option<String>,
503 pub fills: Vec<KrakenFuturesFill>,
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize)]
509pub struct KrakenFuturesFillsDelta {
510 pub feed: KrakenFuturesFeed,
511 #[serde(default)]
512 pub username: Option<String>,
513 pub fills: Vec<KrakenFuturesFill>,
514}
515
516#[cfg(test)]
517mod tests {
518 use rstest::rstest;
519 use rust_decimal_macros::dec;
520
521 use super::*;
522
523 fn test_api_key() -> String {
524 ["api-key-", "12345678"].concat()
525 }
526
527 #[rstest]
528 fn test_challenge_request_serialization_redaction_and_zeroization() {
529 let mut request = KrakenFuturesChallengeRequest {
530 event: KrakenFuturesEvent::Challenge,
531 api_key: SecretString::from(test_api_key()),
532 };
533 let wire = serde_json::to_value(&request).unwrap();
534 let debug = format!("{request:?}");
535
536 assert_eq!(wire["event"], "challenge");
537 assert_eq!(wire["api_key"], "api-key-12345678");
538 assert!(debug.contains(REDACTED));
539 assert!(!debug.contains("api-key-12345678"));
540
541 request.zeroize();
542 assert!(request.api_key.expose_secret().is_empty());
543 }
544
545 #[rstest]
546 fn test_private_request_serialization_redaction_and_zeroization() {
547 let mut request = KrakenFuturesPrivateSubscribeRequest {
548 event: KrakenFuturesEvent::Subscribe,
549 feed: KrakenFuturesFeed::OpenOrders,
550 api_key: SecretString::from(test_api_key()),
551 original_challenge: SecretString::from("original-challenge"),
552 signed_challenge: SecretString::from("signed-challenge"),
553 };
554 let wire = serde_json::to_value(&request).unwrap();
555 let debug = format!("{request:?}");
556
557 assert_eq!(wire["event"], "subscribe");
558 assert_eq!(wire["feed"], "open_orders");
559 assert_eq!(wire["api_key"], "api-key-12345678");
560 assert_eq!(wire["original_challenge"], "original-challenge");
561 assert_eq!(wire["signed_challenge"], "signed-challenge");
562 assert_eq!(debug.matches(REDACTED).count(), 3);
563 assert!(!debug.contains("api-key-12345678"));
564 assert!(!debug.contains("original-challenge"));
565 assert!(!debug.contains("signed-challenge"));
566
567 request.zeroize();
568 assert!(request.api_key.expose_secret().is_empty());
569 assert!(request.original_challenge.expose_secret().is_empty());
570 assert!(request.signed_challenge.expose_secret().is_empty());
571 }
572
573 #[rstest]
574 fn test_deserialize_ticker_data() {
575 let json = r#"{
577 "feed": "ticker",
578 "product_id": "PI_XBTUSD",
579 "time": 1700000000000,
580 "bid": 90650.5,
581 "ask": 90651.0,
582 "bid_size": 10.5,
583 "ask_size": 8.2,
584 "last": 90650.8,
585 "volume": 1234567.89,
586 "index": 90648.5,
587 "markPrice": 90649.2,
588 "funding_rate": 0.0001,
589 "openInterest": 50000000.0
590 }"#;
591
592 let ticker: KrakenFuturesTickerData = serde_json::from_str(json).unwrap();
593 assert_eq!(ticker.feed, KrakenFuturesFeed::Ticker);
594 assert_eq!(ticker.product_id, Ustr::from("PI_XBTUSD"));
595 assert_eq!(ticker.bid, Some(dec!(90650.5)));
596 assert_eq!(ticker.ask, Some(dec!(90651)));
597 assert_eq!(ticker.index, Some(dec!(90648.5)));
598 assert_eq!(ticker.mark_price, Some(dec!(90649.2)));
599 assert_eq!(ticker.funding_rate, Some(dec!(0.0001)));
600 }
601
602 #[rstest]
603 fn test_serialize_subscribe_request() {
604 let request = KrakenFuturesRequest {
605 event: KrakenFuturesEvent::Subscribe,
606 feed: KrakenFuturesFeed::Ticker,
607 product_ids: vec!["PI_XBTUSD".to_string()],
608 };
609
610 let json = serde_json::to_string(&request).unwrap();
611 assert!(json.contains("\"event\":\"subscribe\""));
612 assert!(json.contains("\"feed\":\"ticker\""));
613 assert!(json.contains("PI_XBTUSD"));
614 }
615
616 #[rstest]
617 fn test_deserialize_ticker_from_fixture() {
618 let json = include_str!("../../../test_data/ws_futures_ticker.json");
619 let ticker: KrakenFuturesTickerData = serde_json::from_str(json).unwrap();
620
621 assert_eq!(ticker.feed, KrakenFuturesFeed::Ticker);
622 assert_eq!(ticker.product_id, Ustr::from("PI_XBTUSD"));
623 assert_eq!(ticker.bid, Some(dec!(21978.5)));
624 assert_eq!(ticker.ask, Some(dec!(21987)));
625 assert_eq!(ticker.bid_size, Some(dec!(2536)));
626 assert_eq!(ticker.ask_size, Some(dec!(13948)));
627 assert_eq!(ticker.index, Some(dec!(21984.54)));
628 assert_eq!(ticker.mark_price, Some(dec!(21979.68641534714)));
629 assert!(ticker.funding_rate.is_some());
630 }
631
632 #[rstest]
633 fn test_deserialize_trade_from_fixture() {
634 let json = include_str!("../../../test_data/ws_futures_trade.json");
635 let trade: KrakenFuturesTradeData = serde_json::from_str(json).unwrap();
636
637 assert_eq!(trade.feed, KrakenFuturesFeed::Trade);
638 assert_eq!(trade.product_id, Ustr::from("PI_XBTUSD"));
639 assert_eq!(trade.side, KrakenOrderSide::Sell);
640 assert_eq!(trade.qty, dec!(15000));
641 assert_eq!(trade.price, dec!(34969.5));
642 assert_eq!(trade.seq, 653355);
643 }
644
645 #[rstest]
646 fn test_deserialize_trade_snapshot_from_fixture() {
647 let json = include_str!("../../../test_data/ws_futures_trade_snapshot.json");
648 let snapshot: KrakenFuturesTradeSnapshot = serde_json::from_str(json).unwrap();
649
650 assert_eq!(snapshot.feed, KrakenFuturesFeed::TradeSnapshot);
651 assert_eq!(snapshot.product_id, Ustr::from("PI_XBTUSD"));
652 assert_eq!(snapshot.trades.len(), 2);
653 assert_eq!(snapshot.trades[0].price, dec!(34893));
654 assert_eq!(snapshot.trades[1].price, dec!(34891));
655 }
656
657 #[rstest]
658 fn test_deserialize_book_snapshot_from_fixture() {
659 let json = include_str!("../../../test_data/ws_futures_book_snapshot.json");
660 let snapshot: KrakenFuturesBookSnapshot = serde_json::from_str(json).unwrap();
661
662 assert_eq!(snapshot.feed, KrakenFuturesFeed::BookSnapshot);
663 assert_eq!(snapshot.product_id, Ustr::from("PI_XBTUSD"));
664 assert_eq!(snapshot.bids.len(), 2);
665 assert_eq!(snapshot.asks.len(), 2);
666 assert_eq!(snapshot.bids[0].price, dec!(34892.5));
667 assert_eq!(snapshot.asks[0].price, dec!(34911.5));
668 }
669
670 #[rstest]
671 fn test_deserialize_book_delta_from_fixture() {
672 let json = include_str!("../../../test_data/ws_futures_book_delta.json");
673 let delta: KrakenFuturesBookDelta = serde_json::from_str(json).unwrap();
674
675 assert_eq!(delta.feed, KrakenFuturesFeed::Book);
676 assert_eq!(delta.product_id, Ustr::from("PI_XBTUSD"));
677 assert_eq!(delta.side, KrakenOrderSide::Sell);
678 assert_eq!(delta.price, dec!(34981));
679 assert_eq!(delta.qty, Decimal::ZERO); }
681
682 #[rstest]
683 fn test_deserialize_open_orders_snapshot_from_fixture() {
684 let json = include_str!("../../../test_data/ws_futures_open_orders_snapshot.json");
685 let snapshot: KrakenFuturesOpenOrdersSnapshot = serde_json::from_str(json).unwrap();
686
687 assert_eq!(snapshot.feed, KrakenFuturesFeed::OpenOrdersSnapshot);
688 assert_eq!(snapshot.orders.len(), 1);
689 assert_eq!(snapshot.orders[0].instrument, Ustr::from("PI_XBTUSD"));
690 assert_eq!(snapshot.orders[0].qty, dec!(1000));
691 assert_eq!(
692 snapshot.orders[0].order_type,
693 KrakenFuturesOrderType::StopLower
694 );
695 }
696
697 #[rstest]
698 fn test_deserialize_open_orders_delta_from_fixture() {
699 let json = include_str!("../../../test_data/ws_futures_open_orders_delta.json");
700 let delta: KrakenFuturesOpenOrdersDelta = serde_json::from_str(json).unwrap();
701
702 assert_eq!(delta.feed, KrakenFuturesFeed::OpenOrders);
703 assert!(!delta.is_cancel);
704 assert_eq!(delta.order.instrument, Ustr::from("PI_XBTUSD"));
705 assert_eq!(delta.order.qty, dec!(304));
706 assert_eq!(delta.order.limit_price, Some(dec!(10640)));
707 assert_eq!(delta.order.stop_price, None);
711 }
712
713 #[rstest]
714 fn test_deserialize_open_orders_delta_full_fill_is_fill_driven_cancel() {
715 let json = include_str!("../../../test_data/ws_futures_open_orders_delta_full_fill.json");
720 let delta: KrakenFuturesOpenOrdersDelta = serde_json::from_str(json).unwrap();
721
722 assert!(delta.is_cancel);
723 assert_eq!(delta.reason.as_deref(), Some("full_fill"));
724 assert_eq!(delta.order.qty, Decimal::ZERO);
725 assert_eq!(delta.order.filled, dec!(0.0001));
726 assert!(delta.is_fill_driven_cancel());
727 }
728
729 #[rstest]
730 #[case::placement(false, None, false)]
731 #[case::user_cancel(true, Some("cancelled_by_user"), false)]
732 #[case::post_only_reject(true, Some("post_order_failed_because_it_would_filled"), false)]
733 #[case::full_fill(true, Some("full_fill"), true)]
734 #[case::partial_fill(true, Some("partial_fill"), true)]
735 #[case::cancel_no_reason(true, None, false)]
736 #[case::ioc_not_executed(
740 true,
741 Some("ioc_order_failed_because_it_would_not_be_executed"),
742 false
743 )]
744 #[case::ioc_would_enter_book(true, Some("IOC_WOULD_ENTER_BOOK"), false)]
745 #[case::cancelled_by_self_trade(true, Some("CANCELLED_BY_SELF_TRADE"), false)]
746 fn test_open_orders_delta_is_fill_driven_cancel(
747 #[case] is_cancel: bool,
748 #[case] reason: Option<&'static str>,
749 #[case] expected: bool,
750 ) {
751 let delta = KrakenFuturesOpenOrdersDelta {
752 feed: KrakenFuturesFeed::OpenOrders,
753 order: KrakenFuturesOpenOrder {
754 instrument: Ustr::from("PF_XBTUSD"),
755 time: 0,
756 last_update_time: 0,
757 qty: dec!(0.0001),
758 filled: Decimal::ZERO,
759 limit_price: Some(dec!(70000)),
760 stop_price: None,
761 order_type: KrakenFuturesOrderType::Limit,
762 order_id: "test".to_string(),
763 cli_ord_id: None,
764 direction: 0,
765 reduce_only: false,
766 trigger_signal: None,
767 },
768 is_cancel,
769 reason: reason.map(str::to_string),
770 };
771
772 assert_eq!(delta.is_fill_driven_cancel(), expected);
773 }
774
775 #[rstest]
776 #[case::partial_removal(true, Some("partial_fill"), true)]
777 #[case::resting_partial_fill(false, Some("partial_fill"), false)]
778 #[case::full_fill(true, Some("full_fill"), false)]
779 #[case::user_cancel(true, Some("cancelled_by_user"), false)]
780 fn test_open_orders_delta_is_partial_fill_removal(
781 #[case] is_cancel: bool,
782 #[case] reason: Option<&'static str>,
783 #[case] expected: bool,
784 ) {
785 let delta = KrakenFuturesOpenOrdersDelta {
786 feed: KrakenFuturesFeed::OpenOrders,
787 order: KrakenFuturesOpenOrder {
788 instrument: Ustr::from("PF_XBTUSD"),
789 time: 0,
790 last_update_time: 0,
791 qty: dec!(0.0001),
792 filled: Decimal::ZERO,
793 limit_price: Some(dec!(70000)),
794 stop_price: None,
795 order_type: KrakenFuturesOrderType::Limit,
796 order_id: "test".to_string(),
797 cli_ord_id: None,
798 direction: 0,
799 reduce_only: false,
800 trigger_signal: None,
801 },
802 is_cancel,
803 reason: reason.map(str::to_string),
804 };
805
806 assert_eq!(delta.is_partial_fill_removal(), expected);
807 }
808
809 #[rstest]
810 fn test_deserialize_open_orders_cancel_from_fixture() {
811 let json = include_str!("../../../test_data/ws_futures_open_orders_cancel.json");
812 let cancel: KrakenFuturesOpenOrdersCancel = serde_json::from_str(json).unwrap();
813
814 assert_eq!(cancel.feed, KrakenFuturesFeed::OpenOrders);
815 assert!(cancel.is_cancel);
816 assert_eq!(cancel.order_id, "660c6b23-8007-48c1-a7c9-4893f4572e8c");
817 assert_eq!(cancel.reason, Some("cancelled_by_user".to_string()));
818 assert!(cancel.cli_ord_id.is_none()); }
820
821 #[rstest]
822 fn test_deserialize_fills_snapshot_from_fixture() {
823 let json = include_str!("../../../test_data/ws_futures_fills_snapshot.json");
824 let snapshot: KrakenFuturesFillsSnapshot = serde_json::from_str(json).unwrap();
825
826 assert_eq!(snapshot.feed, KrakenFuturesFeed::FillsSnapshot);
827 assert_eq!(snapshot.fills.len(), 2);
828 assert_eq!(
829 snapshot.fills[0].instrument,
830 Some(Ustr::from("FI_XBTUSD_200925"))
831 );
832 assert!(snapshot.fills[0].buy);
833 assert_eq!(snapshot.fills[0].fill_type, KrakenFillType::Maker);
834 }
835
836 #[rstest]
837 fn test_classify_ticker_message() {
838 let json = include_str!("../../../test_data/ws_futures_ticker.json");
839 let value: Value = serde_json::from_str(json).unwrap();
840 assert_eq!(
841 classify_futures_message(&value),
842 KrakenFuturesMessageType::Ticker
843 );
844 }
845
846 #[rstest]
847 fn test_classify_trade_message() {
848 let json = include_str!("../../../test_data/ws_futures_trade.json");
849 let value: Value = serde_json::from_str(json).unwrap();
850 assert_eq!(
851 classify_futures_message(&value),
852 KrakenFuturesMessageType::Trade
853 );
854 }
855
856 #[rstest]
857 fn test_classify_trade_snapshot_message() {
858 let json = include_str!("../../../test_data/ws_futures_trade_snapshot.json");
859 let value: Value = serde_json::from_str(json).unwrap();
860 assert_eq!(
861 classify_futures_message(&value),
862 KrakenFuturesMessageType::TradeSnapshot
863 );
864 }
865
866 #[rstest]
867 fn test_classify_book_snapshot_message() {
868 let json = include_str!("../../../test_data/ws_futures_book_snapshot.json");
869 let value: Value = serde_json::from_str(json).unwrap();
870 assert_eq!(
871 classify_futures_message(&value),
872 KrakenFuturesMessageType::BookSnapshot
873 );
874 }
875
876 #[rstest]
877 fn test_classify_book_delta_message() {
878 let json = include_str!("../../../test_data/ws_futures_book_delta.json");
879 let value: Value = serde_json::from_str(json).unwrap();
880 assert_eq!(
881 classify_futures_message(&value),
882 KrakenFuturesMessageType::BookDelta
883 );
884 }
885
886 #[rstest]
887 fn test_classify_open_orders_delta_message() {
888 let json = include_str!("../../../test_data/ws_futures_open_orders_delta.json");
889 let value: Value = serde_json::from_str(json).unwrap();
890 assert_eq!(
891 classify_futures_message(&value),
892 KrakenFuturesMessageType::OpenOrdersDelta
893 );
894 }
895
896 #[rstest]
897 fn test_classify_open_orders_cancel_message() {
898 let json = include_str!("../../../test_data/ws_futures_open_orders_cancel.json");
899 let value: Value = serde_json::from_str(json).unwrap();
900 assert_eq!(
901 classify_futures_message(&value),
902 KrakenFuturesMessageType::OpenOrdersCancel
903 );
904 }
905
906 #[rstest]
907 fn test_classify_heartbeat_message() {
908 let json = r#"{"feed":"heartbeat","time":1700000000000}"#;
909 let value: Value = serde_json::from_str(json).unwrap();
910 assert_eq!(
911 classify_futures_message(&value),
912 KrakenFuturesMessageType::Heartbeat
913 );
914 }
915
916 #[rstest]
917 fn test_classify_info_event() {
918 let json = r#"{"event":"info","version":1}"#;
919 let value: Value = serde_json::from_str(json).unwrap();
920 assert_eq!(
921 classify_futures_message(&value),
922 KrakenFuturesMessageType::Info
923 );
924 }
925
926 #[rstest]
927 fn test_classify_subscribed_event() {
928 let json = r#"{"event":"subscribed","feed":"ticker","product_ids":["PI_XBTUSD"]}"#;
929 let value: Value = serde_json::from_str(json).unwrap();
930 assert_eq!(
931 classify_futures_message(&value),
932 KrakenFuturesMessageType::Subscribed
933 );
934 }
935
936 #[rstest]
937 fn test_classify_error_event() {
938 let json = r#"{"event":"error","message":"Unknown product_id"}"#;
939 let value: Value = serde_json::from_str(json).unwrap();
940 assert_eq!(
941 classify_futures_message(&value),
942 KrakenFuturesMessageType::Error
943 );
944 }
945
946 #[rstest]
947 fn test_classify_alert_event() {
948 let json = r#"{"event":"alert","message":"Rate limit exceeded"}"#;
949 let value: Value = serde_json::from_str(json).unwrap();
950 assert_eq!(
951 classify_futures_message(&value),
952 KrakenFuturesMessageType::Alert
953 );
954 }
955}