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    /// The client order ID under the key the open-orders API documents.
264    #[serde(rename = "cliOrdId", default)]
265    pub cli_ord_id: Option<String>,
266    /// The client order ID under the snake-case key the WebSocket feeds use, read as well since the
267    /// key the REST response carries is not verified against the live venue.
268    #[serde(rename = "cli_ord_id", default)]
269    pub cli_ord_id_snake_case: Option<String>,
270}
271
272impl FuturesOpenOrder {
273    /// The client order ID under whichever key the response carries.
274    #[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// Futures Orders Status Models (/orders/status)
295
296/// Order body returned by the `/orders/status` endpoint.
297#[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/// A single order status entry returned by the `/orders/status` endpoint.
318#[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/// Response from the Kraken Futures `/orders/status` endpoint, which reports
330/// orders open or with a fill/cancel event in the last 5 seconds.
331#[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// Futures Order Events Models
344
345/// Wrapper for an order event containing the order data and event type.
346#[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/// The actual order data within an order event.
357#[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/// Order events in the shape the readers consume, built from [`FuturesOrderHistoryResponse`].
382///
383/// This is a projection, not a wire type: a page is decoded as [`FuturesOrderHistoryResponse`]
384/// and converted, so a body of another shape cannot read as an empty page.
385#[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    /// Rows the projection could not represent, which leave the set incomplete.
391    pub skipped_rows: usize,
392    /// The order of each skipped row that names one; a row of an undocumented kind names none.
393    pub skipped_order_ids: Vec<String>,
394    /// The earliest event time on the page in milliseconds, over every element including the
395    /// skipped ones. The venue returns history newest first, so with a continuation token this is
396    /// how far back the page reaches.
397    pub oldest_event_ms: Option<i64>,
398}
399
400// Futures Order History Models
401
402/// Response from the Kraken Futures order history endpoint, `/api/history/v3/orders`.
403///
404/// Each element carries one lifecycle event keyed by its kind, and the order inside it names the
405/// contract as `tradeable` and the side as `direction`, with millisecond timestamps. The venue
406/// can answer with a success status and an error body, so `result` and `error` are read before
407/// the shape is checked; see [`Self::into_order_events`].
408#[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/// One order history element: the event and when it happened.
424#[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/// An order history event, externally tagged by its kind.
433///
434/// The documented kinds are modeled; one the documentation does not list is kept by name so the
435/// surrounding page still decodes.
436#[derive(Debug, Clone)]
437pub enum FuturesOrderHistoryEvent {
438    OrderPlaced(FuturesOrderHistoryOrderEvent),
439    OrderUpdated(FuturesOrderHistoryOrderUpdate),
440    OrderCancelled(FuturesOrderHistoryOrderEvent),
441    OrderRejected(FuturesOrderHistoryOrderEvent),
442    /// An edit the venue refused; the order stands as `old_order`.
443    OrderEditRejected(FuturesOrderHistoryEditRejected),
444    /// A request naming an order the venue does not hold; it carries no order state.
445    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                // The kind is the one key naming a documented event, wherever it sits among any
468                // other keys; the first key is reported when no key does.
469                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/// An event that carries one order: placed, canceled or rejected.
518#[derive(Debug, Clone, Deserialize)]
519#[serde(rename_all = "camelCase")]
520pub struct FuturesOrderHistoryOrderEvent {
521    pub order: FuturesOrderHistoryOrder,
522}
523
524/// An update event, carrying the order before and after the edit.
525#[derive(Debug, Clone, Deserialize)]
526#[serde(rename_all = "camelCase")]
527pub struct FuturesOrderHistoryOrderUpdate {
528    pub new_order: FuturesOrderHistoryOrder,
529}
530
531/// A refused edit, carrying the order as it stands.
532#[derive(Debug, Clone, Deserialize)]
533#[serde(rename_all = "camelCase")]
534pub struct FuturesOrderHistoryEditRejected {
535    pub old_order: FuturesOrderHistoryOrder,
536}
537
538/// A request for an order the venue does not hold.
539#[derive(Debug, Clone, Deserialize)]
540#[serde(rename_all = "camelCase")]
541pub struct FuturesOrderHistoryOrderNotFound {
542    #[serde(default)]
543    pub order_id: Option<String>,
544}
545
546/// An order as the history endpoint reports it.
547#[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    /// Absent for a market order, which the venue reports as zero or as an empty string.
558    #[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    /// Projects each element onto the event wrapper the readers consume.
577    ///
578    /// A venue error reported with a success status fails the read with the venue's reason, and a
579    /// body without an `elements` array fails it as a parse error, so neither reads as an empty
580    /// page. An update, and a refused edit, are represented by the order as it stands afterwards.
581    /// A not-found event carries no order state and is skipped. A kind the documentation does not
582    /// list, an order whose direction the venue could not decode, an order whose type is missing
583    /// or reported as `Unknown`, and an order with a timestamp before the epoch may carry order
584    /// state the projection cannot read, so each is skipped with a warning and counted in
585    /// `skipped_rows`, which leaves the set incomplete. `Unknown` is how the venue reports a
586    /// source type it could not decode, so it establishes no order type, and in particular not a
587    /// market order. The venue fills the required `limitPrice` of a market order with a zero or
588    /// an empty string and a missing client id with an empty string, so a market-like order (a
589    /// market order or a venue-initiated kind) carries no price and an empty client id is
590    /// `None`; a limit order keeps its price even at zero, since futures instruments allow
591    /// non-positive prices. The contract name is kept as the venue spells it;
592    /// the readers resolve it case-insensitively. The history order carries no trigger price, and
593    /// the engine cannot materialize a stop order without one, so a stop row is reported as the
594    /// limit or market order it executes as once triggered.
595    ///
596    /// # Errors
597    ///
598    /// Returns an error if the body carries a venue error or has no `elements` array.
599    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                // The event time is when this state became known; a refused edit, for one,
657                // leaves the order's own stamp at its last change.
658                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                // `Unknown` is how the venue reports a source type it could not decode, so
690                // neither it nor a missing type establishes what the order is.
691                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                    // A stop with the placeholder price has no limit leg.
727                    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// Futures Fills Models
771
772#[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    /// The client order ID under the key the fills API documents.
788    #[serde(rename = "cliOrdId", default)]
789    pub cli_ord_id: Option<String>,
790    /// The client order ID under the snake-case key the WebSocket feeds use, read as well since the
791    /// key the REST response carries is not verified against the live venue.
792    #[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    /// The client order ID under whichever key the response carries.
802    #[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// Futures Positions Models
823
824#[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// Futures Order Execution Models
851
852#[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    // Execution event fields
891    #[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    // Reject event fields
904    #[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/// Response for batch cancel operations via `/derivatives/api/v3/batchorder`.
1016///
1017/// When sending only cancel operations, the response has a different format
1018/// with individual cancel status items.
1019#[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// Futures Public Executions Models
1071
1072/// Response from the Kraken Futures public executions endpoint.
1073#[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/// A single execution element from the public executions response.
1084#[derive(Debug, Clone, Serialize, Deserialize)]
1085pub struct FuturesPublicExecutionElement {
1086    pub uid: String,
1087    pub timestamp: i64,
1088    pub event: FuturesPublicExecutionEvent,
1089}
1090
1091/// The event wrapper containing the execution details.
1092#[derive(Debug, Clone, Serialize, Deserialize)]
1093pub struct FuturesPublicExecutionEvent {
1094    #[serde(rename = "Execution")]
1095    pub execution: FuturesPublicExecutionWrapper,
1096}
1097
1098/// Wrapper containing the actual execution data.
1099#[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/// The actual execution/trade data.
1108#[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/// Order information within an execution.
1126#[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// Futures Accounts Models
1145
1146/// Response from the Kraken Futures accounts endpoint.
1147#[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/// Kraken Futures account type.
1160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1161#[serde(rename_all = "camelCase")]
1162pub enum KrakenFuturesAccountType {
1163    /// Multi-collateral margin account (flex).
1164    MultiCollateralMarginAccount,
1165    /// Single-collateral margin account.
1166    MarginAccount,
1167    /// Cash account (no margin).
1168    CashAccount,
1169    /// Unknown account type.
1170    #[serde(other)]
1171    Unknown,
1172}
1173
1174/// A Kraken Futures account (margin or multi-collateral).
1175#[derive(Debug, Clone, Serialize, Deserialize)]
1176#[serde(rename_all = "camelCase")]
1177pub struct FuturesAccount {
1178    #[serde(rename = "type")]
1179    pub account_type: KrakenFuturesAccountType,
1180    /// Currency of a single-collateral margin account; its `auxiliary` and `marginRequirements`
1181    /// figures are in this currency. Absent on cash and flex accounts.
1182    #[serde(default)]
1183    pub currency: Option<String>,
1184    /// Balances for margin accounts (symbol -> amount).
1185    #[serde(default, with = "decimal_map")]
1186    pub balances: AHashMap<String, Decimal>,
1187    /// Currencies for flex/multi-collateral accounts.
1188    #[serde(default)]
1189    pub currencies: AHashMap<String, FuturesFlexCurrency>,
1190    /// Auxiliary info for margin accounts.
1191    #[serde(default)]
1192    pub auxiliary: Option<FuturesAuxiliary>,
1193    /// Margin requirements.
1194    #[serde(default)]
1195    pub margin_requirements: Option<FuturesMarginRequirements>,
1196    /// Portfolio value (for flex accounts).
1197    #[serde(default, with = "optional_decimal")]
1198    pub portfolio_value: Option<Decimal>,
1199    /// Available margin (for flex accounts).
1200    #[serde(default, with = "optional_decimal")]
1201    pub available_margin: Option<Decimal>,
1202    /// Initial margin (for flex accounts).
1203    #[serde(default, with = "optional_decimal")]
1204    pub initial_margin: Option<Decimal>,
1205    /// Total maintenance margin held for open positions (for flex accounts, in USD).
1206    #[serde(default, with = "optional_decimal")]
1207    pub maintenance_margin: Option<Decimal>,
1208    /// PnL (for flex accounts).
1209    #[serde(default, with = "optional_decimal")]
1210    pub pnl: Option<Decimal>,
1211}
1212
1213/// Currency info for flex/multi-collateral accounts.
1214#[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/// Auxiliary account info for margin accounts.
1228#[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    /// Portfolio value.
1234    #[serde(default, with = "optional_decimal")]
1235    pub pv: Option<Decimal>,
1236    /// Profit/loss.
1237    #[serde(default, with = "optional_decimal")]
1238    pub pnl: Option<Decimal>,
1239    /// Available funds.
1240    #[serde(default, with = "optional_decimal")]
1241    pub af: Option<Decimal>,
1242    #[serde(default, with = "optional_decimal")]
1243    pub funding: Option<Decimal>,
1244}
1245
1246/// Margin requirements for an account.
1247#[derive(Debug, Clone, Serialize, Deserialize)]
1248#[serde(rename_all = "camelCase")]
1249pub struct FuturesMarginRequirements {
1250    /// Initial margin.
1251    #[serde(default, with = "optional_decimal")]
1252    pub im: Option<Decimal>,
1253    /// Maintenance margin.
1254    #[serde(default, with = "optional_decimal")]
1255    pub mm: Option<Decimal>,
1256    /// Liquidation threshold.
1257    #[serde(default, with = "optional_decimal")]
1258    pub lt: Option<Decimal>,
1259    /// Termination threshold.
1260    #[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        // Regression for the venue response shape that broke parsing in production:
1280        // the `cancelStatus.status` field is `noOrdersToCancel` even when one or more
1281        // orders were canceled in the same call. The `cancelledOrders` array carries
1282        // the actual canceled order ids, so the parser must accept this status.
1283        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        // The venue omits makerProtectionMillis entirely on unprotected
1415        // markets; absent must decode the same as no protection configured.
1416        let unprotected = &response.instruments[1];
1417        assert_eq!(unprotected.symbol, "PF_ETHUSD");
1418        assert_eq!(unprotected.maker_protection_millis, None);
1419    }
1420
1421    /// The open-orders and fills APIs document the client order ID as `cliOrdId`. The snake-case
1422    /// key is read as well, and a body carrying both reads the documented one rather than failing.
1423    #[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    /// Each documented event kind maps onto the event type the readers switch on.
1548    #[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    /// An update carries the order after the edit.
1569    #[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    /// The contract name is kept as the venue spells it, and the millisecond timestamps are
1581    /// formatted for the readers, which parse RFC 3339.
1582    #[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    /// A stop row carries no trigger price, which the engine needs to materialize a stop order,
1599    /// so it is reported as the limit or market order it executes as once triggered.
1600    #[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    /// A market order's placeholder price, empty or zero, is no price; a limit order keeps its
1620    /// price even at zero, since futures instruments allow non-positive prices.
1621    #[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    /// A rejected order reports as rejected.
1639    #[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    /// An order whose type the venue reports as `Unknown` establishes no order type, so its row
1651    /// is skipped and counted, while the sibling rows of the page are kept with their fields
1652    /// intact. A not-found event carries no state and is skipped without counting; an
1653    /// undocumented kind may carry state and counts.
1654    #[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    /// A page reaches back to its oldest event whatever the projection keeps, so the oldest time
1689    /// counts a not-found or undocumented element as well as a kept one.
1690    #[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    /// A row skipped for a timestamp before the epoch names the order it carries, so a lookup
1706    /// cannot rule that order out.
1707    #[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    /// A page reaches back to its oldest event whatever the projection keeps, so a skipped row
1719    /// counts toward the oldest time. A skipped row names its order when it carries one; an
1720    /// undocumented kind names none.
1721    #[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    /// `Unknown` is how the venue reports a source type it could not decode, so neither it nor a
1735    /// missing type establishes what the order is: the row is skipped with its price rather than
1736    /// reported as a market order, the sibling row is kept, and the set is incomplete.
1737    #[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    /// A timestamp before the epoch cannot be reported, so the row is skipped rather than dated
1771    /// at the epoch.
1772    #[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    /// The kind is found wherever it sits among the event's keys.
1786    #[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    /// A report is dated at the event when that is later than the order's own update stamp, as
1798    /// for a refused edit.
1799    #[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    /// An empty event object is an undocumented kind, not a failed page.
1811    #[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    /// An order whose direction the venue could not decode has no side to report, so it is skipped.
1823    #[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    /// A venue error reported with a success status fails the read with the venue's reason, not
1837    /// as an empty page.
1838    #[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    /// A body without an `elements` array is not an order history page.
1854    #[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        // Trigger payload uses non-optional enums, so an `"unknown"` on
1868        // triggerSide / triggerSignal must not fail the sendStatus batch.
1869        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}