1use ahash::AHashMap;
19use nautilus_core::{UnixNanos, datetime::unix_nanos_to_iso8601_millis};
20use rust_decimal::Decimal;
21use serde::{Deserialize, Serialize};
22
23use crate::{
24 common::{
25 enums::{
26 KrakenApiResult, KrakenFillType, KrakenFuturesHistoryDirection,
27 KrakenFuturesHistoryOrderType, KrakenFuturesOrderEventType,
28 KrakenFuturesOrderLifecycleStatus, KrakenFuturesOrderStatus, KrakenFuturesOrderType,
29 KrakenInstrumentType, KrakenOrderSide, KrakenPositionSide, KrakenSendStatus,
30 KrakenTriggerSide, KrakenTriggerSignal,
31 },
32 serialization::{
33 decimal, decimal_map, deserialize_decimal_pair, optional_decimal,
34 optional_decimal_or_empty,
35 },
36 },
37 http::error::KrakenHttpError,
38};
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct FuturesMarginLevel {
45 #[serde(alias = "numNonContractUnits", default, with = "decimal")]
48 pub contracts: Decimal,
49 #[serde(with = "decimal")]
50 pub initial_margin: Decimal,
51 #[serde(with = "decimal")]
52 pub maintenance_margin: Decimal,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub struct FuturesInstrument {
58 pub symbol: String,
59 #[serde(rename = "type")]
60 pub instrument_type: KrakenInstrumentType,
61 #[serde(default)]
63 pub underlying: Option<String>,
64 #[serde(with = "decimal")]
65 pub tick_size: Decimal,
66 #[serde(with = "decimal")]
67 pub contract_size: Decimal,
68 pub tradeable: bool,
69 #[serde(default, with = "optional_decimal")]
70 pub impact_mid_size: Option<Decimal>,
71 #[serde(default, with = "optional_decimal")]
72 pub max_position_size: Option<Decimal>,
73 pub opening_date: String,
74 pub margin_levels: Vec<FuturesMarginLevel>,
75 #[serde(default)]
76 pub funding_rate_coefficient: Option<i32>,
77 #[serde(default, with = "optional_decimal")]
78 pub max_relative_funding_rate: Option<Decimal>,
79 #[serde(default)]
80 pub isin: Option<String>,
81 pub contract_value_trade_precision: i32,
82 pub post_only: bool,
83 #[serde(default)]
88 pub maker_protection_millis: Option<i64>,
89 #[serde(default)]
90 pub fee_schedule_uid: Option<String>,
91 pub mtf: bool,
92 pub base: String,
93 pub quote: String,
94 pub pair: String,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct FuturesInstrumentsResponse {
99 pub result: KrakenApiResult,
100 pub instruments: Vec<FuturesInstrument>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub struct FuturesTicker {
108 pub symbol: String,
109 #[serde(default, with = "optional_decimal")]
110 pub last: Option<Decimal>,
111 #[serde(default)]
112 pub last_time: Option<String>,
113 pub tag: String,
114 pub pair: String,
115 #[serde(default, with = "optional_decimal")]
116 pub mark_price: Option<Decimal>,
117 #[serde(default, with = "optional_decimal")]
118 pub bid: Option<Decimal>,
119 #[serde(default, with = "optional_decimal")]
120 pub bid_size: Option<Decimal>,
121 #[serde(default, with = "optional_decimal")]
122 pub ask: Option<Decimal>,
123 #[serde(default, with = "optional_decimal")]
124 pub ask_size: Option<Decimal>,
125 #[serde(rename = "vol24h", default, with = "optional_decimal")]
126 pub vol_24h: Option<Decimal>,
127 #[serde(default, with = "optional_decimal")]
128 pub volume_quote: Option<Decimal>,
129 #[serde(default, with = "optional_decimal")]
130 pub open_interest: Option<Decimal>,
131 #[serde(rename = "open24h", default, with = "optional_decimal")]
132 pub open_24h: Option<Decimal>,
133 #[serde(rename = "high24h", default, with = "optional_decimal")]
134 pub high_24h: Option<Decimal>,
135 #[serde(rename = "low24h", default, with = "optional_decimal")]
136 pub low_24h: Option<Decimal>,
137 #[serde(default, with = "optional_decimal")]
138 pub last_size: Option<Decimal>,
139 #[serde(default, with = "optional_decimal")]
140 pub funding_rate: Option<Decimal>,
141 #[serde(default, with = "optional_decimal")]
142 pub funding_rate_prediction: Option<Decimal>,
143 #[serde(default)]
144 pub suspended: bool,
145 #[serde(default, with = "optional_decimal")]
146 pub index_price: Option<Decimal>,
147 #[serde(default)]
148 pub post_only: bool,
149 #[serde(rename = "change24h", default, with = "optional_decimal")]
150 pub change_24h: Option<Decimal>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154#[serde(rename_all = "camelCase")]
155pub struct FuturesTickersResponse {
156 pub result: KrakenApiResult,
157 #[serde(default)]
158 pub server_time: Option<String>,
159 pub tickers: Vec<FuturesTicker>,
160}
161
162#[derive(Debug, Clone, Serialize)]
166pub struct FuturesOrderBookLevel {
167 #[serde(serialize_with = "decimal::serialize")]
168 pub price: Decimal,
169 #[serde(serialize_with = "decimal::serialize")]
170 pub qty: Decimal,
171}
172
173impl<'de> serde::Deserialize<'de> for FuturesOrderBookLevel {
174 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
175 where
176 D: serde::Deserializer<'de>,
177 {
178 let arr = deserialize_decimal_pair(deserializer)?;
179 Ok(Self {
180 price: arr.0,
181 qty: arr.1,
182 })
183 }
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
187#[serde(rename_all = "camelCase")]
188pub struct FuturesOrderBookData {
189 pub bids: Vec<FuturesOrderBookLevel>,
190 pub asks: Vec<FuturesOrderBookLevel>,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
194#[serde(rename_all = "camelCase")]
195pub struct FuturesOrderBookResponse {
196 pub result: KrakenApiResult,
197 pub order_book: FuturesOrderBookData,
198 #[serde(default)]
199 pub server_time: Option<String>,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
205#[serde(rename_all = "camelCase")]
206pub struct FuturesHistoricalFundingRate {
207 pub timestamp: String,
208 #[serde(with = "decimal")]
209 pub relative_funding_rate: Decimal,
210 #[serde(with = "decimal")]
211 pub funding_rate: Decimal,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct FuturesHistoricalFundingRatesResponse {
217 pub result: KrakenApiResult,
218 pub rates: Vec<FuturesHistoricalFundingRate>,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct FuturesCandle {
225 pub time: i64,
226 pub open: String,
227 pub high: String,
228 pub low: String,
229 pub close: String,
230 pub volume: String,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct FuturesCandlesResponse {
235 pub candles: Vec<FuturesCandle>,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
241#[serde(rename_all = "camelCase")]
242pub struct FuturesOpenOrder {
243 #[serde(rename = "order_id")]
244 pub order_id: String,
245 pub symbol: String,
246 pub side: KrakenOrderSide,
247 pub order_type: KrakenFuturesOrderType,
248 #[serde(default, with = "optional_decimal")]
249 pub limit_price: Option<Decimal>,
250 #[serde(default, with = "optional_decimal")]
251 pub stop_price: Option<Decimal>,
252 #[serde(default, with = "optional_decimal")]
253 pub unfilled_size: Option<Decimal>,
254 pub received_time: String,
255 pub status: KrakenFuturesOrderStatus,
256 #[serde(with = "decimal")]
257 pub filled_size: Decimal,
258 #[serde(default)]
259 pub reduce_only: Option<bool>,
260 pub last_update_time: String,
261 #[serde(default)]
262 pub trigger_signal: Option<KrakenTriggerSignal>,
263 #[serde(rename = "cli_ord_id", default)]
264 pub cli_ord_id: Option<String>,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
268#[serde(rename_all = "camelCase")]
269pub struct FuturesOpenOrdersResponse {
270 pub result: KrakenApiResult,
271 #[serde(default)]
272 pub server_time: Option<String>,
273 #[serde(default)]
274 pub error: Option<String>,
275 #[serde(default)]
276 pub open_orders: Vec<FuturesOpenOrder>,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
283#[serde(rename_all = "camelCase")]
284pub struct FuturesCachedOrder {
285 pub order_id: String,
286 #[serde(default)]
287 pub cli_ord_id: Option<String>,
288 pub symbol: String,
289 pub side: KrakenOrderSide,
290 #[serde(default, with = "optional_decimal")]
291 pub quantity: Option<Decimal>,
292 #[serde(default, with = "optional_decimal")]
293 pub filled: Option<Decimal>,
294 #[serde(default, with = "optional_decimal")]
295 pub limit_price: Option<Decimal>,
296 #[serde(default)]
297 pub reduce_only: bool,
298 pub timestamp: String,
299 pub last_update_timestamp: String,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
304#[serde(rename_all = "camelCase")]
305pub struct FuturesOrderStatusDetails {
306 pub order: FuturesCachedOrder,
307 pub status: KrakenFuturesOrderLifecycleStatus,
308 #[serde(default)]
309 pub update_reason: Option<String>,
310 #[serde(default)]
311 pub error: Option<String>,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
317#[serde(rename_all = "camelCase")]
318pub struct FuturesOrdersStatusResponse {
319 pub result: KrakenApiResult,
320 #[serde(default)]
321 pub server_time: Option<String>,
322 #[serde(default)]
323 pub error: Option<String>,
324 #[serde(default)]
325 pub orders: Vec<FuturesOrderStatusDetails>,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct FuturesOrderEventWrapper {
334 pub order: FuturesOrderEvent,
335 #[serde(rename = "type")]
336 pub event_type: KrakenFuturesOrderEventType,
337 #[serde(default, with = "optional_decimal")]
338 pub reduced_quantity: Option<Decimal>,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
343#[serde(rename_all = "camelCase")]
344pub struct FuturesOrderEvent {
345 pub order_id: String,
346 #[serde(default)]
347 pub cli_ord_id: Option<String>,
348 #[serde(rename = "type")]
349 pub order_type: KrakenFuturesOrderType,
350 pub symbol: String,
351 pub side: KrakenOrderSide,
352 #[serde(with = "decimal")]
353 pub quantity: Decimal,
354 #[serde(with = "decimal")]
355 pub filled: Decimal,
356 #[serde(default, with = "optional_decimal")]
357 pub limit_price: Option<Decimal>,
358 #[serde(default, with = "optional_decimal")]
359 pub stop_price: Option<Decimal>,
360 pub timestamp: String,
361 pub last_update_timestamp: String,
362 #[serde(default)]
363 pub reduce_only: bool,
364}
365
366#[derive(Debug, Clone)]
371pub struct FuturesOrderEventsResponse {
372 pub server_time: Option<String>,
373 pub order_events: Vec<FuturesOrderEventWrapper>,
374 pub continuation_token: Option<String>,
375 pub skipped_rows: usize,
377 pub skipped_order_ids: Vec<String>,
379 pub oldest_event_ms: Option<i64>,
383}
384
385#[derive(Debug, Clone, Deserialize)]
394#[serde(rename_all = "camelCase")]
395pub struct FuturesOrderHistoryResponse {
396 #[serde(default)]
397 pub result: Option<KrakenApiResult>,
398 #[serde(default)]
399 pub error: Option<String>,
400 #[serde(default)]
401 pub server_time: Option<String>,
402 #[serde(default)]
403 pub elements: Option<Vec<FuturesOrderHistoryElement>>,
404 #[serde(default)]
405 pub continuation_token: Option<String>,
406}
407
408#[derive(Debug, Clone, Deserialize)]
410#[serde(rename_all = "camelCase")]
411pub struct FuturesOrderHistoryElement {
412 pub uid: String,
413 pub timestamp: i64,
414 pub event: FuturesOrderHistoryEvent,
415}
416
417#[derive(Debug, Clone)]
422pub enum FuturesOrderHistoryEvent {
423 OrderPlaced(FuturesOrderHistoryOrderEvent),
424 OrderUpdated(FuturesOrderHistoryOrderUpdate),
425 OrderCancelled(FuturesOrderHistoryOrderEvent),
426 OrderRejected(FuturesOrderHistoryOrderEvent),
427 OrderEditRejected(FuturesOrderHistoryEditRejected),
429 OrderNotFound(FuturesOrderHistoryOrderNotFound),
431 Unknown(String),
432}
433
434impl<'de> Deserialize<'de> for FuturesOrderHistoryEvent {
435 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
436 where
437 D: serde::Deserializer<'de>,
438 {
439 struct EventVisitor;
440
441 impl<'de> serde::de::Visitor<'de> for EventVisitor {
442 type Value = FuturesOrderHistoryEvent;
443
444 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
445 formatter.write_str("an order history event keyed by its kind")
446 }
447
448 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
449 where
450 A: serde::de::MapAccess<'de>,
451 {
452 let mut event = None;
455 let mut first_key = None;
456
457 while let Some(key) = map.next_key::<String>()? {
458 let known = match key.as_str() {
459 "OrderPlaced" => {
460 Some(FuturesOrderHistoryEvent::OrderPlaced(map.next_value()?))
461 }
462 "OrderUpdated" => {
463 Some(FuturesOrderHistoryEvent::OrderUpdated(map.next_value()?))
464 }
465 "OrderCancelled" => {
466 Some(FuturesOrderHistoryEvent::OrderCancelled(map.next_value()?))
467 }
468 "OrderRejected" => {
469 Some(FuturesOrderHistoryEvent::OrderRejected(map.next_value()?))
470 }
471 "OrderEditRejected" => Some(FuturesOrderHistoryEvent::OrderEditRejected(
472 map.next_value()?,
473 )),
474 "OrderNotFound" => {
475 Some(FuturesOrderHistoryEvent::OrderNotFound(map.next_value()?))
476 }
477 _ => {
478 map.next_value::<serde::de::IgnoredAny>()?;
479 None
480 }
481 };
482
483 if event.is_none() && known.is_some() {
484 event = known;
485 }
486
487 if first_key.is_none() {
488 first_key = Some(key);
489 }
490 }
491
492 Ok(event.unwrap_or_else(|| {
493 FuturesOrderHistoryEvent::Unknown(first_key.unwrap_or_default())
494 }))
495 }
496 }
497
498 deserializer.deserialize_map(EventVisitor)
499 }
500}
501
502#[derive(Debug, Clone, Deserialize)]
504#[serde(rename_all = "camelCase")]
505pub struct FuturesOrderHistoryOrderEvent {
506 pub order: FuturesOrderHistoryOrder,
507}
508
509#[derive(Debug, Clone, Deserialize)]
511#[serde(rename_all = "camelCase")]
512pub struct FuturesOrderHistoryOrderUpdate {
513 pub new_order: FuturesOrderHistoryOrder,
514}
515
516#[derive(Debug, Clone, Deserialize)]
518#[serde(rename_all = "camelCase")]
519pub struct FuturesOrderHistoryEditRejected {
520 pub old_order: FuturesOrderHistoryOrder,
521}
522
523#[derive(Debug, Clone, Deserialize)]
525#[serde(rename_all = "camelCase")]
526pub struct FuturesOrderHistoryOrderNotFound {
527 #[serde(default)]
528 pub order_id: Option<String>,
529}
530
531#[derive(Debug, Clone, Deserialize)]
533#[serde(rename_all = "camelCase")]
534pub struct FuturesOrderHistoryOrder {
535 pub uid: String,
536 pub tradeable: String,
537 pub direction: KrakenFuturesHistoryDirection,
538 #[serde(with = "decimal")]
539 pub quantity: Decimal,
540 #[serde(with = "decimal")]
541 pub filled: Decimal,
542 #[serde(default, deserialize_with = "optional_decimal_or_empty::deserialize")]
544 pub limit_price: Option<Decimal>,
545 #[serde(default)]
546 pub order_type: Option<KrakenFuturesHistoryOrderType>,
547 #[serde(default)]
548 pub client_id: Option<String>,
549 #[serde(default)]
550 pub reduce_only: bool,
551 pub timestamp: i64,
552 pub last_update_timestamp: i64,
553}
554
555fn millis_to_rfc3339(millis: i64) -> Option<String> {
556 let nanos = u64::try_from(millis).ok()?.checked_mul(1_000_000)?;
557 Some(unix_nanos_to_iso8601_millis(UnixNanos::from(nanos)))
558}
559
560impl FuturesOrderHistoryResponse {
561 pub fn into_order_events(self) -> Result<FuturesOrderEventsResponse, KrakenHttpError> {
585 if let Some(error) = self.error {
586 return Err(KrakenHttpError::ApiError(vec![error]));
587 }
588
589 if self.result == Some(KrakenApiResult::Error) {
590 return Err(KrakenHttpError::ApiError(vec![
591 "order history request failed without a reason".to_string(),
592 ]));
593 }
594
595 let Some(elements) = self.elements else {
596 return Err(KrakenHttpError::ParseError(
597 "order history page has no `elements` array".to_string(),
598 ));
599 };
600
601 let mut skipped_rows = 0usize;
602 let mut skipped_order_ids = Vec::new();
603 let oldest_event_ms = elements.iter().map(|element| element.timestamp).min();
604 let order_events = elements
605 .into_iter()
606 .filter_map(|element| {
607 let (order, event_type) = match element.event {
608 FuturesOrderHistoryEvent::OrderPlaced(event) => {
609 (event.order, KrakenFuturesOrderEventType::Place)
610 }
611 FuturesOrderHistoryEvent::OrderUpdated(event) => {
612 (event.new_order, KrakenFuturesOrderEventType::Edit)
613 }
614 FuturesOrderHistoryEvent::OrderCancelled(event) => {
615 (event.order, KrakenFuturesOrderEventType::Cancel)
616 }
617 FuturesOrderHistoryEvent::OrderRejected(event) => {
618 (event.order, KrakenFuturesOrderEventType::Reject)
619 }
620 FuturesOrderHistoryEvent::OrderEditRejected(event) => {
621 (event.old_order, KrakenFuturesOrderEventType::Edit)
622 }
623 FuturesOrderHistoryEvent::OrderNotFound(event) => {
624 log::warn!(
625 "Skipping order history event {} (order not found {:?}): no order state",
626 element.uid,
627 event.order_id
628 );
629 return None;
630 }
631 FuturesOrderHistoryEvent::Unknown(kind) => {
632 log::warn!(
633 "Skipping order history event {} of undocumented kind {kind:?}; the set is incomplete",
634 element.uid
635 );
636 skipped_rows += 1;
637 return None;
638 }
639 };
640
641 let (Some(timestamp), Some(last_update_timestamp)) = (
644 millis_to_rfc3339(order.timestamp),
645 millis_to_rfc3339(order.last_update_timestamp.max(element.timestamp)),
646 ) else {
647 log::warn!(
648 "Skipping order history event {} for order {} on {}: timestamp before the epoch; the set is incomplete",
649 element.uid,
650 order.uid,
651 order.tradeable
652 );
653 skipped_rows += 1;
654 skipped_order_ids.push(order.uid);
655 return None;
656 };
657
658 let side = match order.direction {
659 KrakenFuturesHistoryDirection::Buy => KrakenOrderSide::Buy,
660 KrakenFuturesHistoryDirection::Sell => KrakenOrderSide::Sell,
661 KrakenFuturesHistoryDirection::Unknown => {
662 log::warn!(
663 "Skipping order history event {} for order {} on {}: direction unknown; the set is incomplete",
664 element.uid,
665 order.uid,
666 order.tradeable
667 );
668 skipped_rows += 1;
669 skipped_order_ids.push(order.uid);
670 return None;
671 }
672 };
673
674 let kind = match order.order_type {
677 Some(KrakenFuturesHistoryOrderType::Unknown) | None => {
678 let situation = if order.order_type.is_none() {
679 "order type missing"
680 } else {
681 "order type reported as Unknown"
682 };
683 log::warn!(
684 "Skipping order history event {} for order {} (client id {:?}) on {}: {situation}; the set is incomplete",
685 element.uid,
686 order.uid,
687 order.client_id.as_deref().filter(|id| !id.is_empty()),
688 order.tradeable
689 );
690 skipped_rows += 1;
691 skipped_order_ids.push(order.uid);
692 return None;
693 }
694 Some(kind) => kind,
695 };
696
697 let market_like = matches!(
698 kind,
699 KrakenFuturesHistoryOrderType::Market
700 | KrakenFuturesHistoryOrderType::Liquidation
701 | KrakenFuturesHistoryOrderType::PartialLiquidation
702 | KrakenFuturesHistoryOrderType::CoveredLiquidation
703 | KrakenFuturesHistoryOrderType::Assignment
704 | KrakenFuturesHistoryOrderType::HedgeAssignment
705 | KrakenFuturesHistoryOrderType::Unwind
706 | KrakenFuturesHistoryOrderType::Block
707 | KrakenFuturesHistoryOrderType::Rfq
708 );
709 let limit_price = match kind {
710 _ if market_like => None,
711 KrakenFuturesHistoryOrderType::Stop => {
713 order.limit_price.filter(|price| !price.is_zero())
714 }
715 _ => order.limit_price,
716 };
717 let order_type = match (kind, limit_price) {
718 (KrakenFuturesHistoryOrderType::Stop, Some(_)) => KrakenFuturesOrderType::Limit,
719 (KrakenFuturesHistoryOrderType::Stop, None) => KrakenFuturesOrderType::Market,
720 (kind, _) => kind.into(),
721 };
722
723 Some(FuturesOrderEventWrapper {
724 order: FuturesOrderEvent {
725 order_id: order.uid,
726 cli_ord_id: order.client_id.filter(|id| !id.is_empty()),
727 order_type,
728 symbol: order.tradeable,
729 side,
730 quantity: order.quantity,
731 filled: order.filled,
732 limit_price,
733 stop_price: None,
734 timestamp,
735 last_update_timestamp,
736 reduce_only: order.reduce_only,
737 },
738 event_type,
739 reduced_quantity: None,
740 })
741 })
742 .collect();
743
744 Ok(FuturesOrderEventsResponse {
745 server_time: self.server_time,
746 order_events,
747 continuation_token: self.continuation_token,
748 skipped_rows,
749 skipped_order_ids,
750 oldest_event_ms,
751 })
752 }
753}
754
755#[derive(Debug, Clone, Serialize, Deserialize)]
758#[serde(rename_all = "camelCase")]
759pub struct FuturesFill {
760 #[serde(rename = "fill_id")]
761 pub fill_id: String,
762 pub symbol: String,
763 pub side: KrakenOrderSide,
764 #[serde(rename = "order_id")]
765 pub order_id: String,
766 pub fill_time: String,
767 #[serde(with = "decimal")]
768 pub size: Decimal,
769 #[serde(with = "decimal")]
770 pub price: Decimal,
771 pub fill_type: KrakenFillType,
772 #[serde(rename = "cli_ord_id", default)]
773 pub cli_ord_id: Option<String>,
774 #[serde(rename = "fee_paid", default, with = "optional_decimal")]
775 pub fee_paid: Option<Decimal>,
776 #[serde(rename = "fee_currency", default)]
777 pub fee_currency: Option<String>,
778}
779
780#[derive(Debug, Clone, Serialize, Deserialize)]
781#[serde(rename_all = "camelCase")]
782pub struct FuturesFillsResponse {
783 pub result: KrakenApiResult,
784 #[serde(default)]
785 pub server_time: Option<String>,
786 #[serde(default)]
787 pub error: Option<String>,
788 #[serde(default)]
789 pub fills: Vec<FuturesFill>,
790}
791
792#[derive(Debug, Clone, Serialize, Deserialize)]
795#[serde(rename_all = "camelCase")]
796pub struct FuturesPosition {
797 pub side: KrakenPositionSide,
798 pub symbol: String,
799 #[serde(with = "decimal")]
800 pub price: Decimal,
801 pub fill_time: String,
802 #[serde(with = "decimal")]
803 pub size: Decimal,
804 #[serde(default, with = "optional_decimal")]
805 pub unrealized_funding: Option<Decimal>,
806}
807
808#[derive(Debug, Clone, Serialize, Deserialize)]
809#[serde(rename_all = "camelCase")]
810pub struct FuturesOpenPositionsResponse {
811 pub result: KrakenApiResult,
812 #[serde(default)]
813 pub server_time: Option<String>,
814 #[serde(default)]
815 pub error: Option<String>,
816 #[serde(default)]
817 pub open_positions: Vec<FuturesPosition>,
818}
819
820#[derive(Debug, Clone, Serialize, Deserialize)]
823#[serde(rename_all = "camelCase")]
824pub struct FuturesSendOrderResponse {
825 pub result: KrakenApiResult,
826 #[serde(default)]
827 pub server_time: Option<String>,
828 #[serde(default)]
829 pub error: Option<String>,
830 pub send_status: Option<FuturesSendStatus>,
831}
832
833#[derive(Debug, Clone, Serialize, Deserialize)]
834#[serde(rename_all = "camelCase")]
835pub struct FuturesSendStatus {
836 #[serde(rename = "order_id", default)]
837 pub order_id: Option<String>,
838 #[serde(rename = "order_tag", default)]
839 pub order_tag: Option<String>,
840 pub status: String,
841 #[serde(default)]
842 pub order_events: Option<Vec<FuturesSendOrderEvent>>,
843 #[serde(rename = "cli_ord_id", default)]
844 pub cli_ord_id: Option<String>,
845 #[serde(rename = "receivedTime", default)]
846 pub received_time: Option<String>,
847}
848
849#[derive(Debug, Clone, Serialize, Deserialize)]
850#[serde(rename_all = "camelCase")]
851pub struct FuturesSendOrderEvent {
852 #[serde(rename = "type")]
853 pub event_type: KrakenFuturesOrderEventType,
854 #[serde(default)]
855 pub order: Option<FuturesOrderEventData>,
856 #[serde(default)]
857 pub order_trigger: Option<FuturesOrderTriggerData>,
858 #[serde(default, with = "optional_decimal")]
859 pub reduced_quantity: Option<Decimal>,
860 #[serde(rename = "executionId", default)]
862 pub execution_id: Option<String>,
863 #[serde(default, with = "optional_decimal")]
864 pub price: Option<Decimal>,
865 #[serde(default, with = "optional_decimal")]
866 pub amount: Option<Decimal>,
867 #[serde(rename = "orderPriorEdit", default)]
868 pub order_prior_edit: Option<Box<FuturesOrderEventData>>,
869 #[serde(rename = "orderPriorExecution", default)]
870 pub order_prior_execution: Option<Box<FuturesOrderEventData>>,
871 #[serde(rename = "takerReducedQuantity", default, with = "optional_decimal")]
872 pub taker_reduced_quantity: Option<Decimal>,
873 #[serde(default)]
875 pub reason: Option<String>,
876 #[serde(default)]
877 pub uid: Option<String>,
878}
879
880#[derive(Debug, Clone, Serialize, Deserialize)]
881#[serde(rename_all = "camelCase")]
882pub struct FuturesOrderEventData {
883 #[serde(rename = "orderId")]
884 pub order_id: String,
885 #[serde(rename = "cliOrdId", default)]
886 pub cli_ord_id: Option<String>,
887 #[serde(rename = "type")]
888 pub order_type: KrakenFuturesOrderType,
889 pub symbol: String,
890 pub side: KrakenOrderSide,
891 #[serde(with = "decimal")]
892 pub quantity: Decimal,
893 #[serde(with = "decimal")]
894 pub filled: Decimal,
895 #[serde(rename = "limitPrice", default, with = "optional_decimal")]
896 pub limit_price: Option<Decimal>,
897 #[serde(rename = "stopPrice", default, with = "optional_decimal")]
898 pub stop_price: Option<Decimal>,
899 pub timestamp: String,
900 #[serde(rename = "lastUpdateTimestamp")]
901 pub last_update_timestamp: String,
902 #[serde(rename = "reduceOnly", default)]
903 pub reduce_only: bool,
904}
905
906#[derive(Debug, Clone, Serialize, Deserialize)]
907#[serde(rename_all = "camelCase")]
908pub struct FuturesOrderTriggerData {
909 pub uid: String,
910 #[serde(rename = "clientId", default)]
911 pub client_id: Option<String>,
912 #[serde(rename = "type")]
913 pub order_type: KrakenFuturesOrderType,
914 pub symbol: String,
915 pub side: KrakenOrderSide,
916 #[serde(with = "decimal")]
917 pub quantity: Decimal,
918 #[serde(rename = "limitPrice", default, with = "optional_decimal")]
919 pub limit_price: Option<Decimal>,
920 #[serde(rename = "limitPriceOffsetValue", default, with = "optional_decimal")]
921 pub limit_price_offset_value: Option<Decimal>,
922 #[serde(rename = "limitPriceOffsetUnit", default)]
923 pub limit_price_offset_unit: Option<String>,
924 #[serde(rename = "triggerPrice")]
925 #[serde(with = "decimal")]
926 pub trigger_price: Decimal,
927 #[serde(rename = "triggerSide")]
928 pub trigger_side: KrakenTriggerSide,
929 #[serde(rename = "triggerSignal")]
930 pub trigger_signal: KrakenTriggerSignal,
931 #[serde(rename = "reduceOnly", default)]
932 pub reduce_only: bool,
933 pub timestamp: String,
934 #[serde(rename = "lastUpdateTimestamp")]
935 pub last_update_timestamp: String,
936}
937
938#[derive(Debug, Clone, Serialize, Deserialize)]
939#[serde(rename_all = "camelCase")]
940pub struct FuturesCancelOrderResponse {
941 pub result: KrakenApiResult,
942 #[serde(default)]
943 pub server_time: Option<String>,
944 pub cancel_status: FuturesCancelStatus,
945}
946
947#[derive(Debug, Clone, Serialize, Deserialize)]
948#[serde(rename_all = "camelCase")]
949pub struct FuturesCancelStatus {
950 pub status: KrakenSendStatus,
951 #[serde(rename = "order_id", default)]
952 pub order_id: Option<String>,
953 #[serde(rename = "cli_ord_id", default)]
954 pub cli_ord_id: Option<String>,
955}
956
957#[derive(Debug, Clone, Serialize, Deserialize)]
958#[serde(rename_all = "camelCase")]
959pub struct FuturesEditOrderResponse {
960 pub result: KrakenApiResult,
961 #[serde(default)]
962 pub server_time: Option<String>,
963 pub edit_status: FuturesEditStatus,
964}
965
966#[derive(Debug, Clone, Serialize, Deserialize)]
967#[serde(rename_all = "camelCase")]
968pub struct FuturesEditStatus {
969 pub status: String,
970 #[serde(rename = "order_id", default)]
971 pub order_id: Option<String>,
972 #[serde(rename = "cli_ord_id", default)]
973 pub cli_ord_id: Option<String>,
974}
975
976#[derive(Debug, Clone, Serialize, Deserialize)]
977#[serde(rename_all = "camelCase")]
978pub struct FuturesBatchOrderResponse {
979 pub result: KrakenApiResult,
980 #[serde(default)]
981 pub server_time: Option<String>,
982 pub batch_status: Vec<FuturesSendStatus>,
983}
984
985#[derive(Debug, Clone, Serialize, Deserialize)]
990#[serde(rename_all = "camelCase")]
991pub struct FuturesBatchCancelResponse {
992 pub result: KrakenApiResult,
993 #[serde(default)]
994 pub server_time: Option<String>,
995 #[serde(default)]
996 pub error: Option<String>,
997 #[serde(default)]
998 pub batch_status: Vec<FuturesBatchCancelStatus>,
999}
1000
1001#[derive(Debug, Clone, Serialize, Deserialize)]
1002#[serde(rename_all = "camelCase")]
1003pub struct FuturesBatchCancelStatus {
1004 #[serde(default)]
1005 pub order_id: Option<String>,
1006 #[serde(default)]
1007 pub cli_ord_id: Option<String>,
1008 #[serde(default)]
1009 pub status: Option<KrakenSendStatus>,
1010 #[serde(default)]
1011 pub cancel_status: Option<FuturesCancelStatus>,
1012}
1013
1014#[derive(Debug, Clone, Serialize, Deserialize)]
1015#[serde(rename_all = "camelCase")]
1016pub struct FuturesCancelAllOrdersResponse {
1017 pub result: KrakenApiResult,
1018 #[serde(default)]
1019 pub server_time: Option<String>,
1020 pub cancel_status: FuturesCancelAllStatus,
1021}
1022
1023#[derive(Debug, Clone, Serialize, Deserialize)]
1024#[serde(rename_all = "camelCase")]
1025pub struct FuturesCancelAllStatus {
1026 pub status: KrakenSendStatus,
1027 #[serde(default)]
1028 pub cancelled_orders: Vec<CancelledOrder>,
1029}
1030
1031#[derive(Debug, Clone, Serialize, Deserialize)]
1032#[serde(rename_all = "camelCase")]
1033pub struct CancelledOrder {
1034 #[serde(rename = "order_id", default)]
1035 pub order_id: Option<String>,
1036 #[serde(default)]
1037 pub cli_ord_id: Option<String>,
1038}
1039
1040#[derive(Debug, Clone, Serialize, Deserialize)]
1044#[serde(rename_all = "camelCase")]
1045pub struct FuturesPublicExecutionsResponse {
1046 pub elements: Vec<FuturesPublicExecutionElement>,
1047 #[serde(default)]
1048 pub len: Option<i64>,
1049 #[serde(default)]
1050 pub continuation_token: Option<String>,
1051}
1052
1053#[derive(Debug, Clone, Serialize, Deserialize)]
1055pub struct FuturesPublicExecutionElement {
1056 pub uid: String,
1057 pub timestamp: i64,
1058 pub event: FuturesPublicExecutionEvent,
1059}
1060
1061#[derive(Debug, Clone, Serialize, Deserialize)]
1063pub struct FuturesPublicExecutionEvent {
1064 #[serde(rename = "Execution")]
1065 pub execution: FuturesPublicExecutionWrapper,
1066}
1067
1068#[derive(Debug, Clone, Serialize, Deserialize)]
1070#[serde(rename_all = "camelCase")]
1071pub struct FuturesPublicExecutionWrapper {
1072 pub execution: FuturesPublicExecution,
1073 #[serde(default)]
1074 pub taker_reduced_quantity: Option<String>,
1075}
1076
1077#[derive(Debug, Clone, Serialize, Deserialize)]
1079#[serde(rename_all = "camelCase")]
1080pub struct FuturesPublicExecution {
1081 pub uid: String,
1082 pub maker_order: FuturesPublicOrder,
1083 pub taker_order: FuturesPublicOrder,
1084 pub timestamp: i64,
1085 pub quantity: String,
1086 pub price: String,
1087 #[serde(default)]
1088 pub mark_price: Option<String>,
1089 #[serde(default)]
1090 pub limit_filled: Option<bool>,
1091 #[serde(default)]
1092 pub usd_value: Option<String>,
1093}
1094
1095#[derive(Debug, Clone, Serialize, Deserialize)]
1097#[serde(rename_all = "camelCase")]
1098pub struct FuturesPublicOrder {
1099 pub uid: String,
1100 pub tradeable: String,
1101 pub direction: String,
1102 pub quantity: String,
1103 pub timestamp: i64,
1104 #[serde(default)]
1105 pub limit_price: Option<String>,
1106 #[serde(default)]
1107 pub order_type: Option<String>,
1108 #[serde(default)]
1109 pub reduce_only: Option<bool>,
1110 #[serde(default)]
1111 pub last_update_timestamp: Option<i64>,
1112}
1113
1114#[derive(Debug, Clone, Serialize, Deserialize)]
1118#[serde(rename_all = "camelCase")]
1119pub struct FuturesAccountsResponse {
1120 pub result: KrakenApiResult,
1121 #[serde(default)]
1122 pub accounts: AHashMap<String, FuturesAccount>,
1123 #[serde(default)]
1124 pub error: Option<String>,
1125 #[serde(default)]
1126 pub server_time: Option<String>,
1127}
1128
1129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1131#[serde(rename_all = "camelCase")]
1132pub enum KrakenFuturesAccountType {
1133 MultiCollateralMarginAccount,
1135 MarginAccount,
1137 CashAccount,
1139 #[serde(other)]
1141 Unknown,
1142}
1143
1144#[derive(Debug, Clone, Serialize, Deserialize)]
1146#[serde(rename_all = "camelCase")]
1147pub struct FuturesAccount {
1148 #[serde(rename = "type")]
1149 pub account_type: KrakenFuturesAccountType,
1150 #[serde(default)]
1153 pub currency: Option<String>,
1154 #[serde(default, with = "decimal_map")]
1156 pub balances: AHashMap<String, Decimal>,
1157 #[serde(default)]
1159 pub currencies: AHashMap<String, FuturesFlexCurrency>,
1160 #[serde(default)]
1162 pub auxiliary: Option<FuturesAuxiliary>,
1163 #[serde(default)]
1165 pub margin_requirements: Option<FuturesMarginRequirements>,
1166 #[serde(default, with = "optional_decimal")]
1168 pub portfolio_value: Option<Decimal>,
1169 #[serde(default, with = "optional_decimal")]
1171 pub available_margin: Option<Decimal>,
1172 #[serde(default, with = "optional_decimal")]
1174 pub initial_margin: Option<Decimal>,
1175 #[serde(default, with = "optional_decimal")]
1177 pub maintenance_margin: Option<Decimal>,
1178 #[serde(default, with = "optional_decimal")]
1180 pub pnl: Option<Decimal>,
1181}
1182
1183#[derive(Debug, Clone, Serialize, Deserialize)]
1185#[serde(rename_all = "camelCase")]
1186pub struct FuturesFlexCurrency {
1187 #[serde(with = "decimal")]
1188 pub quantity: Decimal,
1189 #[serde(default, with = "optional_decimal")]
1190 pub value: Option<Decimal>,
1191 #[serde(default, with = "optional_decimal")]
1192 pub collateral: Option<Decimal>,
1193 #[serde(default, with = "optional_decimal")]
1194 pub available: Option<Decimal>,
1195}
1196
1197#[derive(Debug, Clone, Serialize, Deserialize)]
1199#[serde(rename_all = "camelCase")]
1200pub struct FuturesAuxiliary {
1201 #[serde(default, with = "optional_decimal")]
1202 pub usd: Option<Decimal>,
1203 #[serde(default, with = "optional_decimal")]
1205 pub pv: Option<Decimal>,
1206 #[serde(default, with = "optional_decimal")]
1208 pub pnl: Option<Decimal>,
1209 #[serde(default, with = "optional_decimal")]
1211 pub af: Option<Decimal>,
1212 #[serde(default, with = "optional_decimal")]
1213 pub funding: Option<Decimal>,
1214}
1215
1216#[derive(Debug, Clone, Serialize, Deserialize)]
1218#[serde(rename_all = "camelCase")]
1219pub struct FuturesMarginRequirements {
1220 #[serde(default, with = "optional_decimal")]
1222 pub im: Option<Decimal>,
1223 #[serde(default, with = "optional_decimal")]
1225 pub mm: Option<Decimal>,
1226 #[serde(default, with = "optional_decimal")]
1228 pub lt: Option<Decimal>,
1229 #[serde(default, with = "optional_decimal")]
1231 pub tt: Option<Decimal>,
1232}
1233
1234#[cfg(test)]
1235mod tests {
1236 use rstest::rstest;
1237 use rust_decimal_macros::dec;
1238
1239 use super::*;
1240
1241 fn load_test_data(filename: &str) -> String {
1242 let path = format!("test_data/{filename}");
1243 std::fs::read_to_string(&path)
1244 .unwrap_or_else(|e| panic!("Failed to load test data from {path}: {e}"))
1245 }
1246
1247 #[rstest]
1248 fn test_parse_futures_cancel_all_orders_with_no_orders_to_cancel_status() {
1249 let raw = r#"{
1254 "result": "success",
1255 "cancelStatus": {
1256 "receivedTime": "2026-04-10T13:17:23.291Z",
1257 "cancelOnly": "PF_XBTUSD",
1258 "status": "noOrdersToCancel",
1259 "cancelledOrders": [
1260 {
1261 "order_id": "a182b1c0-cd01-4d1c-853b-605e936f412b",
1262 "cliOrdId": "5f173994-f660-4809-b97a-586221fe5926"
1263 }
1264 ],
1265 "orderEvents": []
1266 },
1267 "serverTime": "2026-04-10T13:17:23.291Z"
1268 }"#;
1269
1270 let response: FuturesCancelAllOrdersResponse =
1271 serde_json::from_str(raw).expect("Failed to parse cancel-all response");
1272
1273 assert_eq!(response.result, KrakenApiResult::Success);
1274 assert_eq!(
1275 response.cancel_status.status,
1276 KrakenSendStatus::NoOrdersToCancel
1277 );
1278 assert_eq!(response.cancel_status.cancelled_orders.len(), 1);
1279 assert_eq!(
1280 response.cancel_status.cancelled_orders[0]
1281 .order_id
1282 .as_deref(),
1283 Some("a182b1c0-cd01-4d1c-853b-605e936f412b")
1284 );
1285 assert_eq!(
1286 response.cancel_status.cancelled_orders[0]
1287 .cli_ord_id
1288 .as_deref(),
1289 Some("5f173994-f660-4809-b97a-586221fe5926")
1290 );
1291 }
1292
1293 #[rstest]
1294 fn test_parse_futures_open_orders() {
1295 let data = load_test_data("http_futures_open_orders.json");
1296 let response: FuturesOpenOrdersResponse =
1297 serde_json::from_str(&data).expect("Failed to parse futures open orders");
1298
1299 assert_eq!(response.result, KrakenApiResult::Success);
1300 assert_eq!(response.open_orders.len(), 3);
1301
1302 let order = &response.open_orders[0];
1303 assert_eq!(order.order_id, "2ce038ae-c144-4de7-a0f1-82f7f4fca864");
1304 assert_eq!(order.symbol, "PI_ETHUSD");
1305 assert_eq!(order.side, KrakenOrderSide::Buy);
1306 assert_eq!(order.order_type, KrakenFuturesOrderType::Limit);
1307 assert_eq!(order.limit_price, Some(dec!(1200)));
1308 assert_eq!(order.unfilled_size, Some(dec!(100)));
1309 assert_eq!(order.filled_size, dec!(0));
1310
1311 let trigger_order = &response.open_orders[1];
1312 assert_eq!(
1313 trigger_order.order_id,
1314 "c8135f52-2a86-4e26-b629-43cc37da9dbf"
1315 );
1316 assert_eq!(trigger_order.order_type, KrakenFuturesOrderType::TakeProfit);
1317 assert_eq!(trigger_order.symbol, "PI_XBTUSD");
1318 assert_eq!(trigger_order.side, KrakenOrderSide::Buy);
1319 assert_eq!(trigger_order.limit_price, None);
1320 assert_eq!(trigger_order.stop_price, Some(dec!(1880.4)));
1321 assert_eq!(trigger_order.unfilled_size, None);
1322 assert_eq!(trigger_order.received_time, "2023-04-07T15:14:25.995Z");
1323 assert_eq!(trigger_order.status, KrakenFuturesOrderStatus::Untouched);
1324 assert_eq!(trigger_order.filled_size, dec!(0));
1325 assert_eq!(trigger_order.reduce_only, Some(true));
1326 assert_eq!(trigger_order.last_update_time, "2023-04-07T15:14:25.995Z");
1327 assert_eq!(
1328 trigger_order.trigger_signal,
1329 Some(KrakenTriggerSignal::Last)
1330 );
1331 assert_eq!(trigger_order.cli_ord_id, None);
1332 }
1333
1334 #[rstest]
1335 fn test_parse_futures_orders_status() {
1336 let data = load_test_data("http_futures_orders_status.json");
1337 let response: FuturesOrdersStatusResponse =
1338 serde_json::from_str(&data).expect("Failed to parse futures orders status");
1339
1340 assert_eq!(response.result, KrakenApiResult::Success);
1341 assert_eq!(response.orders.len(), 2);
1342
1343 let part_filled = &response.orders[0];
1344 assert_eq!(
1345 part_filled.order.order_id,
1346 "5f6d15a5-8c9e-4b0a-9d3f-5a2b7c8d9e0f"
1347 );
1348 assert_eq!(
1349 part_filled.order.cli_ord_id.as_deref(),
1350 Some("uuid-mp-held-001")
1351 );
1352 assert_eq!(part_filled.order.side, KrakenOrderSide::Buy);
1353 assert_eq!(part_filled.order.quantity, Some(dec!(0.001)));
1354 assert_eq!(part_filled.order.filled, Some(dec!(0.0004)));
1355 assert_eq!(part_filled.order.limit_price, Some(dec!(70000)));
1356 assert!(!part_filled.order.reduce_only);
1357 assert_eq!(
1358 part_filled.status,
1359 KrakenFuturesOrderLifecycleStatus::Cancelled
1360 );
1361 assert_eq!(part_filled.update_reason.as_deref(), Some("PARTIAL_FILL"));
1362
1363 let open = &response.orders[1];
1364 assert_eq!(open.order.quantity, Some(dec!(0.0002)));
1365 assert_eq!(open.order.filled, Some(dec!(0)));
1366 assert!(open.order.reduce_only);
1367 assert_eq!(open.status, KrakenFuturesOrderLifecycleStatus::EnteredBook);
1368 assert_eq!(open.update_reason, None);
1369 }
1370
1371 #[rstest]
1372 fn test_parse_futures_instruments_maker_protection() {
1373 let data = load_test_data("http_futures_instruments_maker_protection.json");
1374 let response: FuturesInstrumentsResponse =
1375 serde_json::from_str(&data).expect("Failed to parse futures instruments");
1376
1377 assert_eq!(response.result, KrakenApiResult::Success);
1378 assert_eq!(response.instruments.len(), 2);
1379
1380 let protected = &response.instruments[0];
1381 assert_eq!(protected.symbol, "PF_ATOMUSD");
1382 assert_eq!(protected.maker_protection_millis, Some(20));
1383
1384 let unprotected = &response.instruments[1];
1387 assert_eq!(unprotected.symbol, "PF_ETHUSD");
1388 assert_eq!(unprotected.maker_protection_millis, None);
1389 }
1390
1391 #[rstest]
1392 fn test_parse_futures_fills() {
1393 let data = load_test_data("http_futures_fills.json");
1394 let response: FuturesFillsResponse =
1395 serde_json::from_str(&data).expect("Failed to parse futures fills");
1396
1397 assert_eq!(response.result, KrakenApiResult::Success);
1398 assert_eq!(response.fills.len(), 3);
1399
1400 let fill = &response.fills[0];
1401 assert_eq!(fill.fill_id, "cad76f07-814e-4dc6-8478-7867407b6bff");
1402 assert_eq!(fill.symbol, "PI_XBTUSD");
1403 assert_eq!(fill.side, KrakenOrderSide::Buy);
1404 assert_eq!(fill.size, dec!(5000));
1405 assert_eq!(fill.price, dec!(27937.5));
1406 assert_eq!(fill.fill_type, KrakenFillType::Maker);
1407 assert_eq!(fill.fee_currency, Some("BTC".to_string()));
1408 assert_eq!(response.fills[1].fill_type, KrakenFillType::Taker);
1409 assert_eq!(response.fills[2].fill_type, KrakenFillType::Assignee);
1410 }
1411
1412 #[rstest]
1413 fn test_parse_futures_open_positions() {
1414 let data = load_test_data("http_futures_open_positions.json");
1415 let response: FuturesOpenPositionsResponse =
1416 serde_json::from_str(&data).expect("Failed to parse futures open positions");
1417
1418 assert_eq!(response.result, KrakenApiResult::Success);
1419 assert_eq!(response.open_positions.len(), 2);
1420
1421 let position = &response.open_positions[0];
1422 assert_eq!(position.side, KrakenPositionSide::Short);
1423 assert_eq!(position.symbol, "PI_XBTUSD");
1424 assert_eq!(position.size, dec!(8000));
1425 assert!(position.unrealized_funding.is_some());
1426 }
1427
1428 #[rstest]
1429 fn test_parse_futures_orderbook() {
1430 let data = load_test_data("http_futures_orderbook.json");
1431 let response: FuturesOrderBookResponse =
1432 serde_json::from_str(&data).expect("Failed to parse futures orderbook");
1433
1434 assert_eq!(response.result, KrakenApiResult::Success);
1435 assert_eq!(response.order_book.bids.len(), 3);
1436 assert_eq!(response.order_book.asks.len(), 3);
1437
1438 let best_bid = &response.order_book.bids[0];
1439 assert_eq!(best_bid.price, dec!(105900));
1440 assert_eq!(best_bid.qty, dec!(0.5));
1441
1442 let best_ask = &response.order_book.asks[0];
1443 assert_eq!(best_ask.price, dec!(105950));
1444 assert_eq!(best_ask.qty, dec!(0.3));
1445 }
1446
1447 #[rstest]
1448 fn test_parse_futures_historical_funding_rates() {
1449 let data = load_test_data("http_futures_historical_funding_rates.json");
1450 let response: FuturesHistoricalFundingRatesResponse =
1451 serde_json::from_str(&data).expect("Failed to parse historical funding rates");
1452
1453 assert_eq!(response.result, KrakenApiResult::Success);
1454 assert_eq!(response.rates.len(), 3);
1455
1456 let rate = &response.rates[0];
1457 assert_eq!(rate.timestamp, "2025-07-11T08:00:00.000Z");
1458 assert_eq!(rate.relative_funding_rate, dec!(0.0001));
1459 assert_eq!(rate.funding_rate, dec!(0.00005));
1460
1461 let negative_rate = &response.rates[1];
1462 assert_eq!(negative_rate.relative_funding_rate, dec!(-0.00005));
1463 }
1464
1465 #[rstest]
1466 fn test_parse_futures_orderbook_preserves_decimal_precision() {
1467 let data = load_test_data("http_futures_orderbook_precision.json");
1468 let level: FuturesOrderBookLevel = serde_json::from_str(&data).unwrap();
1469
1470 assert_eq!(level.price, dec!(0.1234567890123456789012345678));
1471 assert_eq!(level.qty, dec!(123456789.123456789));
1472 }
1473
1474 fn order_history_events(fixture: &str) -> FuturesOrderEventsResponse {
1475 let data = load_test_data(fixture);
1476 order_history_events_from(&data)
1477 }
1478
1479 fn order_history_events_from(data: &str) -> FuturesOrderEventsResponse {
1480 let response: FuturesOrderHistoryResponse =
1481 serde_json::from_str(data).expect("Failed to parse futures order history");
1482 response
1483 .into_order_events()
1484 .expect("the page carries elements")
1485 }
1486
1487 fn placed_element(order: &str) -> String {
1488 format!(
1489 r#"{{"elements":[{{"uid":"e1","timestamp":1680876930250,"event":{{"OrderPlaced":{{"order":{order}}}}}}}]}}"#
1490 )
1491 }
1492
1493 #[rstest]
1495 fn test_parse_futures_order_events_uses_enum_event_type() {
1496 let response = order_history_events("http_futures_order_events.json");
1497
1498 assert_eq!(response.order_events.len(), 3);
1499 assert_eq!(
1500 response.order_events[0].event_type,
1501 KrakenFuturesOrderEventType::Place
1502 );
1503 assert_eq!(
1504 response.order_events[1].event_type,
1505 KrakenFuturesOrderEventType::Edit
1506 );
1507 assert_eq!(
1508 response.order_events[2].event_type,
1509 KrakenFuturesOrderEventType::Cancel
1510 );
1511 assert_eq!(response.continuation_token.as_deref(), Some("simb178"));
1512 }
1513
1514 #[rstest]
1516 fn test_parse_futures_order_events_update_uses_the_new_order() {
1517 let response = order_history_events("http_futures_order_events.json");
1518 let updated = &response.order_events[1].order;
1519
1520 assert_eq!(updated.filled, dec!(10000));
1521 assert_eq!(updated.order_type, KrakenFuturesOrderType::Market);
1522 assert_eq!(updated.limit_price, None, "a zero limit price is no price");
1523 assert_eq!(updated.cli_ord_id, None, "an empty client id is no id");
1524 }
1525
1526 #[rstest]
1529 fn test_parse_futures_order_events_keeps_the_tradeable_and_formats_timestamps() {
1530 let response = order_history_events("http_futures_order_events.json");
1531 let canceled = &response.order_events[2].order;
1532
1533 assert_eq!(canceled.symbol, "pi_xbtusd");
1534 assert_eq!(canceled.timestamp, "2023-04-07T13:00:00.000Z");
1535 assert_eq!(canceled.last_update_timestamp, "2023-04-07T16:00:00.000Z");
1536 assert!(canceled.reduce_only);
1537 assert_eq!(canceled.limit_price, Some(dec!(26000.0)));
1538 assert_eq!(
1539 canceled.stop_price, None,
1540 "the history schema has no trigger price"
1541 );
1542 }
1543
1544 #[rstest]
1547 #[case::with_limit("26000.0", KrakenFuturesOrderType::Limit, Some(dec!(26000.0)))]
1548 #[case::without_limit("0", KrakenFuturesOrderType::Market, None)]
1549 fn test_parse_futures_order_events_reports_a_stop_as_its_triggered_order(
1550 #[case] limit_price: &str,
1551 #[case] expected_type: KrakenFuturesOrderType,
1552 #[case] expected_price: Option<Decimal>,
1553 ) {
1554 let data = placed_element(&format!(
1555 r#"{{"uid":"o1","tradeable":"PI_XBTUSD","direction":"Sell","quantity":"2000","filled":"0","limitPrice":"{limit_price}","orderType":"Stop","clientId":"","reduceOnly":true,"timestamp":1680876930250,"lastUpdateTimestamp":1680876930250}}"#
1556 ));
1557 let response = order_history_events_from(&data);
1558 let order = &response.order_events[0].order;
1559
1560 assert_eq!(order.order_type, expected_type);
1561 assert_eq!(order.limit_price, expected_price);
1562 assert_eq!(order.stop_price, None);
1563 }
1564
1565 #[rstest]
1568 #[case::market_empty("Market", "", None)]
1569 #[case::market_zero("Market", "0", None)]
1570 #[case::limit_zero("Limit", "0", Some(dec!(0)))]
1571 fn test_parse_futures_order_events_reads_the_limit_price_by_order_kind(
1572 #[case] order_type: &str,
1573 #[case] limit_price: &str,
1574 #[case] expected: Option<Decimal>,
1575 ) {
1576 let data = placed_element(&format!(
1577 r#"{{"uid":"o1","tradeable":"PI_XBTUSD","direction":"Buy","quantity":"1","filled":"1","limitPrice":"{limit_price}","orderType":"{order_type}","clientId":"","reduceOnly":false,"timestamp":1680876930250,"lastUpdateTimestamp":1680876930250}}"#
1578 ));
1579 let response = order_history_events_from(&data);
1580
1581 assert_eq!(response.order_events[0].order.limit_price, expected);
1582 }
1583
1584 #[rstest]
1586 fn test_parse_futures_order_events_maps_a_rejection() {
1587 let data = r#"{"elements":[{"uid":"e1","timestamp":1680876930250,"event":{"OrderRejected":{"order":{"uid":"o1","tradeable":"PI_XBTUSD","direction":"Buy","quantity":"1","filled":"0","limitPrice":"70000","orderType":"Limit","clientId":"","reduceOnly":false,"timestamp":1680876930250,"lastUpdateTimestamp":1680876930250},"reason":"insufficient_margin"}}}]}"#;
1588 let response = order_history_events_from(data);
1589
1590 assert_eq!(
1591 response.order_events[0].event_type,
1592 KrakenFuturesOrderEventType::Reject
1593 );
1594 }
1595
1596 #[rstest]
1601 fn test_parse_futures_order_events_skips_an_unknown_order_type_and_keeps_siblings() {
1602 let response = order_history_events("http_futures_order_events_unknown.json");
1603
1604 assert_eq!(response.order_events.len(), 1);
1605 let sibling = &response.order_events[0];
1606 assert_eq!(sibling.event_type, KrakenFuturesOrderEventType::Edit);
1607 assert_eq!(sibling.reduced_quantity, None);
1608 assert_eq!(sibling.order.order_id, "abc");
1609 assert_eq!(sibling.order.cli_ord_id, None);
1610 assert_eq!(sibling.order.order_type, KrakenFuturesOrderType::Limit);
1611 assert_eq!(sibling.order.symbol, "PF_XBTUSD");
1612 assert_eq!(sibling.order.side, KrakenOrderSide::Buy);
1613 assert_eq!(sibling.order.quantity, dec!(1.0));
1614 assert_eq!(sibling.order.filled, dec!(0.5));
1615 assert_eq!(sibling.order.limit_price, Some(dec!(70000.0)));
1616 assert_eq!(sibling.order.stop_price, None);
1617 assert_eq!(sibling.order.timestamp, "2026-05-18T00:00:00.000Z");
1618 assert_eq!(
1619 sibling.order.last_update_timestamp,
1620 "2026-05-18T00:00:01.000Z"
1621 );
1622 assert!(!sibling.order.reduce_only);
1623 assert_eq!(
1624 response.server_time.as_deref(),
1625 Some("2026-05-18T00:00:03.000Z")
1626 );
1627 assert_eq!(response.continuation_token, None);
1628 assert_eq!(
1629 response.skipped_rows, 2,
1630 "the Unknown type and the undocumented kind count; the not-found event does not"
1631 );
1632 }
1633
1634 #[rstest]
1637 #[case::not_found(r#"{"OrderNotFound":{"orderId":"gone"}}"#)]
1638 #[case::undocumented(r#"{"OrderSomethingNew":{"order":{"uid":"x"}}}"#)]
1639 fn test_parse_futures_order_events_oldest_time_counts_every_element(#[case] oldest: &str) {
1640 let kept = r#"{"OrderPlaced":{"order":{"uid":"o1","tradeable":"PF_XBTUSD","direction":"Buy","quantity":"1","filled":"0","limitPrice":"70000","orderType":"Limit","clientId":"","reduceOnly":false,"timestamp":1680876930250,"lastUpdateTimestamp":1680876930250},"reason":"new_user_order"}}"#;
1641 let data = format!(
1642 r#"{{"elements":[{{"uid":"e2","timestamp":1680876931000,"event":{kept}}},{{"uid":"e1","timestamp":1680876930000,"event":{oldest}}}]}}"#
1643 );
1644
1645 let response = order_history_events_from(&data);
1646
1647 assert_eq!(response.order_events.len(), 1);
1648 assert_eq!(response.oldest_event_ms, Some(1_680_876_930_000));
1649 }
1650
1651 #[rstest]
1654 fn test_parse_futures_order_events_names_the_order_of_a_pre_epoch_row() {
1655 let data = r#"{"elements":[{"uid":"e1","timestamp":1680876930250,"event":{"OrderPlaced":{"order":{"uid":"o-pre","tradeable":"PF_XBTUSD","direction":"Buy","quantity":"1","filled":"0","limitPrice":"70000","orderType":"Limit","clientId":"","reduceOnly":false,"timestamp":-1,"lastUpdateTimestamp":-1},"reason":"new_user_order"}}}]}"#;
1656
1657 let response = order_history_events_from(data);
1658
1659 assert!(response.order_events.is_empty());
1660 assert_eq!(response.skipped_rows, 1);
1661 assert_eq!(response.skipped_order_ids, vec!["o-pre".to_string()]);
1662 }
1663
1664 #[rstest]
1668 fn test_parse_futures_order_events_reports_what_the_page_covers() {
1669 let response = order_history_events("http_futures_order_events_unknown.json");
1670
1671 assert_eq!(
1672 response.oldest_event_ms,
1673 Some(1_779_062_400_000),
1674 "the oldest element is the skipped Unknown-type row"
1675 );
1676 assert_eq!(response.skipped_order_ids, vec!["abc".to_string()]);
1677 assert_eq!(response.skipped_rows, 2);
1678 }
1679
1680 #[rstest]
1684 #[case::missing_type("")]
1685 #[case::undocumented_type(r#""orderType":"SomethingNew","#)]
1686 fn test_parse_futures_order_events_skips_an_unmappable_order_type(#[case] order_type: &str) {
1687 let data = format!(
1688 r#"{{"elements":[{{"uid":"e1","timestamp":1680876930250,"event":{{"OrderPlaced":{{"order":{{"uid":"o1","tradeable":"PF_XBTUSD","direction":"Sell","quantity":"3","filled":"1",{order_type}"limitPrice":"70000","clientId":"cl-1","reduceOnly":true,"timestamp":1680876930250,"lastUpdateTimestamp":1680876930250}}}}}}}},{{"uid":"e2","timestamp":1680877245500,"event":{{"OrderPlaced":{{"order":{{"uid":"o2","tradeable":"PF_XBTUSD","direction":"Buy","quantity":"2","filled":"0.5","limitPrice":"69500.5","orderType":"Limit","clientId":"cl-2","reduceOnly":false,"timestamp":1680877245500,"lastUpdateTimestamp":1680877245500}}}}}}}}]}}"#
1689 );
1690 let response = order_history_events_from(&data);
1691
1692 assert_eq!(response.order_events.len(), 1);
1693 let sibling = &response.order_events[0];
1694 assert_eq!(sibling.event_type, KrakenFuturesOrderEventType::Place);
1695 assert_eq!(sibling.order.order_id, "o2");
1696 assert_eq!(sibling.order.cli_ord_id.as_deref(), Some("cl-2"));
1697 assert_eq!(sibling.order.order_type, KrakenFuturesOrderType::Limit);
1698 assert_eq!(sibling.order.symbol, "PF_XBTUSD");
1699 assert_eq!(sibling.order.side, KrakenOrderSide::Buy);
1700 assert_eq!(sibling.order.quantity, dec!(2));
1701 assert_eq!(sibling.order.filled, dec!(0.5));
1702 assert_eq!(sibling.order.limit_price, Some(dec!(69500.5)));
1703 assert_eq!(sibling.order.stop_price, None);
1704 assert_eq!(sibling.order.timestamp, "2023-04-07T14:20:45.500Z");
1705 assert_eq!(
1706 sibling.order.last_update_timestamp,
1707 "2023-04-07T14:20:45.500Z"
1708 );
1709 assert!(!sibling.order.reduce_only);
1710 assert_eq!(
1711 response.skipped_rows, 1,
1712 "the skip leaves the set incomplete"
1713 );
1714 }
1715
1716 #[rstest]
1719 fn test_parse_futures_order_events_skips_a_negative_timestamp() {
1720 let data = r#"{"elements":[{"uid":"e1","timestamp":-1,"event":{"OrderPlaced":{"order":{"uid":"o1","tradeable":"PF_XBTUSD","direction":"Buy","quantity":"1","filled":"0","limitPrice":"70000","orderType":"Limit","clientId":"","reduceOnly":false,"timestamp":-1,"lastUpdateTimestamp":-1}}}}]}"#;
1721 let response: FuturesOrderHistoryResponse = serde_json::from_str(data).unwrap();
1722 let response = response.into_order_events().unwrap();
1723
1724 assert!(response.order_events.is_empty());
1725 assert_eq!(
1726 response.skipped_rows, 1,
1727 "the skip leaves the set incomplete"
1728 );
1729 }
1730
1731 #[rstest]
1733 fn test_parse_futures_order_events_finds_the_kind_after_other_keys() {
1734 let data = r#"{"elements":[{"uid":"e1","timestamp":1680877245500,"event":{"version":1,"OrderCancelled":{"order":{"uid":"o1","tradeable":"PI_XBTUSD","direction":"Buy","quantity":"1","filled":"0","limitPrice":"70000","orderType":"Limit","clientId":"","reduceOnly":false,"timestamp":1680876930250,"lastUpdateTimestamp":1680876930250}}}}]}"#;
1735 let response = order_history_events_from(data);
1736
1737 assert_eq!(
1738 response.order_events[0].event_type,
1739 KrakenFuturesOrderEventType::Cancel
1740 );
1741 }
1742
1743 #[rstest]
1746 fn test_parse_futures_order_events_dates_a_report_at_the_event() {
1747 let data = r#"{"elements":[{"uid":"e1","timestamp":1680877245500,"event":{"OrderEditRejected":{"oldOrder":{"uid":"o1","tradeable":"PI_XBTUSD","direction":"Buy","quantity":"1","filled":"0","limitPrice":"70000","orderType":"Limit","clientId":"","reduceOnly":false,"timestamp":1680876930250,"lastUpdateTimestamp":1680876930250}}}}]}"#;
1748 let response = order_history_events_from(data);
1749
1750 assert_eq!(
1751 response.order_events[0].order.last_update_timestamp,
1752 "2023-04-07T14:20:45.500Z"
1753 );
1754 }
1755
1756 #[rstest]
1758 fn test_parse_futures_order_events_tolerates_an_empty_event() {
1759 let data = r#"{"elements":[{"uid":"e1","timestamp":1,"event":{}}]}"#;
1760 let response: FuturesOrderHistoryResponse = serde_json::from_str(data).unwrap();
1761
1762 assert!(matches!(
1763 response.elements.as_deref(),
1764 Some([FuturesOrderHistoryElement { event: FuturesOrderHistoryEvent::Unknown(kind), .. }]) if kind.is_empty()
1765 ));
1766 }
1767
1768 #[rstest]
1770 fn test_parse_futures_order_events_skips_an_unknown_direction() {
1771 let data = r#"{"elements":[{"uid":"e1","timestamp":1680876930250,"event":{"OrderPlaced":{"order":{"uid":"o1","tradeable":"PF_XBTUSD","direction":"Unknown","quantity":"1","filled":"0","limitPrice":"70000","orderType":"Limit","clientId":"","reduceOnly":false,"timestamp":1680876930250,"lastUpdateTimestamp":1680876930250},"reason":"","reducedQuantity":""}}}]}"#;
1772 let response: FuturesOrderHistoryResponse = serde_json::from_str(data).unwrap();
1773 let response = response.into_order_events().unwrap();
1774
1775 assert!(response.order_events.is_empty());
1776 assert_eq!(
1777 response.skipped_rows, 1,
1778 "the skip leaves the set incomplete"
1779 );
1780 }
1781
1782 #[rstest]
1785 fn test_parse_futures_order_events_fails_on_a_venue_error_body() {
1786 let data = r#"{"result":"error","error":"apiLimitExceeded"}"#;
1787 let response: FuturesOrderHistoryResponse = serde_json::from_str(data).unwrap();
1788
1789 let error = response
1790 .into_order_events()
1791 .expect_err("a venue error must fail the read");
1792
1793 assert!(
1794 matches!(&error, KrakenHttpError::ApiError(reasons) if reasons == &["apiLimitExceeded"]),
1795 "unexpected error: {error}"
1796 );
1797 }
1798
1799 #[rstest]
1801 fn test_parse_futures_order_events_requires_elements() {
1802 let data = r#"{"serverTime":"2023-04-07T16:30:45.678Z"}"#;
1803 let response: FuturesOrderHistoryResponse = serde_json::from_str(data).unwrap();
1804
1805 assert!(matches!(
1806 response.into_order_events(),
1807 Err(KrakenHttpError::ParseError(_))
1808 ));
1809 }
1810
1811 #[rstest]
1812 fn test_parse_futures_order_trigger_data_tolerates_unknown_enum_values() {
1813 let data = load_test_data("http_send_order_futures_unknown_trigger.json");
1816 let response: FuturesSendOrderResponse =
1817 serde_json::from_str(&data).expect("Failed to parse send-order response with unknown");
1818
1819 let send_status = response.send_status.expect("sendStatus missing");
1820 let order_events = send_status.order_events.expect("orderEvents missing");
1821 let trigger = order_events[0]
1822 .order_trigger
1823 .as_ref()
1824 .expect("orderTrigger missing");
1825
1826 assert_eq!(trigger.order_type, KrakenFuturesOrderType::Unknown);
1827 assert_eq!(trigger.trigger_side, KrakenTriggerSide::Unknown);
1828 assert_eq!(trigger.trigger_signal, KrakenTriggerSignal::Unknown);
1829 }
1830
1831 #[rstest]
1832 fn test_parse_futures_send_order_execution_event_uses_enum_event_type() {
1833 let data = r#"
1834 {
1835 "result": "success",
1836 "sendStatus": {
1837 "status": "placed",
1838 "orderEvents": [
1839 {
1840 "type": "EXECUTION",
1841 "executionId": "c8a35168-8d52-4609-944f-3f32bb0d5c77",
1842 "price": 35000.5,
1843 "amount": 1.25,
1844 "orderPriorExecution": {
1845 "orderId": "c8a35168-8d52-4609-944f-3f32bb0d5c77",
1846 "cliOrdId": "test-order-001",
1847 "type": "lmt",
1848 "symbol": "PI_XBTUSD",
1849 "side": "buy",
1850 "quantity": 2.0,
1851 "filled": 0.0,
1852 "limitPrice": 35000.5,
1853 "timestamp": "2024-01-15T10:30:45.123Z",
1854 "lastUpdateTimestamp": "2024-01-15T10:30:45.123Z",
1855 "reduceOnly": false
1856 }
1857 }
1858 ]
1859 }
1860 }
1861 "#;
1862 let response: FuturesSendOrderResponse =
1863 serde_json::from_str(data).expect("Failed to parse futures send order response");
1864
1865 let send_status = response.send_status.expect("sendStatus missing");
1866 let order_events = send_status.order_events.expect("orderEvents missing");
1867
1868 assert_eq!(order_events.len(), 1);
1869 assert_eq!(
1870 order_events[0].event_type,
1871 KrakenFuturesOrderEventType::Execution
1872 );
1873 }
1874}