Skip to main content

nautilus_kraken/http/futures/
models.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Data models for Kraken Futures HTTP API responses.
17
18use 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// Futures Instruments Models
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct FuturesMarginLevel {
45    /// Number of contracts (for inverse futures) or notional units (for flexible futures).
46    /// The field name varies: `contracts` for inverse, `numNonContractUnits` for flexible.
47    #[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    /// Only present for inverse futures, not for flexible futures.
62    #[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    /// Maker Protection hold window in milliseconds for this market.
84    ///
85    /// Only present when the venue has Maker Protection configured for the
86    /// market; absent means no hold (treat the same as zero).
87    #[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// Futures Ticker Models
104
105#[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// Futures Order Book Models
163
164/// A `[price, qty]` pair from the Kraken Futures orderbook endpoint.
165#[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// Futures Historical Funding Rates Models
203
204#[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// Futures OHLC (Candles) Models
222
223#[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// Futures Open Orders Models
239
240#[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// Futures Orders Status Models (/orders/status)
280
281/// Order body returned by the `/orders/status` endpoint.
282#[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/// A single order status entry returned by the `/orders/status` endpoint.
303#[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/// Response from the Kraken Futures `/orders/status` endpoint, which reports
315/// orders open or with a fill/cancel event in the last 5 seconds.
316#[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// Futures Order Events Models
329
330/// Wrapper for an order event containing the order data and event type.
331#[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/// The actual order data within an order event.
342#[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/// Order events in the shape the readers consume, built from [`FuturesOrderHistoryResponse`].
367///
368/// This is a projection, not a wire type: a page is decoded as [`FuturesOrderHistoryResponse`]
369/// and converted, so a body of another shape cannot read as an empty page.
370#[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    /// Rows the projection could not represent, which leave the set incomplete.
376    pub skipped_rows: usize,
377    /// The order of each skipped row that names one; a row of an undocumented kind names none.
378    pub skipped_order_ids: Vec<String>,
379    /// The earliest event time on the page in milliseconds, over every element including the
380    /// skipped ones. The venue returns history newest first, so with a continuation token this is
381    /// how far back the page reaches.
382    pub oldest_event_ms: Option<i64>,
383}
384
385// Futures Order History Models
386
387/// Response from the Kraken Futures order history endpoint, `/api/history/v3/orders`.
388///
389/// Each element carries one lifecycle event keyed by its kind, and the order inside it names the
390/// contract as `tradeable` and the side as `direction`, with millisecond timestamps. The venue
391/// can answer with a success status and an error body, so `result` and `error` are read before
392/// the shape is checked; see [`Self::into_order_events`].
393#[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/// One order history element: the event and when it happened.
409#[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/// An order history event, externally tagged by its kind.
418///
419/// The documented kinds are modeled; one the documentation does not list is kept by name so the
420/// surrounding page still decodes.
421#[derive(Debug, Clone)]
422pub enum FuturesOrderHistoryEvent {
423    OrderPlaced(FuturesOrderHistoryOrderEvent),
424    OrderUpdated(FuturesOrderHistoryOrderUpdate),
425    OrderCancelled(FuturesOrderHistoryOrderEvent),
426    OrderRejected(FuturesOrderHistoryOrderEvent),
427    /// An edit the venue refused; the order stands as `old_order`.
428    OrderEditRejected(FuturesOrderHistoryEditRejected),
429    /// A request naming an order the venue does not hold; it carries no order state.
430    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                // The kind is the one key naming a documented event, wherever it sits among any
453                // other keys; the first key is reported when no key does.
454                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/// An event that carries one order: placed, canceled or rejected.
503#[derive(Debug, Clone, Deserialize)]
504#[serde(rename_all = "camelCase")]
505pub struct FuturesOrderHistoryOrderEvent {
506    pub order: FuturesOrderHistoryOrder,
507}
508
509/// An update event, carrying the order before and after the edit.
510#[derive(Debug, Clone, Deserialize)]
511#[serde(rename_all = "camelCase")]
512pub struct FuturesOrderHistoryOrderUpdate {
513    pub new_order: FuturesOrderHistoryOrder,
514}
515
516/// A refused edit, carrying the order as it stands.
517#[derive(Debug, Clone, Deserialize)]
518#[serde(rename_all = "camelCase")]
519pub struct FuturesOrderHistoryEditRejected {
520    pub old_order: FuturesOrderHistoryOrder,
521}
522
523/// A request for an order the venue does not hold.
524#[derive(Debug, Clone, Deserialize)]
525#[serde(rename_all = "camelCase")]
526pub struct FuturesOrderHistoryOrderNotFound {
527    #[serde(default)]
528    pub order_id: Option<String>,
529}
530
531/// An order as the history endpoint reports it.
532#[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    /// Absent for a market order, which the venue reports as zero or as an empty string.
543    #[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    /// Projects each element onto the event wrapper the readers consume.
562    ///
563    /// A venue error reported with a success status fails the read with the venue's reason, and a
564    /// body without an `elements` array fails it as a parse error, so neither reads as an empty
565    /// page. An update, and a refused edit, are represented by the order as it stands afterwards.
566    /// A not-found event carries no order state and is skipped. A kind the documentation does not
567    /// list, an order whose direction the venue could not decode, an order whose type is missing
568    /// or reported as `Unknown`, and an order with a timestamp before the epoch may carry order
569    /// state the projection cannot read, so each is skipped with a warning and counted in
570    /// `skipped_rows`, which leaves the set incomplete. `Unknown` is how the venue reports a
571    /// source type it could not decode, so it establishes no order type, and in particular not a
572    /// market order. The venue fills the required `limitPrice` of a market order with a zero or
573    /// an empty string and a missing client id with an empty string, so a market-like order (a
574    /// market order or a venue-initiated kind) carries no price and an empty client id is
575    /// `None`; a limit order keeps its price even at zero, since futures instruments allow
576    /// non-positive prices. The contract name is kept as the venue spells it;
577    /// the readers resolve it case-insensitively. The history order carries no trigger price, and
578    /// the engine cannot materialize a stop order without one, so a stop row is reported as the
579    /// limit or market order it executes as once triggered.
580    ///
581    /// # Errors
582    ///
583    /// Returns an error if the body carries a venue error or has no `elements` array.
584    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                // The event time is when this state became known; a refused edit, for one,
642                // leaves the order's own stamp at its last change.
643                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                // `Unknown` is how the venue reports a source type it could not decode, so
675                // neither it nor a missing type establishes what the order is.
676                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                    // A stop with the placeholder price has no limit leg.
712                    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// Futures Fills Models
756
757#[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// Futures Positions Models
793
794#[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// Futures Order Execution Models
821
822#[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    // Execution event fields
861    #[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    // Reject event fields
874    #[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/// Response for batch cancel operations via `/derivatives/api/v3/batchorder`.
986///
987/// When sending only cancel operations, the response has a different format
988/// with individual cancel status items.
989#[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// Futures Public Executions Models
1041
1042/// Response from the Kraken Futures public executions endpoint.
1043#[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/// A single execution element from the public executions response.
1054#[derive(Debug, Clone, Serialize, Deserialize)]
1055pub struct FuturesPublicExecutionElement {
1056    pub uid: String,
1057    pub timestamp: i64,
1058    pub event: FuturesPublicExecutionEvent,
1059}
1060
1061/// The event wrapper containing the execution details.
1062#[derive(Debug, Clone, Serialize, Deserialize)]
1063pub struct FuturesPublicExecutionEvent {
1064    #[serde(rename = "Execution")]
1065    pub execution: FuturesPublicExecutionWrapper,
1066}
1067
1068/// Wrapper containing the actual execution data.
1069#[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/// The actual execution/trade data.
1078#[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/// Order information within an execution.
1096#[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// Futures Accounts Models
1115
1116/// Response from the Kraken Futures accounts endpoint.
1117#[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/// Kraken Futures account type.
1130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1131#[serde(rename_all = "camelCase")]
1132pub enum KrakenFuturesAccountType {
1133    /// Multi-collateral margin account (flex).
1134    MultiCollateralMarginAccount,
1135    /// Single-collateral margin account.
1136    MarginAccount,
1137    /// Cash account (no margin).
1138    CashAccount,
1139    /// Unknown account type.
1140    #[serde(other)]
1141    Unknown,
1142}
1143
1144/// A Kraken Futures account (margin or multi-collateral).
1145#[derive(Debug, Clone, Serialize, Deserialize)]
1146#[serde(rename_all = "camelCase")]
1147pub struct FuturesAccount {
1148    #[serde(rename = "type")]
1149    pub account_type: KrakenFuturesAccountType,
1150    /// Currency of a single-collateral margin account; its `auxiliary` and `marginRequirements`
1151    /// figures are in this currency. Absent on cash and flex accounts.
1152    #[serde(default)]
1153    pub currency: Option<String>,
1154    /// Balances for margin accounts (symbol -> amount).
1155    #[serde(default, with = "decimal_map")]
1156    pub balances: AHashMap<String, Decimal>,
1157    /// Currencies for flex/multi-collateral accounts.
1158    #[serde(default)]
1159    pub currencies: AHashMap<String, FuturesFlexCurrency>,
1160    /// Auxiliary info for margin accounts.
1161    #[serde(default)]
1162    pub auxiliary: Option<FuturesAuxiliary>,
1163    /// Margin requirements.
1164    #[serde(default)]
1165    pub margin_requirements: Option<FuturesMarginRequirements>,
1166    /// Portfolio value (for flex accounts).
1167    #[serde(default, with = "optional_decimal")]
1168    pub portfolio_value: Option<Decimal>,
1169    /// Available margin (for flex accounts).
1170    #[serde(default, with = "optional_decimal")]
1171    pub available_margin: Option<Decimal>,
1172    /// Initial margin (for flex accounts).
1173    #[serde(default, with = "optional_decimal")]
1174    pub initial_margin: Option<Decimal>,
1175    /// Total maintenance margin held for open positions (for flex accounts, in USD).
1176    #[serde(default, with = "optional_decimal")]
1177    pub maintenance_margin: Option<Decimal>,
1178    /// PnL (for flex accounts).
1179    #[serde(default, with = "optional_decimal")]
1180    pub pnl: Option<Decimal>,
1181}
1182
1183/// Currency info for flex/multi-collateral accounts.
1184#[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/// Auxiliary account info for margin accounts.
1198#[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    /// Portfolio value.
1204    #[serde(default, with = "optional_decimal")]
1205    pub pv: Option<Decimal>,
1206    /// Profit/loss.
1207    #[serde(default, with = "optional_decimal")]
1208    pub pnl: Option<Decimal>,
1209    /// Available funds.
1210    #[serde(default, with = "optional_decimal")]
1211    pub af: Option<Decimal>,
1212    #[serde(default, with = "optional_decimal")]
1213    pub funding: Option<Decimal>,
1214}
1215
1216/// Margin requirements for an account.
1217#[derive(Debug, Clone, Serialize, Deserialize)]
1218#[serde(rename_all = "camelCase")]
1219pub struct FuturesMarginRequirements {
1220    /// Initial margin.
1221    #[serde(default, with = "optional_decimal")]
1222    pub im: Option<Decimal>,
1223    /// Maintenance margin.
1224    #[serde(default, with = "optional_decimal")]
1225    pub mm: Option<Decimal>,
1226    /// Liquidation threshold.
1227    #[serde(default, with = "optional_decimal")]
1228    pub lt: Option<Decimal>,
1229    /// Termination threshold.
1230    #[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        // Regression for the venue response shape that broke parsing in production:
1250        // the `cancelStatus.status` field is `noOrdersToCancel` even when one or more
1251        // orders were canceled in the same call. The `cancelledOrders` array carries
1252        // the actual canceled order ids, so the parser must accept this status.
1253        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        // The venue omits makerProtectionMillis entirely on unprotected
1385        // markets; absent must decode the same as no protection configured.
1386        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    /// Each documented event kind maps onto the event type the readers switch on.
1494    #[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    /// An update carries the order after the edit.
1515    #[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    /// The contract name is kept as the venue spells it, and the millisecond timestamps are
1527    /// formatted for the readers, which parse RFC 3339.
1528    #[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    /// A stop row carries no trigger price, which the engine needs to materialize a stop order,
1545    /// so it is reported as the limit or market order it executes as once triggered.
1546    #[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    /// A market order's placeholder price, empty or zero, is no price; a limit order keeps its
1566    /// price even at zero, since futures instruments allow non-positive prices.
1567    #[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    /// A rejected order reports as rejected.
1585    #[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    /// An order whose type the venue reports as `Unknown` establishes no order type, so its row
1597    /// is skipped and counted, while the sibling rows of the page are kept with their fields
1598    /// intact. A not-found event carries no state and is skipped without counting; an
1599    /// undocumented kind may carry state and counts.
1600    #[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    /// A page reaches back to its oldest event whatever the projection keeps, so the oldest time
1635    /// counts a not-found or undocumented element as well as a kept one.
1636    #[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    /// A row skipped for a timestamp before the epoch names the order it carries, so a lookup
1652    /// cannot rule that order out.
1653    #[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    /// A page reaches back to its oldest event whatever the projection keeps, so a skipped row
1665    /// counts toward the oldest time. A skipped row names its order when it carries one; an
1666    /// undocumented kind names none.
1667    #[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    /// `Unknown` is how the venue reports a source type it could not decode, so neither it nor a
1681    /// missing type establishes what the order is: the row is skipped with its price rather than
1682    /// reported as a market order, the sibling row is kept, and the set is incomplete.
1683    #[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    /// A timestamp before the epoch cannot be reported, so the row is skipped rather than dated
1717    /// at the epoch.
1718    #[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    /// The kind is found wherever it sits among the event's keys.
1732    #[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    /// A report is dated at the event when that is later than the order's own update stamp, as
1744    /// for a refused edit.
1745    #[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    /// An empty event object is an undocumented kind, not a failed page.
1757    #[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    /// An order whose direction the venue could not decode has no side to report, so it is skipped.
1769    #[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    /// A venue error reported with a success status fails the read with the venue's reason, not
1783    /// as an empty page.
1784    #[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    /// A body without an `elements` array is not an order history page.
1800    #[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        // Trigger payload uses non-optional enums, so an `"unknown"` on
1814        // triggerSide / triggerSignal must not fail the sendStatus batch.
1815        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}