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 = "cliOrdId", default)]
265 pub cli_ord_id: Option<String>,
266 #[serde(rename = "cli_ord_id", default)]
269 pub cli_ord_id_snake_case: Option<String>,
270}
271
272impl FuturesOpenOrder {
273 #[must_use]
275 pub fn client_order_id(&self) -> Option<&str> {
276 self.cli_ord_id
277 .as_deref()
278 .or(self.cli_ord_id_snake_case.as_deref())
279 }
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
283#[serde(rename_all = "camelCase")]
284pub struct FuturesOpenOrdersResponse {
285 pub result: KrakenApiResult,
286 #[serde(default)]
287 pub server_time: Option<String>,
288 #[serde(default)]
289 pub error: Option<String>,
290 #[serde(default)]
291 pub open_orders: Vec<FuturesOpenOrder>,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
298#[serde(rename_all = "camelCase")]
299pub struct FuturesCachedOrder {
300 pub order_id: String,
301 #[serde(default)]
302 pub cli_ord_id: Option<String>,
303 pub symbol: String,
304 pub side: KrakenOrderSide,
305 #[serde(default, with = "optional_decimal")]
306 pub quantity: Option<Decimal>,
307 #[serde(default, with = "optional_decimal")]
308 pub filled: Option<Decimal>,
309 #[serde(default, with = "optional_decimal")]
310 pub limit_price: Option<Decimal>,
311 #[serde(default)]
312 pub reduce_only: bool,
313 pub timestamp: String,
314 pub last_update_timestamp: String,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319#[serde(rename_all = "camelCase")]
320pub struct FuturesOrderStatusDetails {
321 pub order: FuturesCachedOrder,
322 pub status: KrakenFuturesOrderLifecycleStatus,
323 #[serde(default)]
324 pub update_reason: Option<String>,
325 #[serde(default)]
326 pub error: Option<String>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct FuturesOrdersStatusResponse {
334 pub result: KrakenApiResult,
335 #[serde(default)]
336 pub server_time: Option<String>,
337 #[serde(default)]
338 pub error: Option<String>,
339 #[serde(default)]
340 pub orders: Vec<FuturesOrderStatusDetails>,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
347#[serde(rename_all = "camelCase")]
348pub struct FuturesOrderEventWrapper {
349 pub order: FuturesOrderEvent,
350 #[serde(rename = "type")]
351 pub event_type: KrakenFuturesOrderEventType,
352 #[serde(default, with = "optional_decimal")]
353 pub reduced_quantity: Option<Decimal>,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
358#[serde(rename_all = "camelCase")]
359pub struct FuturesOrderEvent {
360 pub order_id: String,
361 #[serde(default)]
362 pub cli_ord_id: Option<String>,
363 #[serde(rename = "type")]
364 pub order_type: KrakenFuturesOrderType,
365 pub symbol: String,
366 pub side: KrakenOrderSide,
367 #[serde(with = "decimal")]
368 pub quantity: Decimal,
369 #[serde(with = "decimal")]
370 pub filled: Decimal,
371 #[serde(default, with = "optional_decimal")]
372 pub limit_price: Option<Decimal>,
373 #[serde(default, with = "optional_decimal")]
374 pub stop_price: Option<Decimal>,
375 pub timestamp: String,
376 pub last_update_timestamp: String,
377 #[serde(default)]
378 pub reduce_only: bool,
379}
380
381#[derive(Debug, Clone)]
386pub struct FuturesOrderEventsResponse {
387 pub server_time: Option<String>,
388 pub order_events: Vec<FuturesOrderEventWrapper>,
389 pub continuation_token: Option<String>,
390 pub skipped_rows: usize,
392 pub skipped_order_ids: Vec<String>,
394 pub oldest_event_ms: Option<i64>,
398}
399
400#[derive(Debug, Clone, Deserialize)]
409#[serde(rename_all = "camelCase")]
410pub struct FuturesOrderHistoryResponse {
411 #[serde(default)]
412 pub result: Option<KrakenApiResult>,
413 #[serde(default)]
414 pub error: Option<String>,
415 #[serde(default)]
416 pub server_time: Option<String>,
417 #[serde(default)]
418 pub elements: Option<Vec<FuturesOrderHistoryElement>>,
419 #[serde(default)]
420 pub continuation_token: Option<String>,
421}
422
423#[derive(Debug, Clone, Deserialize)]
425#[serde(rename_all = "camelCase")]
426pub struct FuturesOrderHistoryElement {
427 pub uid: String,
428 pub timestamp: i64,
429 pub event: FuturesOrderHistoryEvent,
430}
431
432#[derive(Debug, Clone)]
437pub enum FuturesOrderHistoryEvent {
438 OrderPlaced(FuturesOrderHistoryOrderEvent),
439 OrderUpdated(FuturesOrderHistoryOrderUpdate),
440 OrderCancelled(FuturesOrderHistoryOrderEvent),
441 OrderRejected(FuturesOrderHistoryOrderEvent),
442 OrderEditRejected(FuturesOrderHistoryEditRejected),
444 OrderNotFound(FuturesOrderHistoryOrderNotFound),
446 Unknown(String),
447}
448
449impl<'de> Deserialize<'de> for FuturesOrderHistoryEvent {
450 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
451 where
452 D: serde::Deserializer<'de>,
453 {
454 struct EventVisitor;
455
456 impl<'de> serde::de::Visitor<'de> for EventVisitor {
457 type Value = FuturesOrderHistoryEvent;
458
459 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
460 formatter.write_str("an order history event keyed by its kind")
461 }
462
463 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
464 where
465 A: serde::de::MapAccess<'de>,
466 {
467 let mut event = None;
470 let mut first_key = None;
471
472 while let Some(key) = map.next_key::<String>()? {
473 let known = match key.as_str() {
474 "OrderPlaced" => {
475 Some(FuturesOrderHistoryEvent::OrderPlaced(map.next_value()?))
476 }
477 "OrderUpdated" => {
478 Some(FuturesOrderHistoryEvent::OrderUpdated(map.next_value()?))
479 }
480 "OrderCancelled" => {
481 Some(FuturesOrderHistoryEvent::OrderCancelled(map.next_value()?))
482 }
483 "OrderRejected" => {
484 Some(FuturesOrderHistoryEvent::OrderRejected(map.next_value()?))
485 }
486 "OrderEditRejected" => Some(FuturesOrderHistoryEvent::OrderEditRejected(
487 map.next_value()?,
488 )),
489 "OrderNotFound" => {
490 Some(FuturesOrderHistoryEvent::OrderNotFound(map.next_value()?))
491 }
492 _ => {
493 map.next_value::<serde::de::IgnoredAny>()?;
494 None
495 }
496 };
497
498 if event.is_none() && known.is_some() {
499 event = known;
500 }
501
502 if first_key.is_none() {
503 first_key = Some(key);
504 }
505 }
506
507 Ok(event.unwrap_or_else(|| {
508 FuturesOrderHistoryEvent::Unknown(first_key.unwrap_or_default())
509 }))
510 }
511 }
512
513 deserializer.deserialize_map(EventVisitor)
514 }
515}
516
517#[derive(Debug, Clone, Deserialize)]
519#[serde(rename_all = "camelCase")]
520pub struct FuturesOrderHistoryOrderEvent {
521 pub order: FuturesOrderHistoryOrder,
522}
523
524#[derive(Debug, Clone, Deserialize)]
526#[serde(rename_all = "camelCase")]
527pub struct FuturesOrderHistoryOrderUpdate {
528 pub new_order: FuturesOrderHistoryOrder,
529}
530
531#[derive(Debug, Clone, Deserialize)]
533#[serde(rename_all = "camelCase")]
534pub struct FuturesOrderHistoryEditRejected {
535 pub old_order: FuturesOrderHistoryOrder,
536}
537
538#[derive(Debug, Clone, Deserialize)]
540#[serde(rename_all = "camelCase")]
541pub struct FuturesOrderHistoryOrderNotFound {
542 #[serde(default)]
543 pub order_id: Option<String>,
544}
545
546#[derive(Debug, Clone, Deserialize)]
548#[serde(rename_all = "camelCase")]
549pub struct FuturesOrderHistoryOrder {
550 pub uid: String,
551 pub tradeable: String,
552 pub direction: KrakenFuturesHistoryDirection,
553 #[serde(with = "decimal")]
554 pub quantity: Decimal,
555 #[serde(with = "decimal")]
556 pub filled: Decimal,
557 #[serde(default, deserialize_with = "optional_decimal_or_empty::deserialize")]
559 pub limit_price: Option<Decimal>,
560 #[serde(default)]
561 pub order_type: Option<KrakenFuturesHistoryOrderType>,
562 #[serde(default)]
563 pub client_id: Option<String>,
564 #[serde(default)]
565 pub reduce_only: bool,
566 pub timestamp: i64,
567 pub last_update_timestamp: i64,
568}
569
570fn millis_to_rfc3339(millis: i64) -> Option<String> {
571 let nanos = u64::try_from(millis).ok()?.checked_mul(1_000_000)?;
572 Some(unix_nanos_to_iso8601_millis(UnixNanos::from(nanos)))
573}
574
575impl FuturesOrderHistoryResponse {
576 pub fn into_order_events(self) -> Result<FuturesOrderEventsResponse, KrakenHttpError> {
600 if let Some(error) = self.error {
601 return Err(KrakenHttpError::ApiError(vec![error]));
602 }
603
604 if self.result == Some(KrakenApiResult::Error) {
605 return Err(KrakenHttpError::ApiError(vec![
606 "order history request failed without a reason".to_string(),
607 ]));
608 }
609
610 let Some(elements) = self.elements else {
611 return Err(KrakenHttpError::ParseError(
612 "order history page has no `elements` array".to_string(),
613 ));
614 };
615
616 let mut skipped_rows = 0usize;
617 let mut skipped_order_ids = Vec::new();
618 let oldest_event_ms = elements.iter().map(|element| element.timestamp).min();
619 let order_events = elements
620 .into_iter()
621 .filter_map(|element| {
622 let (order, event_type) = match element.event {
623 FuturesOrderHistoryEvent::OrderPlaced(event) => {
624 (event.order, KrakenFuturesOrderEventType::Place)
625 }
626 FuturesOrderHistoryEvent::OrderUpdated(event) => {
627 (event.new_order, KrakenFuturesOrderEventType::Edit)
628 }
629 FuturesOrderHistoryEvent::OrderCancelled(event) => {
630 (event.order, KrakenFuturesOrderEventType::Cancel)
631 }
632 FuturesOrderHistoryEvent::OrderRejected(event) => {
633 (event.order, KrakenFuturesOrderEventType::Reject)
634 }
635 FuturesOrderHistoryEvent::OrderEditRejected(event) => {
636 (event.old_order, KrakenFuturesOrderEventType::Edit)
637 }
638 FuturesOrderHistoryEvent::OrderNotFound(event) => {
639 log::warn!(
640 "Skipping order history event {} (order not found {:?}): no order state",
641 element.uid,
642 event.order_id
643 );
644 return None;
645 }
646 FuturesOrderHistoryEvent::Unknown(kind) => {
647 log::warn!(
648 "Skipping order history event {} of undocumented kind {kind:?}; the set is incomplete",
649 element.uid
650 );
651 skipped_rows += 1;
652 return None;
653 }
654 };
655
656 let (Some(timestamp), Some(last_update_timestamp)) = (
659 millis_to_rfc3339(order.timestamp),
660 millis_to_rfc3339(order.last_update_timestamp.max(element.timestamp)),
661 ) else {
662 log::warn!(
663 "Skipping order history event {} for order {} on {}: timestamp before the epoch; 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 let side = match order.direction {
674 KrakenFuturesHistoryDirection::Buy => KrakenOrderSide::Buy,
675 KrakenFuturesHistoryDirection::Sell => KrakenOrderSide::Sell,
676 KrakenFuturesHistoryDirection::Unknown => {
677 log::warn!(
678 "Skipping order history event {} for order {} on {}: direction unknown; the set is incomplete",
679 element.uid,
680 order.uid,
681 order.tradeable
682 );
683 skipped_rows += 1;
684 skipped_order_ids.push(order.uid);
685 return None;
686 }
687 };
688
689 let kind = match order.order_type {
692 Some(KrakenFuturesHistoryOrderType::Unknown) | None => {
693 let situation = if order.order_type.is_none() {
694 "order type missing"
695 } else {
696 "order type reported as Unknown"
697 };
698 log::warn!(
699 "Skipping order history event {} for order {} (client id {:?}) on {}: {situation}; the set is incomplete",
700 element.uid,
701 order.uid,
702 order.client_id.as_deref().filter(|id| !id.is_empty()),
703 order.tradeable
704 );
705 skipped_rows += 1;
706 skipped_order_ids.push(order.uid);
707 return None;
708 }
709 Some(kind) => kind,
710 };
711
712 let market_like = matches!(
713 kind,
714 KrakenFuturesHistoryOrderType::Market
715 | KrakenFuturesHistoryOrderType::Liquidation
716 | KrakenFuturesHistoryOrderType::PartialLiquidation
717 | KrakenFuturesHistoryOrderType::CoveredLiquidation
718 | KrakenFuturesHistoryOrderType::Assignment
719 | KrakenFuturesHistoryOrderType::HedgeAssignment
720 | KrakenFuturesHistoryOrderType::Unwind
721 | KrakenFuturesHistoryOrderType::Block
722 | KrakenFuturesHistoryOrderType::Rfq
723 );
724 let limit_price = match kind {
725 _ if market_like => None,
726 KrakenFuturesHistoryOrderType::Stop => {
728 order.limit_price.filter(|price| !price.is_zero())
729 }
730 _ => order.limit_price,
731 };
732 let order_type = match (kind, limit_price) {
733 (KrakenFuturesHistoryOrderType::Stop, Some(_)) => KrakenFuturesOrderType::Limit,
734 (KrakenFuturesHistoryOrderType::Stop, None) => KrakenFuturesOrderType::Market,
735 (kind, _) => kind.into(),
736 };
737
738 Some(FuturesOrderEventWrapper {
739 order: FuturesOrderEvent {
740 order_id: order.uid,
741 cli_ord_id: order.client_id.filter(|id| !id.is_empty()),
742 order_type,
743 symbol: order.tradeable,
744 side,
745 quantity: order.quantity,
746 filled: order.filled,
747 limit_price,
748 stop_price: None,
749 timestamp,
750 last_update_timestamp,
751 reduce_only: order.reduce_only,
752 },
753 event_type,
754 reduced_quantity: None,
755 })
756 })
757 .collect();
758
759 Ok(FuturesOrderEventsResponse {
760 server_time: self.server_time,
761 order_events,
762 continuation_token: self.continuation_token,
763 skipped_rows,
764 skipped_order_ids,
765 oldest_event_ms,
766 })
767 }
768}
769
770#[derive(Debug, Clone, Serialize, Deserialize)]
773#[serde(rename_all = "camelCase")]
774pub struct FuturesFill {
775 #[serde(rename = "fill_id")]
776 pub fill_id: String,
777 pub symbol: String,
778 pub side: KrakenOrderSide,
779 #[serde(rename = "order_id")]
780 pub order_id: String,
781 pub fill_time: String,
782 #[serde(with = "decimal")]
783 pub size: Decimal,
784 #[serde(with = "decimal")]
785 pub price: Decimal,
786 pub fill_type: KrakenFillType,
787 #[serde(rename = "cliOrdId", default)]
789 pub cli_ord_id: Option<String>,
790 #[serde(rename = "cli_ord_id", default)]
793 pub cli_ord_id_snake_case: Option<String>,
794 #[serde(rename = "fee_paid", default, with = "optional_decimal")]
795 pub fee_paid: Option<Decimal>,
796 #[serde(rename = "fee_currency", default)]
797 pub fee_currency: Option<String>,
798}
799
800impl FuturesFill {
801 #[must_use]
803 pub fn client_order_id(&self) -> Option<&str> {
804 self.cli_ord_id
805 .as_deref()
806 .or(self.cli_ord_id_snake_case.as_deref())
807 }
808}
809
810#[derive(Debug, Clone, Serialize, Deserialize)]
811#[serde(rename_all = "camelCase")]
812pub struct FuturesFillsResponse {
813 pub result: KrakenApiResult,
814 #[serde(default)]
815 pub server_time: Option<String>,
816 #[serde(default)]
817 pub error: Option<String>,
818 #[serde(default)]
819 pub fills: Vec<FuturesFill>,
820}
821
822#[derive(Debug, Clone, Serialize, Deserialize)]
825#[serde(rename_all = "camelCase")]
826pub struct FuturesPosition {
827 pub side: KrakenPositionSide,
828 pub symbol: String,
829 #[serde(with = "decimal")]
830 pub price: Decimal,
831 pub fill_time: String,
832 #[serde(with = "decimal")]
833 pub size: Decimal,
834 #[serde(default, with = "optional_decimal")]
835 pub unrealized_funding: Option<Decimal>,
836}
837
838#[derive(Debug, Clone, Serialize, Deserialize)]
839#[serde(rename_all = "camelCase")]
840pub struct FuturesOpenPositionsResponse {
841 pub result: KrakenApiResult,
842 #[serde(default)]
843 pub server_time: Option<String>,
844 #[serde(default)]
845 pub error: Option<String>,
846 #[serde(default)]
847 pub open_positions: Vec<FuturesPosition>,
848}
849
850#[derive(Debug, Clone, Serialize, Deserialize)]
853#[serde(rename_all = "camelCase")]
854pub struct FuturesSendOrderResponse {
855 pub result: KrakenApiResult,
856 #[serde(default)]
857 pub server_time: Option<String>,
858 #[serde(default)]
859 pub error: Option<String>,
860 pub send_status: Option<FuturesSendStatus>,
861}
862
863#[derive(Debug, Clone, Serialize, Deserialize)]
864#[serde(rename_all = "camelCase")]
865pub struct FuturesSendStatus {
866 #[serde(rename = "order_id", default)]
867 pub order_id: Option<String>,
868 #[serde(rename = "order_tag", default)]
869 pub order_tag: Option<String>,
870 pub status: String,
871 #[serde(default)]
872 pub order_events: Option<Vec<FuturesSendOrderEvent>>,
873 #[serde(rename = "cli_ord_id", default)]
874 pub cli_ord_id: Option<String>,
875 #[serde(rename = "receivedTime", default)]
876 pub received_time: Option<String>,
877}
878
879#[derive(Debug, Clone, Serialize, Deserialize)]
880#[serde(rename_all = "camelCase")]
881pub struct FuturesSendOrderEvent {
882 #[serde(rename = "type")]
883 pub event_type: KrakenFuturesOrderEventType,
884 #[serde(default)]
885 pub order: Option<FuturesOrderEventData>,
886 #[serde(default)]
887 pub order_trigger: Option<FuturesOrderTriggerData>,
888 #[serde(default, with = "optional_decimal")]
889 pub reduced_quantity: Option<Decimal>,
890 #[serde(rename = "executionId", default)]
892 pub execution_id: Option<String>,
893 #[serde(default, with = "optional_decimal")]
894 pub price: Option<Decimal>,
895 #[serde(default, with = "optional_decimal")]
896 pub amount: Option<Decimal>,
897 #[serde(rename = "orderPriorEdit", default)]
898 pub order_prior_edit: Option<Box<FuturesOrderEventData>>,
899 #[serde(rename = "orderPriorExecution", default)]
900 pub order_prior_execution: Option<Box<FuturesOrderEventData>>,
901 #[serde(rename = "takerReducedQuantity", default, with = "optional_decimal")]
902 pub taker_reduced_quantity: Option<Decimal>,
903 #[serde(default)]
905 pub reason: Option<String>,
906 #[serde(default)]
907 pub uid: Option<String>,
908}
909
910#[derive(Debug, Clone, Serialize, Deserialize)]
911#[serde(rename_all = "camelCase")]
912pub struct FuturesOrderEventData {
913 #[serde(rename = "orderId")]
914 pub order_id: String,
915 #[serde(rename = "cliOrdId", default)]
916 pub cli_ord_id: Option<String>,
917 #[serde(rename = "type")]
918 pub order_type: KrakenFuturesOrderType,
919 pub symbol: String,
920 pub side: KrakenOrderSide,
921 #[serde(with = "decimal")]
922 pub quantity: Decimal,
923 #[serde(with = "decimal")]
924 pub filled: Decimal,
925 #[serde(rename = "limitPrice", default, with = "optional_decimal")]
926 pub limit_price: Option<Decimal>,
927 #[serde(rename = "stopPrice", default, with = "optional_decimal")]
928 pub stop_price: Option<Decimal>,
929 pub timestamp: String,
930 #[serde(rename = "lastUpdateTimestamp")]
931 pub last_update_timestamp: String,
932 #[serde(rename = "reduceOnly", default)]
933 pub reduce_only: bool,
934}
935
936#[derive(Debug, Clone, Serialize, Deserialize)]
937#[serde(rename_all = "camelCase")]
938pub struct FuturesOrderTriggerData {
939 pub uid: String,
940 #[serde(rename = "clientId", default)]
941 pub client_id: Option<String>,
942 #[serde(rename = "type")]
943 pub order_type: KrakenFuturesOrderType,
944 pub symbol: String,
945 pub side: KrakenOrderSide,
946 #[serde(with = "decimal")]
947 pub quantity: Decimal,
948 #[serde(rename = "limitPrice", default, with = "optional_decimal")]
949 pub limit_price: Option<Decimal>,
950 #[serde(rename = "limitPriceOffsetValue", default, with = "optional_decimal")]
951 pub limit_price_offset_value: Option<Decimal>,
952 #[serde(rename = "limitPriceOffsetUnit", default)]
953 pub limit_price_offset_unit: Option<String>,
954 #[serde(rename = "triggerPrice")]
955 #[serde(with = "decimal")]
956 pub trigger_price: Decimal,
957 #[serde(rename = "triggerSide")]
958 pub trigger_side: KrakenTriggerSide,
959 #[serde(rename = "triggerSignal")]
960 pub trigger_signal: KrakenTriggerSignal,
961 #[serde(rename = "reduceOnly", default)]
962 pub reduce_only: bool,
963 pub timestamp: String,
964 #[serde(rename = "lastUpdateTimestamp")]
965 pub last_update_timestamp: String,
966}
967
968#[derive(Debug, Clone, Serialize, Deserialize)]
969#[serde(rename_all = "camelCase")]
970pub struct FuturesCancelOrderResponse {
971 pub result: KrakenApiResult,
972 #[serde(default)]
973 pub server_time: Option<String>,
974 pub cancel_status: FuturesCancelStatus,
975}
976
977#[derive(Debug, Clone, Serialize, Deserialize)]
978#[serde(rename_all = "camelCase")]
979pub struct FuturesCancelStatus {
980 pub status: KrakenSendStatus,
981 #[serde(rename = "order_id", default)]
982 pub order_id: Option<String>,
983 #[serde(rename = "cli_ord_id", default)]
984 pub cli_ord_id: Option<String>,
985}
986
987#[derive(Debug, Clone, Serialize, Deserialize)]
988#[serde(rename_all = "camelCase")]
989pub struct FuturesEditOrderResponse {
990 pub result: KrakenApiResult,
991 #[serde(default)]
992 pub server_time: Option<String>,
993 pub edit_status: FuturesEditStatus,
994}
995
996#[derive(Debug, Clone, Serialize, Deserialize)]
997#[serde(rename_all = "camelCase")]
998pub struct FuturesEditStatus {
999 pub status: String,
1000 #[serde(rename = "order_id", default)]
1001 pub order_id: Option<String>,
1002 #[serde(rename = "cli_ord_id", default)]
1003 pub cli_ord_id: Option<String>,
1004}
1005
1006#[derive(Debug, Clone, Serialize, Deserialize)]
1007#[serde(rename_all = "camelCase")]
1008pub struct FuturesBatchOrderResponse {
1009 pub result: KrakenApiResult,
1010 #[serde(default)]
1011 pub server_time: Option<String>,
1012 pub batch_status: Vec<FuturesSendStatus>,
1013}
1014
1015#[derive(Debug, Clone, Serialize, Deserialize)]
1020#[serde(rename_all = "camelCase")]
1021pub struct FuturesBatchCancelResponse {
1022 pub result: KrakenApiResult,
1023 #[serde(default)]
1024 pub server_time: Option<String>,
1025 #[serde(default)]
1026 pub error: Option<String>,
1027 #[serde(default)]
1028 pub batch_status: Vec<FuturesBatchCancelStatus>,
1029}
1030
1031#[derive(Debug, Clone, Serialize, Deserialize)]
1032#[serde(rename_all = "camelCase")]
1033pub struct FuturesBatchCancelStatus {
1034 #[serde(default)]
1035 pub order_id: Option<String>,
1036 #[serde(default)]
1037 pub cli_ord_id: Option<String>,
1038 #[serde(default)]
1039 pub status: Option<KrakenSendStatus>,
1040 #[serde(default)]
1041 pub cancel_status: Option<FuturesCancelStatus>,
1042}
1043
1044#[derive(Debug, Clone, Serialize, Deserialize)]
1045#[serde(rename_all = "camelCase")]
1046pub struct FuturesCancelAllOrdersResponse {
1047 pub result: KrakenApiResult,
1048 #[serde(default)]
1049 pub server_time: Option<String>,
1050 pub cancel_status: FuturesCancelAllStatus,
1051}
1052
1053#[derive(Debug, Clone, Serialize, Deserialize)]
1054#[serde(rename_all = "camelCase")]
1055pub struct FuturesCancelAllStatus {
1056 pub status: KrakenSendStatus,
1057 #[serde(default)]
1058 pub cancelled_orders: Vec<CancelledOrder>,
1059}
1060
1061#[derive(Debug, Clone, Serialize, Deserialize)]
1062#[serde(rename_all = "camelCase")]
1063pub struct CancelledOrder {
1064 #[serde(rename = "order_id", default)]
1065 pub order_id: Option<String>,
1066 #[serde(default)]
1067 pub cli_ord_id: Option<String>,
1068}
1069
1070#[derive(Debug, Clone, Serialize, Deserialize)]
1074#[serde(rename_all = "camelCase")]
1075pub struct FuturesPublicExecutionsResponse {
1076 pub elements: Vec<FuturesPublicExecutionElement>,
1077 #[serde(default)]
1078 pub len: Option<i64>,
1079 #[serde(default)]
1080 pub continuation_token: Option<String>,
1081}
1082
1083#[derive(Debug, Clone, Serialize, Deserialize)]
1085pub struct FuturesPublicExecutionElement {
1086 pub uid: String,
1087 pub timestamp: i64,
1088 pub event: FuturesPublicExecutionEvent,
1089}
1090
1091#[derive(Debug, Clone, Serialize, Deserialize)]
1093pub struct FuturesPublicExecutionEvent {
1094 #[serde(rename = "Execution")]
1095 pub execution: FuturesPublicExecutionWrapper,
1096}
1097
1098#[derive(Debug, Clone, Serialize, Deserialize)]
1100#[serde(rename_all = "camelCase")]
1101pub struct FuturesPublicExecutionWrapper {
1102 pub execution: FuturesPublicExecution,
1103 #[serde(default)]
1104 pub taker_reduced_quantity: Option<String>,
1105}
1106
1107#[derive(Debug, Clone, Serialize, Deserialize)]
1109#[serde(rename_all = "camelCase")]
1110pub struct FuturesPublicExecution {
1111 pub uid: String,
1112 pub maker_order: FuturesPublicOrder,
1113 pub taker_order: FuturesPublicOrder,
1114 pub timestamp: i64,
1115 pub quantity: String,
1116 pub price: String,
1117 #[serde(default)]
1118 pub mark_price: Option<String>,
1119 #[serde(default)]
1120 pub limit_filled: Option<bool>,
1121 #[serde(default)]
1122 pub usd_value: Option<String>,
1123}
1124
1125#[derive(Debug, Clone, Serialize, Deserialize)]
1127#[serde(rename_all = "camelCase")]
1128pub struct FuturesPublicOrder {
1129 pub uid: String,
1130 pub tradeable: String,
1131 pub direction: String,
1132 pub quantity: String,
1133 pub timestamp: i64,
1134 #[serde(default)]
1135 pub limit_price: Option<String>,
1136 #[serde(default)]
1137 pub order_type: Option<String>,
1138 #[serde(default)]
1139 pub reduce_only: Option<bool>,
1140 #[serde(default)]
1141 pub last_update_timestamp: Option<i64>,
1142}
1143
1144#[derive(Debug, Clone, Serialize, Deserialize)]
1148#[serde(rename_all = "camelCase")]
1149pub struct FuturesAccountsResponse {
1150 pub result: KrakenApiResult,
1151 #[serde(default)]
1152 pub accounts: AHashMap<String, FuturesAccount>,
1153 #[serde(default)]
1154 pub error: Option<String>,
1155 #[serde(default)]
1156 pub server_time: Option<String>,
1157}
1158
1159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1161#[serde(rename_all = "camelCase")]
1162pub enum KrakenFuturesAccountType {
1163 MultiCollateralMarginAccount,
1165 MarginAccount,
1167 CashAccount,
1169 #[serde(other)]
1171 Unknown,
1172}
1173
1174#[derive(Debug, Clone, Serialize, Deserialize)]
1176#[serde(rename_all = "camelCase")]
1177pub struct FuturesAccount {
1178 #[serde(rename = "type")]
1179 pub account_type: KrakenFuturesAccountType,
1180 #[serde(default)]
1183 pub currency: Option<String>,
1184 #[serde(default, with = "decimal_map")]
1186 pub balances: AHashMap<String, Decimal>,
1187 #[serde(default)]
1189 pub currencies: AHashMap<String, FuturesFlexCurrency>,
1190 #[serde(default)]
1192 pub auxiliary: Option<FuturesAuxiliary>,
1193 #[serde(default)]
1195 pub margin_requirements: Option<FuturesMarginRequirements>,
1196 #[serde(default, with = "optional_decimal")]
1198 pub portfolio_value: Option<Decimal>,
1199 #[serde(default, with = "optional_decimal")]
1201 pub available_margin: Option<Decimal>,
1202 #[serde(default, with = "optional_decimal")]
1204 pub initial_margin: Option<Decimal>,
1205 #[serde(default, with = "optional_decimal")]
1207 pub maintenance_margin: Option<Decimal>,
1208 #[serde(default, with = "optional_decimal")]
1210 pub pnl: Option<Decimal>,
1211}
1212
1213#[derive(Debug, Clone, Serialize, Deserialize)]
1215#[serde(rename_all = "camelCase")]
1216pub struct FuturesFlexCurrency {
1217 #[serde(with = "decimal")]
1218 pub quantity: Decimal,
1219 #[serde(default, with = "optional_decimal")]
1220 pub value: Option<Decimal>,
1221 #[serde(default, with = "optional_decimal")]
1222 pub collateral: Option<Decimal>,
1223 #[serde(default, with = "optional_decimal")]
1224 pub available: Option<Decimal>,
1225}
1226
1227#[derive(Debug, Clone, Serialize, Deserialize)]
1229#[serde(rename_all = "camelCase")]
1230pub struct FuturesAuxiliary {
1231 #[serde(default, with = "optional_decimal")]
1232 pub usd: Option<Decimal>,
1233 #[serde(default, with = "optional_decimal")]
1235 pub pv: Option<Decimal>,
1236 #[serde(default, with = "optional_decimal")]
1238 pub pnl: Option<Decimal>,
1239 #[serde(default, with = "optional_decimal")]
1241 pub af: Option<Decimal>,
1242 #[serde(default, with = "optional_decimal")]
1243 pub funding: Option<Decimal>,
1244}
1245
1246#[derive(Debug, Clone, Serialize, Deserialize)]
1248#[serde(rename_all = "camelCase")]
1249pub struct FuturesMarginRequirements {
1250 #[serde(default, with = "optional_decimal")]
1252 pub im: Option<Decimal>,
1253 #[serde(default, with = "optional_decimal")]
1255 pub mm: Option<Decimal>,
1256 #[serde(default, with = "optional_decimal")]
1258 pub lt: Option<Decimal>,
1259 #[serde(default, with = "optional_decimal")]
1261 pub tt: Option<Decimal>,
1262}
1263
1264#[cfg(test)]
1265mod tests {
1266 use rstest::rstest;
1267 use rust_decimal_macros::dec;
1268
1269 use super::*;
1270
1271 fn load_test_data(filename: &str) -> String {
1272 let path = format!("test_data/{filename}");
1273 std::fs::read_to_string(&path)
1274 .unwrap_or_else(|e| panic!("Failed to load test data from {path}: {e}"))
1275 }
1276
1277 #[rstest]
1278 fn test_parse_futures_cancel_all_orders_with_no_orders_to_cancel_status() {
1279 let raw = r#"{
1284 "result": "success",
1285 "cancelStatus": {
1286 "receivedTime": "2026-04-10T13:17:23.291Z",
1287 "cancelOnly": "PF_XBTUSD",
1288 "status": "noOrdersToCancel",
1289 "cancelledOrders": [
1290 {
1291 "order_id": "a182b1c0-cd01-4d1c-853b-605e936f412b",
1292 "cliOrdId": "5f173994-f660-4809-b97a-586221fe5926"
1293 }
1294 ],
1295 "orderEvents": []
1296 },
1297 "serverTime": "2026-04-10T13:17:23.291Z"
1298 }"#;
1299
1300 let response: FuturesCancelAllOrdersResponse =
1301 serde_json::from_str(raw).expect("Failed to parse cancel-all response");
1302
1303 assert_eq!(response.result, KrakenApiResult::Success);
1304 assert_eq!(
1305 response.cancel_status.status,
1306 KrakenSendStatus::NoOrdersToCancel
1307 );
1308 assert_eq!(response.cancel_status.cancelled_orders.len(), 1);
1309 assert_eq!(
1310 response.cancel_status.cancelled_orders[0]
1311 .order_id
1312 .as_deref(),
1313 Some("a182b1c0-cd01-4d1c-853b-605e936f412b")
1314 );
1315 assert_eq!(
1316 response.cancel_status.cancelled_orders[0]
1317 .cli_ord_id
1318 .as_deref(),
1319 Some("5f173994-f660-4809-b97a-586221fe5926")
1320 );
1321 }
1322
1323 #[rstest]
1324 fn test_parse_futures_open_orders() {
1325 let data = load_test_data("http_futures_open_orders.json");
1326 let response: FuturesOpenOrdersResponse =
1327 serde_json::from_str(&data).expect("Failed to parse futures open orders");
1328
1329 assert_eq!(response.result, KrakenApiResult::Success);
1330 assert_eq!(response.open_orders.len(), 3);
1331
1332 let order = &response.open_orders[0];
1333 assert_eq!(order.order_id, "2ce038ae-c144-4de7-a0f1-82f7f4fca864");
1334 assert_eq!(order.symbol, "PI_ETHUSD");
1335 assert_eq!(order.side, KrakenOrderSide::Buy);
1336 assert_eq!(order.order_type, KrakenFuturesOrderType::Limit);
1337 assert_eq!(order.limit_price, Some(dec!(1200)));
1338 assert_eq!(order.unfilled_size, Some(dec!(100)));
1339 assert_eq!(order.filled_size, dec!(0));
1340
1341 let trigger_order = &response.open_orders[1];
1342 assert_eq!(
1343 trigger_order.order_id,
1344 "c8135f52-2a86-4e26-b629-43cc37da9dbf"
1345 );
1346 assert_eq!(trigger_order.order_type, KrakenFuturesOrderType::TakeProfit);
1347 assert_eq!(trigger_order.symbol, "PI_XBTUSD");
1348 assert_eq!(trigger_order.side, KrakenOrderSide::Buy);
1349 assert_eq!(trigger_order.limit_price, None);
1350 assert_eq!(trigger_order.stop_price, Some(dec!(1880.4)));
1351 assert_eq!(trigger_order.unfilled_size, None);
1352 assert_eq!(trigger_order.received_time, "2023-04-07T15:14:25.995Z");
1353 assert_eq!(trigger_order.status, KrakenFuturesOrderStatus::Untouched);
1354 assert_eq!(trigger_order.filled_size, dec!(0));
1355 assert_eq!(trigger_order.reduce_only, Some(true));
1356 assert_eq!(trigger_order.last_update_time, "2023-04-07T15:14:25.995Z");
1357 assert_eq!(
1358 trigger_order.trigger_signal,
1359 Some(KrakenTriggerSignal::Last)
1360 );
1361 assert_eq!(trigger_order.cli_ord_id, None);
1362 }
1363
1364 #[rstest]
1365 fn test_parse_futures_orders_status() {
1366 let data = load_test_data("http_futures_orders_status.json");
1367 let response: FuturesOrdersStatusResponse =
1368 serde_json::from_str(&data).expect("Failed to parse futures orders status");
1369
1370 assert_eq!(response.result, KrakenApiResult::Success);
1371 assert_eq!(response.orders.len(), 2);
1372
1373 let part_filled = &response.orders[0];
1374 assert_eq!(
1375 part_filled.order.order_id,
1376 "5f6d15a5-8c9e-4b0a-9d3f-5a2b7c8d9e0f"
1377 );
1378 assert_eq!(
1379 part_filled.order.cli_ord_id.as_deref(),
1380 Some("uuid-mp-held-001")
1381 );
1382 assert_eq!(part_filled.order.side, KrakenOrderSide::Buy);
1383 assert_eq!(part_filled.order.quantity, Some(dec!(0.001)));
1384 assert_eq!(part_filled.order.filled, Some(dec!(0.0004)));
1385 assert_eq!(part_filled.order.limit_price, Some(dec!(70000)));
1386 assert!(!part_filled.order.reduce_only);
1387 assert_eq!(
1388 part_filled.status,
1389 KrakenFuturesOrderLifecycleStatus::Cancelled
1390 );
1391 assert_eq!(part_filled.update_reason.as_deref(), Some("PARTIAL_FILL"));
1392
1393 let open = &response.orders[1];
1394 assert_eq!(open.order.quantity, Some(dec!(0.0002)));
1395 assert_eq!(open.order.filled, Some(dec!(0)));
1396 assert!(open.order.reduce_only);
1397 assert_eq!(open.status, KrakenFuturesOrderLifecycleStatus::EnteredBook);
1398 assert_eq!(open.update_reason, None);
1399 }
1400
1401 #[rstest]
1402 fn test_parse_futures_instruments_maker_protection() {
1403 let data = load_test_data("http_futures_instruments_maker_protection.json");
1404 let response: FuturesInstrumentsResponse =
1405 serde_json::from_str(&data).expect("Failed to parse futures instruments");
1406
1407 assert_eq!(response.result, KrakenApiResult::Success);
1408 assert_eq!(response.instruments.len(), 2);
1409
1410 let protected = &response.instruments[0];
1411 assert_eq!(protected.symbol, "PF_ATOMUSD");
1412 assert_eq!(protected.maker_protection_millis, Some(20));
1413
1414 let unprotected = &response.instruments[1];
1417 assert_eq!(unprotected.symbol, "PF_ETHUSD");
1418 assert_eq!(unprotected.maker_protection_millis, None);
1419 }
1420
1421 #[rstest]
1424 #[case::documented_key(r#","cliOrdId":"doc-1""#, Some("doc-1"))]
1425 #[case::snake_case_key(r#","cli_ord_id":"snake-1""#, Some("snake-1"))]
1426 #[case::both_keys(r#","cliOrdId":"doc-1","cli_ord_id":"snake-1""#, Some("doc-1"))]
1427 #[case::neither_key("", None)]
1428 fn test_futures_open_order_and_fill_read_either_client_order_id_key(
1429 #[case] keys: &str,
1430 #[case] expected: Option<&str>,
1431 ) {
1432 let order: FuturesOpenOrder = serde_json::from_str(&format!(
1433 r#"{{"order_id":"o1","symbol":"PI_XBTUSD","side":"buy","orderType":"lmt","limitPrice":1.0,"unfilledSize":1.0,"receivedTime":"2023-04-07T14:15:30.250Z","status":"untouched","filledSize":0.0,"reduceOnly":false,"lastUpdateTime":"2023-04-07T14:15:30.250Z"{keys}}}"#
1434 ))
1435 .unwrap();
1436 let fill: FuturesFill = serde_json::from_str(&format!(
1437 r#"{{"fill_id":"f1","symbol":"PI_XBTUSD","side":"buy","order_id":"o1","fillTime":"2023-04-07T15:20:45.500Z","size":1,"price":1.0,"fillType":"taker"{keys}}}"#
1438 ))
1439 .unwrap();
1440
1441 assert_eq!(order.client_order_id(), expected);
1442 assert_eq!(fill.client_order_id(), expected);
1443 }
1444
1445 #[rstest]
1446 fn test_parse_futures_fills() {
1447 let data = load_test_data("http_futures_fills.json");
1448 let response: FuturesFillsResponse =
1449 serde_json::from_str(&data).expect("Failed to parse futures fills");
1450
1451 assert_eq!(response.result, KrakenApiResult::Success);
1452 assert_eq!(response.fills.len(), 3);
1453
1454 let fill = &response.fills[0];
1455 assert_eq!(fill.fill_id, "cad76f07-814e-4dc6-8478-7867407b6bff");
1456 assert_eq!(fill.symbol, "PI_XBTUSD");
1457 assert_eq!(fill.side, KrakenOrderSide::Buy);
1458 assert_eq!(fill.size, dec!(5000));
1459 assert_eq!(fill.price, dec!(27937.5));
1460 assert_eq!(fill.fill_type, KrakenFillType::Maker);
1461 assert_eq!(fill.fee_currency, Some("BTC".to_string()));
1462 assert_eq!(response.fills[1].fill_type, KrakenFillType::Taker);
1463 assert_eq!(response.fills[2].fill_type, KrakenFillType::Assignee);
1464 }
1465
1466 #[rstest]
1467 fn test_parse_futures_open_positions() {
1468 let data = load_test_data("http_futures_open_positions.json");
1469 let response: FuturesOpenPositionsResponse =
1470 serde_json::from_str(&data).expect("Failed to parse futures open positions");
1471
1472 assert_eq!(response.result, KrakenApiResult::Success);
1473 assert_eq!(response.open_positions.len(), 2);
1474
1475 let position = &response.open_positions[0];
1476 assert_eq!(position.side, KrakenPositionSide::Short);
1477 assert_eq!(position.symbol, "PI_XBTUSD");
1478 assert_eq!(position.size, dec!(8000));
1479 assert!(position.unrealized_funding.is_some());
1480 }
1481
1482 #[rstest]
1483 fn test_parse_futures_orderbook() {
1484 let data = load_test_data("http_futures_orderbook.json");
1485 let response: FuturesOrderBookResponse =
1486 serde_json::from_str(&data).expect("Failed to parse futures orderbook");
1487
1488 assert_eq!(response.result, KrakenApiResult::Success);
1489 assert_eq!(response.order_book.bids.len(), 3);
1490 assert_eq!(response.order_book.asks.len(), 3);
1491
1492 let best_bid = &response.order_book.bids[0];
1493 assert_eq!(best_bid.price, dec!(105900));
1494 assert_eq!(best_bid.qty, dec!(0.5));
1495
1496 let best_ask = &response.order_book.asks[0];
1497 assert_eq!(best_ask.price, dec!(105950));
1498 assert_eq!(best_ask.qty, dec!(0.3));
1499 }
1500
1501 #[rstest]
1502 fn test_parse_futures_historical_funding_rates() {
1503 let data = load_test_data("http_futures_historical_funding_rates.json");
1504 let response: FuturesHistoricalFundingRatesResponse =
1505 serde_json::from_str(&data).expect("Failed to parse historical funding rates");
1506
1507 assert_eq!(response.result, KrakenApiResult::Success);
1508 assert_eq!(response.rates.len(), 3);
1509
1510 let rate = &response.rates[0];
1511 assert_eq!(rate.timestamp, "2025-07-11T08:00:00.000Z");
1512 assert_eq!(rate.relative_funding_rate, dec!(0.0001));
1513 assert_eq!(rate.funding_rate, dec!(0.00005));
1514
1515 let negative_rate = &response.rates[1];
1516 assert_eq!(negative_rate.relative_funding_rate, dec!(-0.00005));
1517 }
1518
1519 #[rstest]
1520 fn test_parse_futures_orderbook_preserves_decimal_precision() {
1521 let data = load_test_data("http_futures_orderbook_precision.json");
1522 let level: FuturesOrderBookLevel = serde_json::from_str(&data).unwrap();
1523
1524 assert_eq!(level.price, dec!(0.1234567890123456789012345678));
1525 assert_eq!(level.qty, dec!(123456789.123456789));
1526 }
1527
1528 fn order_history_events(fixture: &str) -> FuturesOrderEventsResponse {
1529 let data = load_test_data(fixture);
1530 order_history_events_from(&data)
1531 }
1532
1533 fn order_history_events_from(data: &str) -> FuturesOrderEventsResponse {
1534 let response: FuturesOrderHistoryResponse =
1535 serde_json::from_str(data).expect("Failed to parse futures order history");
1536 response
1537 .into_order_events()
1538 .expect("the page carries elements")
1539 }
1540
1541 fn placed_element(order: &str) -> String {
1542 format!(
1543 r#"{{"elements":[{{"uid":"e1","timestamp":1680876930250,"event":{{"OrderPlaced":{{"order":{order}}}}}}}]}}"#
1544 )
1545 }
1546
1547 #[rstest]
1549 fn test_parse_futures_order_events_uses_enum_event_type() {
1550 let response = order_history_events("http_futures_order_events.json");
1551
1552 assert_eq!(response.order_events.len(), 3);
1553 assert_eq!(
1554 response.order_events[0].event_type,
1555 KrakenFuturesOrderEventType::Place
1556 );
1557 assert_eq!(
1558 response.order_events[1].event_type,
1559 KrakenFuturesOrderEventType::Edit
1560 );
1561 assert_eq!(
1562 response.order_events[2].event_type,
1563 KrakenFuturesOrderEventType::Cancel
1564 );
1565 assert_eq!(response.continuation_token.as_deref(), Some("simb178"));
1566 }
1567
1568 #[rstest]
1570 fn test_parse_futures_order_events_update_uses_the_new_order() {
1571 let response = order_history_events("http_futures_order_events.json");
1572 let updated = &response.order_events[1].order;
1573
1574 assert_eq!(updated.filled, dec!(10000));
1575 assert_eq!(updated.order_type, KrakenFuturesOrderType::Market);
1576 assert_eq!(updated.limit_price, None, "a zero limit price is no price");
1577 assert_eq!(updated.cli_ord_id, None, "an empty client id is no id");
1578 }
1579
1580 #[rstest]
1583 fn test_parse_futures_order_events_keeps_the_tradeable_and_formats_timestamps() {
1584 let response = order_history_events("http_futures_order_events.json");
1585 let canceled = &response.order_events[2].order;
1586
1587 assert_eq!(canceled.symbol, "pi_xbtusd");
1588 assert_eq!(canceled.timestamp, "2023-04-07T13:00:00.000Z");
1589 assert_eq!(canceled.last_update_timestamp, "2023-04-07T16:00:00.000Z");
1590 assert!(canceled.reduce_only);
1591 assert_eq!(canceled.limit_price, Some(dec!(26000.0)));
1592 assert_eq!(
1593 canceled.stop_price, None,
1594 "the history schema has no trigger price"
1595 );
1596 }
1597
1598 #[rstest]
1601 #[case::with_limit("26000.0", KrakenFuturesOrderType::Limit, Some(dec!(26000.0)))]
1602 #[case::without_limit("0", KrakenFuturesOrderType::Market, None)]
1603 fn test_parse_futures_order_events_reports_a_stop_as_its_triggered_order(
1604 #[case] limit_price: &str,
1605 #[case] expected_type: KrakenFuturesOrderType,
1606 #[case] expected_price: Option<Decimal>,
1607 ) {
1608 let data = placed_element(&format!(
1609 r#"{{"uid":"o1","tradeable":"PI_XBTUSD","direction":"Sell","quantity":"2000","filled":"0","limitPrice":"{limit_price}","orderType":"Stop","clientId":"","reduceOnly":true,"timestamp":1680876930250,"lastUpdateTimestamp":1680876930250}}"#
1610 ));
1611 let response = order_history_events_from(&data);
1612 let order = &response.order_events[0].order;
1613
1614 assert_eq!(order.order_type, expected_type);
1615 assert_eq!(order.limit_price, expected_price);
1616 assert_eq!(order.stop_price, None);
1617 }
1618
1619 #[rstest]
1622 #[case::market_empty("Market", "", None)]
1623 #[case::market_zero("Market", "0", None)]
1624 #[case::limit_zero("Limit", "0", Some(dec!(0)))]
1625 fn test_parse_futures_order_events_reads_the_limit_price_by_order_kind(
1626 #[case] order_type: &str,
1627 #[case] limit_price: &str,
1628 #[case] expected: Option<Decimal>,
1629 ) {
1630 let data = placed_element(&format!(
1631 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}}"#
1632 ));
1633 let response = order_history_events_from(&data);
1634
1635 assert_eq!(response.order_events[0].order.limit_price, expected);
1636 }
1637
1638 #[rstest]
1640 fn test_parse_futures_order_events_maps_a_rejection() {
1641 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"}}}]}"#;
1642 let response = order_history_events_from(data);
1643
1644 assert_eq!(
1645 response.order_events[0].event_type,
1646 KrakenFuturesOrderEventType::Reject
1647 );
1648 }
1649
1650 #[rstest]
1655 fn test_parse_futures_order_events_skips_an_unknown_order_type_and_keeps_siblings() {
1656 let response = order_history_events("http_futures_order_events_unknown.json");
1657
1658 assert_eq!(response.order_events.len(), 1);
1659 let sibling = &response.order_events[0];
1660 assert_eq!(sibling.event_type, KrakenFuturesOrderEventType::Edit);
1661 assert_eq!(sibling.reduced_quantity, None);
1662 assert_eq!(sibling.order.order_id, "abc");
1663 assert_eq!(sibling.order.cli_ord_id, None);
1664 assert_eq!(sibling.order.order_type, KrakenFuturesOrderType::Limit);
1665 assert_eq!(sibling.order.symbol, "PF_XBTUSD");
1666 assert_eq!(sibling.order.side, KrakenOrderSide::Buy);
1667 assert_eq!(sibling.order.quantity, dec!(1.0));
1668 assert_eq!(sibling.order.filled, dec!(0.5));
1669 assert_eq!(sibling.order.limit_price, Some(dec!(70000.0)));
1670 assert_eq!(sibling.order.stop_price, None);
1671 assert_eq!(sibling.order.timestamp, "2026-05-18T00:00:00.000Z");
1672 assert_eq!(
1673 sibling.order.last_update_timestamp,
1674 "2026-05-18T00:00:01.000Z"
1675 );
1676 assert!(!sibling.order.reduce_only);
1677 assert_eq!(
1678 response.server_time.as_deref(),
1679 Some("2026-05-18T00:00:03.000Z")
1680 );
1681 assert_eq!(response.continuation_token, None);
1682 assert_eq!(
1683 response.skipped_rows, 2,
1684 "the Unknown type and the undocumented kind count; the not-found event does not"
1685 );
1686 }
1687
1688 #[rstest]
1691 #[case::not_found(r#"{"OrderNotFound":{"orderId":"gone"}}"#)]
1692 #[case::undocumented(r#"{"OrderSomethingNew":{"order":{"uid":"x"}}}"#)]
1693 fn test_parse_futures_order_events_oldest_time_counts_every_element(#[case] oldest: &str) {
1694 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"}}"#;
1695 let data = format!(
1696 r#"{{"elements":[{{"uid":"e2","timestamp":1680876931000,"event":{kept}}},{{"uid":"e1","timestamp":1680876930000,"event":{oldest}}}]}}"#
1697 );
1698
1699 let response = order_history_events_from(&data);
1700
1701 assert_eq!(response.order_events.len(), 1);
1702 assert_eq!(response.oldest_event_ms, Some(1_680_876_930_000));
1703 }
1704
1705 #[rstest]
1708 fn test_parse_futures_order_events_names_the_order_of_a_pre_epoch_row() {
1709 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"}}}]}"#;
1710
1711 let response = order_history_events_from(data);
1712
1713 assert!(response.order_events.is_empty());
1714 assert_eq!(response.skipped_rows, 1);
1715 assert_eq!(response.skipped_order_ids, vec!["o-pre".to_string()]);
1716 }
1717
1718 #[rstest]
1722 fn test_parse_futures_order_events_reports_what_the_page_covers() {
1723 let response = order_history_events("http_futures_order_events_unknown.json");
1724
1725 assert_eq!(
1726 response.oldest_event_ms,
1727 Some(1_779_062_400_000),
1728 "the oldest element is the skipped Unknown-type row"
1729 );
1730 assert_eq!(response.skipped_order_ids, vec!["abc".to_string()]);
1731 assert_eq!(response.skipped_rows, 2);
1732 }
1733
1734 #[rstest]
1738 #[case::missing_type("")]
1739 #[case::undocumented_type(r#""orderType":"SomethingNew","#)]
1740 fn test_parse_futures_order_events_skips_an_unmappable_order_type(#[case] order_type: &str) {
1741 let data = format!(
1742 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}}}}}}}}]}}"#
1743 );
1744 let response = order_history_events_from(&data);
1745
1746 assert_eq!(response.order_events.len(), 1);
1747 let sibling = &response.order_events[0];
1748 assert_eq!(sibling.event_type, KrakenFuturesOrderEventType::Place);
1749 assert_eq!(sibling.order.order_id, "o2");
1750 assert_eq!(sibling.order.cli_ord_id.as_deref(), Some("cl-2"));
1751 assert_eq!(sibling.order.order_type, KrakenFuturesOrderType::Limit);
1752 assert_eq!(sibling.order.symbol, "PF_XBTUSD");
1753 assert_eq!(sibling.order.side, KrakenOrderSide::Buy);
1754 assert_eq!(sibling.order.quantity, dec!(2));
1755 assert_eq!(sibling.order.filled, dec!(0.5));
1756 assert_eq!(sibling.order.limit_price, Some(dec!(69500.5)));
1757 assert_eq!(sibling.order.stop_price, None);
1758 assert_eq!(sibling.order.timestamp, "2023-04-07T14:20:45.500Z");
1759 assert_eq!(
1760 sibling.order.last_update_timestamp,
1761 "2023-04-07T14:20:45.500Z"
1762 );
1763 assert!(!sibling.order.reduce_only);
1764 assert_eq!(
1765 response.skipped_rows, 1,
1766 "the skip leaves the set incomplete"
1767 );
1768 }
1769
1770 #[rstest]
1773 fn test_parse_futures_order_events_skips_a_negative_timestamp() {
1774 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}}}}]}"#;
1775 let response: FuturesOrderHistoryResponse = serde_json::from_str(data).unwrap();
1776 let response = response.into_order_events().unwrap();
1777
1778 assert!(response.order_events.is_empty());
1779 assert_eq!(
1780 response.skipped_rows, 1,
1781 "the skip leaves the set incomplete"
1782 );
1783 }
1784
1785 #[rstest]
1787 fn test_parse_futures_order_events_finds_the_kind_after_other_keys() {
1788 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}}}}]}"#;
1789 let response = order_history_events_from(data);
1790
1791 assert_eq!(
1792 response.order_events[0].event_type,
1793 KrakenFuturesOrderEventType::Cancel
1794 );
1795 }
1796
1797 #[rstest]
1800 fn test_parse_futures_order_events_dates_a_report_at_the_event() {
1801 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}}}}]}"#;
1802 let response = order_history_events_from(data);
1803
1804 assert_eq!(
1805 response.order_events[0].order.last_update_timestamp,
1806 "2023-04-07T14:20:45.500Z"
1807 );
1808 }
1809
1810 #[rstest]
1812 fn test_parse_futures_order_events_tolerates_an_empty_event() {
1813 let data = r#"{"elements":[{"uid":"e1","timestamp":1,"event":{}}]}"#;
1814 let response: FuturesOrderHistoryResponse = serde_json::from_str(data).unwrap();
1815
1816 assert!(matches!(
1817 response.elements.as_deref(),
1818 Some([FuturesOrderHistoryElement { event: FuturesOrderHistoryEvent::Unknown(kind), .. }]) if kind.is_empty()
1819 ));
1820 }
1821
1822 #[rstest]
1824 fn test_parse_futures_order_events_skips_an_unknown_direction() {
1825 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":""}}}]}"#;
1826 let response: FuturesOrderHistoryResponse = serde_json::from_str(data).unwrap();
1827 let response = response.into_order_events().unwrap();
1828
1829 assert!(response.order_events.is_empty());
1830 assert_eq!(
1831 response.skipped_rows, 1,
1832 "the skip leaves the set incomplete"
1833 );
1834 }
1835
1836 #[rstest]
1839 fn test_parse_futures_order_events_fails_on_a_venue_error_body() {
1840 let data = r#"{"result":"error","error":"apiLimitExceeded"}"#;
1841 let response: FuturesOrderHistoryResponse = serde_json::from_str(data).unwrap();
1842
1843 let error = response
1844 .into_order_events()
1845 .expect_err("a venue error must fail the read");
1846
1847 assert!(
1848 matches!(&error, KrakenHttpError::ApiError(reasons) if reasons == &["apiLimitExceeded"]),
1849 "unexpected error: {error}"
1850 );
1851 }
1852
1853 #[rstest]
1855 fn test_parse_futures_order_events_requires_elements() {
1856 let data = r#"{"serverTime":"2023-04-07T16:30:45.678Z"}"#;
1857 let response: FuturesOrderHistoryResponse = serde_json::from_str(data).unwrap();
1858
1859 assert!(matches!(
1860 response.into_order_events(),
1861 Err(KrakenHttpError::ParseError(_))
1862 ));
1863 }
1864
1865 #[rstest]
1866 fn test_parse_futures_order_trigger_data_tolerates_unknown_enum_values() {
1867 let data = load_test_data("http_send_order_futures_unknown_trigger.json");
1870 let response: FuturesSendOrderResponse =
1871 serde_json::from_str(&data).expect("Failed to parse send-order response with unknown");
1872
1873 let send_status = response.send_status.expect("sendStatus missing");
1874 let order_events = send_status.order_events.expect("orderEvents missing");
1875 let trigger = order_events[0]
1876 .order_trigger
1877 .as_ref()
1878 .expect("orderTrigger missing");
1879
1880 assert_eq!(trigger.order_type, KrakenFuturesOrderType::Unknown);
1881 assert_eq!(trigger.trigger_side, KrakenTriggerSide::Unknown);
1882 assert_eq!(trigger.trigger_signal, KrakenTriggerSignal::Unknown);
1883 }
1884
1885 #[rstest]
1886 fn test_parse_futures_send_order_execution_event_uses_enum_event_type() {
1887 let data = r#"
1888 {
1889 "result": "success",
1890 "sendStatus": {
1891 "status": "placed",
1892 "orderEvents": [
1893 {
1894 "type": "EXECUTION",
1895 "executionId": "c8a35168-8d52-4609-944f-3f32bb0d5c77",
1896 "price": 35000.5,
1897 "amount": 1.25,
1898 "orderPriorExecution": {
1899 "orderId": "c8a35168-8d52-4609-944f-3f32bb0d5c77",
1900 "cliOrdId": "test-order-001",
1901 "type": "lmt",
1902 "symbol": "PI_XBTUSD",
1903 "side": "buy",
1904 "quantity": 2.0,
1905 "filled": 0.0,
1906 "limitPrice": 35000.5,
1907 "timestamp": "2024-01-15T10:30:45.123Z",
1908 "lastUpdateTimestamp": "2024-01-15T10:30:45.123Z",
1909 "reduceOnly": false
1910 }
1911 }
1912 ]
1913 }
1914 }
1915 "#;
1916 let response: FuturesSendOrderResponse =
1917 serde_json::from_str(data).expect("Failed to parse futures send order response");
1918
1919 let send_status = response.send_status.expect("sendStatus missing");
1920 let order_events = send_status.order_events.expect("orderEvents missing");
1921
1922 assert_eq!(order_events.len(), 1);
1923 assert_eq!(
1924 order_events[0].event_type,
1925 KrakenFuturesOrderEventType::Execution
1926 );
1927 }
1928}