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 rust_decimal::Decimal;
20use serde::{Deserialize, Serialize};
21
22use crate::common::{
23    enums::{
24        KrakenApiResult, KrakenFillType, KrakenFuturesOrderEventType,
25        KrakenFuturesOrderLifecycleStatus, KrakenFuturesOrderStatus, KrakenFuturesOrderType,
26        KrakenInstrumentType, KrakenOrderSide, KrakenPositionSide, KrakenSendStatus,
27        KrakenTriggerSide, KrakenTriggerSignal,
28    },
29    serialization::{decimal, decimal_map, deserialize_decimal_pair, optional_decimal},
30};
31
32// Futures Instruments Models
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct FuturesMarginLevel {
37    /// Number of contracts (for inverse futures) or notional units (for flexible futures).
38    /// The field name varies: `contracts` for inverse, `numNonContractUnits` for flexible.
39    #[serde(alias = "numNonContractUnits", default, with = "decimal")]
40    pub contracts: Decimal,
41    #[serde(with = "decimal")]
42    pub initial_margin: Decimal,
43    #[serde(with = "decimal")]
44    pub maintenance_margin: Decimal,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase")]
49pub struct FuturesInstrument {
50    pub symbol: String,
51    #[serde(rename = "type")]
52    pub instrument_type: KrakenInstrumentType,
53    /// Only present for inverse futures, not for flexible futures.
54    #[serde(default)]
55    pub underlying: Option<String>,
56    #[serde(with = "decimal")]
57    pub tick_size: Decimal,
58    #[serde(with = "decimal")]
59    pub contract_size: Decimal,
60    pub tradeable: bool,
61    #[serde(default, with = "optional_decimal")]
62    pub impact_mid_size: Option<Decimal>,
63    #[serde(default, with = "optional_decimal")]
64    pub max_position_size: Option<Decimal>,
65    pub opening_date: String,
66    pub margin_levels: Vec<FuturesMarginLevel>,
67    #[serde(default)]
68    pub funding_rate_coefficient: Option<i32>,
69    #[serde(default, with = "optional_decimal")]
70    pub max_relative_funding_rate: Option<Decimal>,
71    #[serde(default)]
72    pub isin: Option<String>,
73    pub contract_value_trade_precision: i32,
74    pub post_only: bool,
75    /// Maker Protection hold window in milliseconds for this market.
76    ///
77    /// Only present when the venue has Maker Protection configured for the
78    /// market; absent means no hold (treat the same as zero).
79    #[serde(default)]
80    pub maker_protection_millis: Option<i64>,
81    #[serde(default)]
82    pub fee_schedule_uid: Option<String>,
83    pub mtf: bool,
84    pub base: String,
85    pub quote: String,
86    pub pair: String,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct FuturesInstrumentsResponse {
91    pub result: KrakenApiResult,
92    pub instruments: Vec<FuturesInstrument>,
93}
94
95// Futures Ticker Models
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct FuturesTicker {
100    pub symbol: String,
101    #[serde(default, with = "optional_decimal")]
102    pub last: Option<Decimal>,
103    #[serde(default)]
104    pub last_time: Option<String>,
105    pub tag: String,
106    pub pair: String,
107    #[serde(default, with = "optional_decimal")]
108    pub mark_price: Option<Decimal>,
109    #[serde(default, with = "optional_decimal")]
110    pub bid: Option<Decimal>,
111    #[serde(default, with = "optional_decimal")]
112    pub bid_size: Option<Decimal>,
113    #[serde(default, with = "optional_decimal")]
114    pub ask: Option<Decimal>,
115    #[serde(default, with = "optional_decimal")]
116    pub ask_size: Option<Decimal>,
117    #[serde(rename = "vol24h", default, with = "optional_decimal")]
118    pub vol_24h: Option<Decimal>,
119    #[serde(default, with = "optional_decimal")]
120    pub volume_quote: Option<Decimal>,
121    #[serde(default, with = "optional_decimal")]
122    pub open_interest: Option<Decimal>,
123    #[serde(rename = "open24h", default, with = "optional_decimal")]
124    pub open_24h: Option<Decimal>,
125    #[serde(rename = "high24h", default, with = "optional_decimal")]
126    pub high_24h: Option<Decimal>,
127    #[serde(rename = "low24h", default, with = "optional_decimal")]
128    pub low_24h: Option<Decimal>,
129    #[serde(default, with = "optional_decimal")]
130    pub last_size: Option<Decimal>,
131    #[serde(default, with = "optional_decimal")]
132    pub funding_rate: Option<Decimal>,
133    #[serde(default, with = "optional_decimal")]
134    pub funding_rate_prediction: Option<Decimal>,
135    #[serde(default)]
136    pub suspended: bool,
137    #[serde(default, with = "optional_decimal")]
138    pub index_price: Option<Decimal>,
139    #[serde(default)]
140    pub post_only: bool,
141    #[serde(rename = "change24h", default, with = "optional_decimal")]
142    pub change_24h: Option<Decimal>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct FuturesTickersResponse {
148    pub result: KrakenApiResult,
149    #[serde(default)]
150    pub server_time: Option<String>,
151    pub tickers: Vec<FuturesTicker>,
152}
153
154// Futures Order Book Models
155
156/// A `[price, qty]` pair from the Kraken Futures orderbook endpoint.
157#[derive(Debug, Clone, Serialize)]
158pub struct FuturesOrderBookLevel {
159    #[serde(serialize_with = "decimal::serialize")]
160    pub price: Decimal,
161    #[serde(serialize_with = "decimal::serialize")]
162    pub qty: Decimal,
163}
164
165impl<'de> serde::Deserialize<'de> for FuturesOrderBookLevel {
166    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
167    where
168        D: serde::Deserializer<'de>,
169    {
170        let arr = deserialize_decimal_pair(deserializer)?;
171        Ok(Self {
172            price: arr.0,
173            qty: arr.1,
174        })
175    }
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct FuturesOrderBookData {
181    pub bids: Vec<FuturesOrderBookLevel>,
182    pub asks: Vec<FuturesOrderBookLevel>,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct FuturesOrderBookResponse {
188    pub result: KrakenApiResult,
189    pub order_book: FuturesOrderBookData,
190    #[serde(default)]
191    pub server_time: Option<String>,
192}
193
194// Futures Historical Funding Rates Models
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct FuturesHistoricalFundingRate {
199    pub timestamp: String,
200    #[serde(with = "decimal")]
201    pub relative_funding_rate: Decimal,
202    #[serde(with = "decimal")]
203    pub funding_rate: Decimal,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
207#[serde(rename_all = "camelCase")]
208pub struct FuturesHistoricalFundingRatesResponse {
209    pub result: KrakenApiResult,
210    pub rates: Vec<FuturesHistoricalFundingRate>,
211}
212
213// Futures OHLC (Candles) Models
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct FuturesCandle {
217    pub time: i64,
218    pub open: String,
219    pub high: String,
220    pub low: String,
221    pub close: String,
222    pub volume: String,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct FuturesCandlesResponse {
227    pub candles: Vec<FuturesCandle>,
228}
229
230// Futures Open Orders Models
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
233#[serde(rename_all = "camelCase")]
234pub struct FuturesOpenOrder {
235    #[serde(rename = "order_id")]
236    pub order_id: String,
237    pub symbol: String,
238    pub side: KrakenOrderSide,
239    pub order_type: KrakenFuturesOrderType,
240    #[serde(default, with = "optional_decimal")]
241    pub limit_price: Option<Decimal>,
242    #[serde(default, with = "optional_decimal")]
243    pub stop_price: Option<Decimal>,
244    #[serde(default, with = "optional_decimal")]
245    pub unfilled_size: Option<Decimal>,
246    pub received_time: String,
247    pub status: KrakenFuturesOrderStatus,
248    #[serde(with = "decimal")]
249    pub filled_size: Decimal,
250    #[serde(default)]
251    pub reduce_only: Option<bool>,
252    pub last_update_time: String,
253    #[serde(default)]
254    pub trigger_signal: Option<KrakenTriggerSignal>,
255    #[serde(rename = "cli_ord_id", default)]
256    pub cli_ord_id: Option<String>,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
260#[serde(rename_all = "camelCase")]
261pub struct FuturesOpenOrdersResponse {
262    pub result: KrakenApiResult,
263    #[serde(default)]
264    pub server_time: Option<String>,
265    #[serde(default)]
266    pub error: Option<String>,
267    #[serde(default)]
268    pub open_orders: Vec<FuturesOpenOrder>,
269}
270
271// Futures Orders Status Models (/orders/status)
272
273/// Order body returned by the `/orders/status` endpoint.
274#[derive(Debug, Clone, Serialize, Deserialize)]
275#[serde(rename_all = "camelCase")]
276pub struct FuturesCachedOrder {
277    pub order_id: String,
278    #[serde(default)]
279    pub cli_ord_id: Option<String>,
280    pub symbol: String,
281    pub side: KrakenOrderSide,
282    #[serde(default, with = "optional_decimal")]
283    pub quantity: Option<Decimal>,
284    #[serde(default, with = "optional_decimal")]
285    pub filled: Option<Decimal>,
286    #[serde(default, with = "optional_decimal")]
287    pub limit_price: Option<Decimal>,
288    #[serde(default)]
289    pub reduce_only: bool,
290    pub timestamp: String,
291    pub last_update_timestamp: String,
292}
293
294/// A single order status entry returned by the `/orders/status` endpoint.
295#[derive(Debug, Clone, Serialize, Deserialize)]
296#[serde(rename_all = "camelCase")]
297pub struct FuturesOrderStatusDetails {
298    pub order: FuturesCachedOrder,
299    pub status: KrakenFuturesOrderLifecycleStatus,
300    #[serde(default)]
301    pub update_reason: Option<String>,
302    #[serde(default)]
303    pub error: Option<String>,
304}
305
306/// Response from the Kraken Futures `/orders/status` endpoint, which reports
307/// orders open or with a fill/cancel event in the last 5 seconds.
308#[derive(Debug, Clone, Serialize, Deserialize)]
309#[serde(rename_all = "camelCase")]
310pub struct FuturesOrdersStatusResponse {
311    pub result: KrakenApiResult,
312    #[serde(default)]
313    pub server_time: Option<String>,
314    #[serde(default)]
315    pub error: Option<String>,
316    #[serde(default)]
317    pub orders: Vec<FuturesOrderStatusDetails>,
318}
319
320// Futures Order Events Models (v2 API)
321
322/// Wrapper for an order event containing the order data and event type.
323#[derive(Debug, Clone, Serialize, Deserialize)]
324#[serde(rename_all = "camelCase")]
325pub struct FuturesOrderEventWrapper {
326    pub order: FuturesOrderEvent,
327    #[serde(rename = "type")]
328    pub event_type: KrakenFuturesOrderEventType,
329    #[serde(default, with = "optional_decimal")]
330    pub reduced_quantity: Option<Decimal>,
331}
332
333/// The actual order data within an order event.
334#[derive(Debug, Clone, Serialize, Deserialize)]
335#[serde(rename_all = "camelCase")]
336pub struct FuturesOrderEvent {
337    pub order_id: String,
338    #[serde(default)]
339    pub cli_ord_id: Option<String>,
340    #[serde(rename = "type")]
341    pub order_type: KrakenFuturesOrderType,
342    pub symbol: String,
343    pub side: KrakenOrderSide,
344    #[serde(with = "decimal")]
345    pub quantity: Decimal,
346    #[serde(with = "decimal")]
347    pub filled: Decimal,
348    #[serde(default, with = "optional_decimal")]
349    pub limit_price: Option<Decimal>,
350    #[serde(default, with = "optional_decimal")]
351    pub stop_price: Option<Decimal>,
352    pub timestamp: String,
353    pub last_update_timestamp: String,
354    #[serde(default)]
355    pub reduce_only: bool,
356}
357
358/// Response from the Kraken Futures order events v2 endpoint.
359#[derive(Debug, Clone, Serialize, Deserialize)]
360#[serde(rename_all = "camelCase")]
361pub struct FuturesOrderEventsResponse {
362    #[serde(default)]
363    pub server_time: Option<String>,
364    #[serde(default)]
365    pub order_events: Vec<FuturesOrderEventWrapper>,
366    #[serde(default)]
367    pub continuation_token: Option<String>,
368}
369
370// Futures Fills Models
371
372#[derive(Debug, Clone, Serialize, Deserialize)]
373#[serde(rename_all = "camelCase")]
374pub struct FuturesFill {
375    #[serde(rename = "fill_id")]
376    pub fill_id: String,
377    pub symbol: String,
378    pub side: KrakenOrderSide,
379    #[serde(rename = "order_id")]
380    pub order_id: String,
381    pub fill_time: String,
382    #[serde(with = "decimal")]
383    pub size: Decimal,
384    #[serde(with = "decimal")]
385    pub price: Decimal,
386    pub fill_type: KrakenFillType,
387    #[serde(rename = "cli_ord_id", default)]
388    pub cli_ord_id: Option<String>,
389    #[serde(rename = "fee_paid", default, with = "optional_decimal")]
390    pub fee_paid: Option<Decimal>,
391    #[serde(rename = "fee_currency", default)]
392    pub fee_currency: Option<String>,
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
396#[serde(rename_all = "camelCase")]
397pub struct FuturesFillsResponse {
398    pub result: KrakenApiResult,
399    #[serde(default)]
400    pub server_time: Option<String>,
401    #[serde(default)]
402    pub error: Option<String>,
403    #[serde(default)]
404    pub fills: Vec<FuturesFill>,
405}
406
407// Futures Positions Models
408
409#[derive(Debug, Clone, Serialize, Deserialize)]
410#[serde(rename_all = "camelCase")]
411pub struct FuturesPosition {
412    pub side: KrakenPositionSide,
413    pub symbol: String,
414    #[serde(with = "decimal")]
415    pub price: Decimal,
416    pub fill_time: String,
417    #[serde(with = "decimal")]
418    pub size: Decimal,
419    #[serde(default, with = "optional_decimal")]
420    pub unrealized_funding: Option<Decimal>,
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize)]
424#[serde(rename_all = "camelCase")]
425pub struct FuturesOpenPositionsResponse {
426    pub result: KrakenApiResult,
427    #[serde(default)]
428    pub server_time: Option<String>,
429    #[serde(default)]
430    pub error: Option<String>,
431    #[serde(default)]
432    pub open_positions: Vec<FuturesPosition>,
433}
434
435// Futures Order Execution Models
436
437#[derive(Debug, Clone, Serialize, Deserialize)]
438#[serde(rename_all = "camelCase")]
439pub struct FuturesSendOrderResponse {
440    pub result: KrakenApiResult,
441    #[serde(default)]
442    pub server_time: Option<String>,
443    #[serde(default)]
444    pub error: Option<String>,
445    pub send_status: Option<FuturesSendStatus>,
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize)]
449#[serde(rename_all = "camelCase")]
450pub struct FuturesSendStatus {
451    #[serde(rename = "order_id", default)]
452    pub order_id: Option<String>,
453    #[serde(rename = "order_tag", default)]
454    pub order_tag: Option<String>,
455    pub status: String,
456    #[serde(default)]
457    pub order_events: Option<Vec<FuturesSendOrderEvent>>,
458    #[serde(rename = "cli_ord_id", default)]
459    pub cli_ord_id: Option<String>,
460    #[serde(rename = "receivedTime", default)]
461    pub received_time: Option<String>,
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize)]
465#[serde(rename_all = "camelCase")]
466pub struct FuturesSendOrderEvent {
467    #[serde(rename = "type")]
468    pub event_type: KrakenFuturesOrderEventType,
469    #[serde(default)]
470    pub order: Option<FuturesOrderEventData>,
471    #[serde(default)]
472    pub order_trigger: Option<FuturesOrderTriggerData>,
473    #[serde(default, with = "optional_decimal")]
474    pub reduced_quantity: Option<Decimal>,
475    // Execution event fields
476    #[serde(rename = "executionId", default)]
477    pub execution_id: Option<String>,
478    #[serde(default, with = "optional_decimal")]
479    pub price: Option<Decimal>,
480    #[serde(default, with = "optional_decimal")]
481    pub amount: Option<Decimal>,
482    #[serde(rename = "orderPriorEdit", default)]
483    pub order_prior_edit: Option<Box<FuturesOrderEventData>>,
484    #[serde(rename = "orderPriorExecution", default)]
485    pub order_prior_execution: Option<Box<FuturesOrderEventData>>,
486    #[serde(rename = "takerReducedQuantity", default, with = "optional_decimal")]
487    pub taker_reduced_quantity: Option<Decimal>,
488    // Reject event fields
489    #[serde(default)]
490    pub reason: Option<String>,
491    #[serde(default)]
492    pub uid: Option<String>,
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize)]
496#[serde(rename_all = "camelCase")]
497pub struct FuturesOrderEventData {
498    #[serde(rename = "orderId")]
499    pub order_id: String,
500    #[serde(rename = "cliOrdId", default)]
501    pub cli_ord_id: Option<String>,
502    #[serde(rename = "type")]
503    pub order_type: KrakenFuturesOrderType,
504    pub symbol: String,
505    pub side: KrakenOrderSide,
506    #[serde(with = "decimal")]
507    pub quantity: Decimal,
508    #[serde(with = "decimal")]
509    pub filled: Decimal,
510    #[serde(rename = "limitPrice", default, with = "optional_decimal")]
511    pub limit_price: Option<Decimal>,
512    #[serde(rename = "stopPrice", default, with = "optional_decimal")]
513    pub stop_price: Option<Decimal>,
514    pub timestamp: String,
515    #[serde(rename = "lastUpdateTimestamp")]
516    pub last_update_timestamp: String,
517    #[serde(rename = "reduceOnly", default)]
518    pub reduce_only: bool,
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize)]
522#[serde(rename_all = "camelCase")]
523pub struct FuturesOrderTriggerData {
524    pub uid: String,
525    #[serde(rename = "clientId", default)]
526    pub client_id: Option<String>,
527    #[serde(rename = "type")]
528    pub order_type: KrakenFuturesOrderType,
529    pub symbol: String,
530    pub side: KrakenOrderSide,
531    #[serde(with = "decimal")]
532    pub quantity: Decimal,
533    #[serde(rename = "limitPrice", default, with = "optional_decimal")]
534    pub limit_price: Option<Decimal>,
535    #[serde(rename = "limitPriceOffsetValue", default, with = "optional_decimal")]
536    pub limit_price_offset_value: Option<Decimal>,
537    #[serde(rename = "limitPriceOffsetUnit", default)]
538    pub limit_price_offset_unit: Option<String>,
539    #[serde(rename = "triggerPrice")]
540    #[serde(with = "decimal")]
541    pub trigger_price: Decimal,
542    #[serde(rename = "triggerSide")]
543    pub trigger_side: KrakenTriggerSide,
544    #[serde(rename = "triggerSignal")]
545    pub trigger_signal: KrakenTriggerSignal,
546    #[serde(rename = "reduceOnly", default)]
547    pub reduce_only: bool,
548    pub timestamp: String,
549    #[serde(rename = "lastUpdateTimestamp")]
550    pub last_update_timestamp: String,
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize)]
554#[serde(rename_all = "camelCase")]
555pub struct FuturesCancelOrderResponse {
556    pub result: KrakenApiResult,
557    #[serde(default)]
558    pub server_time: Option<String>,
559    pub cancel_status: FuturesCancelStatus,
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize)]
563#[serde(rename_all = "camelCase")]
564pub struct FuturesCancelStatus {
565    pub status: KrakenSendStatus,
566    #[serde(rename = "order_id", default)]
567    pub order_id: Option<String>,
568    #[serde(rename = "cli_ord_id", default)]
569    pub cli_ord_id: Option<String>,
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize)]
573#[serde(rename_all = "camelCase")]
574pub struct FuturesEditOrderResponse {
575    pub result: KrakenApiResult,
576    #[serde(default)]
577    pub server_time: Option<String>,
578    pub edit_status: FuturesEditStatus,
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize)]
582#[serde(rename_all = "camelCase")]
583pub struct FuturesEditStatus {
584    pub status: String,
585    #[serde(rename = "order_id", default)]
586    pub order_id: Option<String>,
587    #[serde(rename = "cli_ord_id", default)]
588    pub cli_ord_id: Option<String>,
589}
590
591#[derive(Debug, Clone, Serialize, Deserialize)]
592#[serde(rename_all = "camelCase")]
593pub struct FuturesBatchOrderResponse {
594    pub result: KrakenApiResult,
595    #[serde(default)]
596    pub server_time: Option<String>,
597    pub batch_status: Vec<FuturesSendStatus>,
598}
599
600/// Response for batch cancel operations via `/derivatives/api/v3/batchorder`.
601///
602/// When sending only cancel operations, the response has a different format
603/// with individual cancel status items.
604#[derive(Debug, Clone, Serialize, Deserialize)]
605#[serde(rename_all = "camelCase")]
606pub struct FuturesBatchCancelResponse {
607    pub result: KrakenApiResult,
608    #[serde(default)]
609    pub server_time: Option<String>,
610    #[serde(default)]
611    pub error: Option<String>,
612    #[serde(default)]
613    pub batch_status: Vec<FuturesBatchCancelStatus>,
614}
615
616#[derive(Debug, Clone, Serialize, Deserialize)]
617#[serde(rename_all = "camelCase")]
618pub struct FuturesBatchCancelStatus {
619    #[serde(default)]
620    pub order_id: Option<String>,
621    #[serde(default)]
622    pub cli_ord_id: Option<String>,
623    #[serde(default)]
624    pub status: Option<KrakenSendStatus>,
625    #[serde(default)]
626    pub cancel_status: Option<FuturesCancelStatus>,
627}
628
629#[derive(Debug, Clone, Serialize, Deserialize)]
630#[serde(rename_all = "camelCase")]
631pub struct FuturesCancelAllOrdersResponse {
632    pub result: KrakenApiResult,
633    #[serde(default)]
634    pub server_time: Option<String>,
635    pub cancel_status: FuturesCancelAllStatus,
636}
637
638#[derive(Debug, Clone, Serialize, Deserialize)]
639#[serde(rename_all = "camelCase")]
640pub struct FuturesCancelAllStatus {
641    pub status: KrakenSendStatus,
642    #[serde(default)]
643    pub cancelled_orders: Vec<CancelledOrder>,
644}
645
646#[derive(Debug, Clone, Serialize, Deserialize)]
647#[serde(rename_all = "camelCase")]
648pub struct CancelledOrder {
649    #[serde(rename = "order_id", default)]
650    pub order_id: Option<String>,
651    #[serde(default)]
652    pub cli_ord_id: Option<String>,
653}
654
655// Futures Public Executions Models
656
657/// Response from the Kraken Futures public executions endpoint.
658#[derive(Debug, Clone, Serialize, Deserialize)]
659#[serde(rename_all = "camelCase")]
660pub struct FuturesPublicExecutionsResponse {
661    pub elements: Vec<FuturesPublicExecutionElement>,
662    #[serde(default)]
663    pub len: Option<i64>,
664    #[serde(default)]
665    pub continuation_token: Option<String>,
666}
667
668/// A single execution element from the public executions response.
669#[derive(Debug, Clone, Serialize, Deserialize)]
670pub struct FuturesPublicExecutionElement {
671    pub uid: String,
672    pub timestamp: i64,
673    pub event: FuturesPublicExecutionEvent,
674}
675
676/// The event wrapper containing the execution details.
677#[derive(Debug, Clone, Serialize, Deserialize)]
678pub struct FuturesPublicExecutionEvent {
679    #[serde(rename = "Execution")]
680    pub execution: FuturesPublicExecutionWrapper,
681}
682
683/// Wrapper containing the actual execution data.
684#[derive(Debug, Clone, Serialize, Deserialize)]
685#[serde(rename_all = "camelCase")]
686pub struct FuturesPublicExecutionWrapper {
687    pub execution: FuturesPublicExecution,
688    #[serde(default)]
689    pub taker_reduced_quantity: Option<String>,
690}
691
692/// The actual execution/trade data.
693#[derive(Debug, Clone, Serialize, Deserialize)]
694#[serde(rename_all = "camelCase")]
695pub struct FuturesPublicExecution {
696    pub uid: String,
697    pub maker_order: FuturesPublicOrder,
698    pub taker_order: FuturesPublicOrder,
699    pub timestamp: i64,
700    pub quantity: String,
701    pub price: String,
702    #[serde(default)]
703    pub mark_price: Option<String>,
704    #[serde(default)]
705    pub limit_filled: Option<bool>,
706    #[serde(default)]
707    pub usd_value: Option<String>,
708}
709
710/// Order information within an execution.
711#[derive(Debug, Clone, Serialize, Deserialize)]
712#[serde(rename_all = "camelCase")]
713pub struct FuturesPublicOrder {
714    pub uid: String,
715    pub tradeable: String,
716    pub direction: String,
717    pub quantity: String,
718    pub timestamp: i64,
719    #[serde(default)]
720    pub limit_price: Option<String>,
721    #[serde(default)]
722    pub order_type: Option<String>,
723    #[serde(default)]
724    pub reduce_only: Option<bool>,
725    #[serde(default)]
726    pub last_update_timestamp: Option<i64>,
727}
728
729// Futures Accounts Models
730
731/// Response from the Kraken Futures accounts endpoint.
732#[derive(Debug, Clone, Serialize, Deserialize)]
733#[serde(rename_all = "camelCase")]
734pub struct FuturesAccountsResponse {
735    pub result: KrakenApiResult,
736    #[serde(default)]
737    pub accounts: AHashMap<String, FuturesAccount>,
738    #[serde(default)]
739    pub error: Option<String>,
740    #[serde(default)]
741    pub server_time: Option<String>,
742}
743
744/// Kraken Futures account type.
745#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
746#[serde(rename_all = "camelCase")]
747pub enum KrakenFuturesAccountType {
748    /// Multi-collateral margin account (flex).
749    MultiCollateralMarginAccount,
750    /// Single-collateral margin account.
751    MarginAccount,
752    /// Cash account (no margin).
753    CashAccount,
754    /// Unknown account type.
755    #[serde(other)]
756    Unknown,
757}
758
759/// A Kraken Futures account (margin or multi-collateral).
760#[derive(Debug, Clone, Serialize, Deserialize)]
761#[serde(rename_all = "camelCase")]
762pub struct FuturesAccount {
763    #[serde(rename = "type")]
764    pub account_type: KrakenFuturesAccountType,
765    /// Balances for margin accounts (symbol -> amount).
766    #[serde(default, with = "decimal_map")]
767    pub balances: AHashMap<String, Decimal>,
768    /// Currencies for flex/multi-collateral accounts.
769    #[serde(default)]
770    pub currencies: AHashMap<String, FuturesFlexCurrency>,
771    /// Auxiliary info for margin accounts.
772    #[serde(default)]
773    pub auxiliary: Option<FuturesAuxiliary>,
774    /// Margin requirements.
775    #[serde(default)]
776    pub margin_requirements: Option<FuturesMarginRequirements>,
777    /// Portfolio value (for flex accounts).
778    #[serde(default, with = "optional_decimal")]
779    pub portfolio_value: Option<Decimal>,
780    /// Available margin (for flex accounts).
781    #[serde(default, with = "optional_decimal")]
782    pub available_margin: Option<Decimal>,
783    /// Initial margin (for flex accounts).
784    #[serde(default, with = "optional_decimal")]
785    pub initial_margin: Option<Decimal>,
786    /// PnL (for flex accounts).
787    #[serde(default, with = "optional_decimal")]
788    pub pnl: Option<Decimal>,
789}
790
791/// Currency info for flex/multi-collateral accounts.
792#[derive(Debug, Clone, Serialize, Deserialize)]
793#[serde(rename_all = "camelCase")]
794pub struct FuturesFlexCurrency {
795    #[serde(with = "decimal")]
796    pub quantity: Decimal,
797    #[serde(default, with = "optional_decimal")]
798    pub value: Option<Decimal>,
799    #[serde(default, with = "optional_decimal")]
800    pub collateral: Option<Decimal>,
801    #[serde(default, with = "optional_decimal")]
802    pub available: Option<Decimal>,
803}
804
805/// Auxiliary account info for margin accounts.
806#[derive(Debug, Clone, Serialize, Deserialize)]
807#[serde(rename_all = "camelCase")]
808pub struct FuturesAuxiliary {
809    #[serde(default, with = "optional_decimal")]
810    pub usd: Option<Decimal>,
811    /// Portfolio value.
812    #[serde(default, with = "optional_decimal")]
813    pub pv: Option<Decimal>,
814    /// Profit/loss.
815    #[serde(default, with = "optional_decimal")]
816    pub pnl: Option<Decimal>,
817    /// Available funds.
818    #[serde(default, with = "optional_decimal")]
819    pub af: Option<Decimal>,
820    #[serde(default, with = "optional_decimal")]
821    pub funding: Option<Decimal>,
822}
823
824/// Margin requirements for an account.
825#[derive(Debug, Clone, Serialize, Deserialize)]
826#[serde(rename_all = "camelCase")]
827pub struct FuturesMarginRequirements {
828    /// Initial margin.
829    #[serde(default, with = "optional_decimal")]
830    pub im: Option<Decimal>,
831    /// Maintenance margin.
832    #[serde(default, with = "optional_decimal")]
833    pub mm: Option<Decimal>,
834    /// Liquidation threshold.
835    #[serde(default, with = "optional_decimal")]
836    pub lt: Option<Decimal>,
837    /// Termination threshold.
838    #[serde(default, with = "optional_decimal")]
839    pub tt: Option<Decimal>,
840}
841
842#[cfg(test)]
843mod tests {
844    use rstest::rstest;
845    use rust_decimal_macros::dec;
846
847    use super::*;
848
849    fn load_test_data(filename: &str) -> String {
850        let path = format!("test_data/{filename}");
851        std::fs::read_to_string(&path)
852            .unwrap_or_else(|e| panic!("Failed to load test data from {path}: {e}"))
853    }
854
855    #[rstest]
856    fn test_parse_futures_cancel_all_orders_with_no_orders_to_cancel_status() {
857        // Regression for the venue response shape that broke parsing in production:
858        // the `cancelStatus.status` field is `noOrdersToCancel` even when one or more
859        // orders were canceled in the same call. The `cancelledOrders` array carries
860        // the actual canceled order ids, so the parser must accept this status.
861        let raw = r#"{
862            "result": "success",
863            "cancelStatus": {
864                "receivedTime": "2026-04-10T13:17:23.291Z",
865                "cancelOnly": "PF_XBTUSD",
866                "status": "noOrdersToCancel",
867                "cancelledOrders": [
868                    {
869                        "order_id": "a182b1c0-cd01-4d1c-853b-605e936f412b",
870                        "cliOrdId": "5f173994-f660-4809-b97a-586221fe5926"
871                    }
872                ],
873                "orderEvents": []
874            },
875            "serverTime": "2026-04-10T13:17:23.291Z"
876        }"#;
877
878        let response: FuturesCancelAllOrdersResponse =
879            serde_json::from_str(raw).expect("Failed to parse cancel-all response");
880
881        assert_eq!(response.result, KrakenApiResult::Success);
882        assert_eq!(
883            response.cancel_status.status,
884            KrakenSendStatus::NoOrdersToCancel
885        );
886        assert_eq!(response.cancel_status.cancelled_orders.len(), 1);
887        assert_eq!(
888            response.cancel_status.cancelled_orders[0]
889                .order_id
890                .as_deref(),
891            Some("a182b1c0-cd01-4d1c-853b-605e936f412b")
892        );
893        assert_eq!(
894            response.cancel_status.cancelled_orders[0]
895                .cli_ord_id
896                .as_deref(),
897            Some("5f173994-f660-4809-b97a-586221fe5926")
898        );
899    }
900
901    #[rstest]
902    fn test_parse_futures_open_orders() {
903        let data = load_test_data("http_futures_open_orders.json");
904        let response: FuturesOpenOrdersResponse =
905            serde_json::from_str(&data).expect("Failed to parse futures open orders");
906
907        assert_eq!(response.result, KrakenApiResult::Success);
908        assert_eq!(response.open_orders.len(), 3);
909
910        let order = &response.open_orders[0];
911        assert_eq!(order.order_id, "2ce038ae-c144-4de7-a0f1-82f7f4fca864");
912        assert_eq!(order.symbol, "PI_ETHUSD");
913        assert_eq!(order.side, KrakenOrderSide::Buy);
914        assert_eq!(order.order_type, KrakenFuturesOrderType::Limit);
915        assert_eq!(order.limit_price, Some(dec!(1200)));
916        assert_eq!(order.unfilled_size, Some(dec!(100)));
917        assert_eq!(order.filled_size, dec!(0));
918
919        let trigger_order = &response.open_orders[1];
920        assert_eq!(
921            trigger_order.order_id,
922            "c8135f52-2a86-4e26-b629-43cc37da9dbf"
923        );
924        assert_eq!(trigger_order.order_type, KrakenFuturesOrderType::TakeProfit);
925        assert_eq!(trigger_order.symbol, "PI_XBTUSD");
926        assert_eq!(trigger_order.side, KrakenOrderSide::Buy);
927        assert_eq!(trigger_order.limit_price, None);
928        assert_eq!(trigger_order.stop_price, Some(dec!(1880.4)));
929        assert_eq!(trigger_order.unfilled_size, None);
930        assert_eq!(trigger_order.received_time, "2023-04-07T15:14:25.995Z");
931        assert_eq!(trigger_order.status, KrakenFuturesOrderStatus::Untouched);
932        assert_eq!(trigger_order.filled_size, dec!(0));
933        assert_eq!(trigger_order.reduce_only, Some(true));
934        assert_eq!(trigger_order.last_update_time, "2023-04-07T15:14:25.995Z");
935        assert_eq!(
936            trigger_order.trigger_signal,
937            Some(KrakenTriggerSignal::Last)
938        );
939        assert_eq!(trigger_order.cli_ord_id, None);
940    }
941
942    #[rstest]
943    fn test_parse_futures_orders_status() {
944        let data = load_test_data("http_futures_orders_status.json");
945        let response: FuturesOrdersStatusResponse =
946            serde_json::from_str(&data).expect("Failed to parse futures orders status");
947
948        assert_eq!(response.result, KrakenApiResult::Success);
949        assert_eq!(response.orders.len(), 2);
950
951        let part_filled = &response.orders[0];
952        assert_eq!(
953            part_filled.order.order_id,
954            "5f6d15a5-8c9e-4b0a-9d3f-5a2b7c8d9e0f"
955        );
956        assert_eq!(
957            part_filled.order.cli_ord_id.as_deref(),
958            Some("uuid-mp-held-001")
959        );
960        assert_eq!(part_filled.order.side, KrakenOrderSide::Buy);
961        assert_eq!(part_filled.order.quantity, Some(dec!(0.001)));
962        assert_eq!(part_filled.order.filled, Some(dec!(0.0004)));
963        assert_eq!(part_filled.order.limit_price, Some(dec!(70000)));
964        assert!(!part_filled.order.reduce_only);
965        assert_eq!(
966            part_filled.status,
967            KrakenFuturesOrderLifecycleStatus::Cancelled
968        );
969        assert_eq!(part_filled.update_reason.as_deref(), Some("PARTIAL_FILL"));
970
971        let open = &response.orders[1];
972        assert_eq!(open.order.quantity, Some(dec!(0.0002)));
973        assert_eq!(open.order.filled, Some(dec!(0)));
974        assert!(open.order.reduce_only);
975        assert_eq!(open.status, KrakenFuturesOrderLifecycleStatus::EnteredBook);
976        assert_eq!(open.update_reason, None);
977    }
978
979    #[rstest]
980    fn test_parse_futures_instruments_maker_protection() {
981        let data = load_test_data("http_futures_instruments_maker_protection.json");
982        let response: FuturesInstrumentsResponse =
983            serde_json::from_str(&data).expect("Failed to parse futures instruments");
984
985        assert_eq!(response.result, KrakenApiResult::Success);
986        assert_eq!(response.instruments.len(), 2);
987
988        let protected = &response.instruments[0];
989        assert_eq!(protected.symbol, "PF_ATOMUSD");
990        assert_eq!(protected.maker_protection_millis, Some(20));
991
992        // The venue omits makerProtectionMillis entirely on unprotected
993        // markets; absent must decode the same as no protection configured.
994        let unprotected = &response.instruments[1];
995        assert_eq!(unprotected.symbol, "PF_ETHUSD");
996        assert_eq!(unprotected.maker_protection_millis, None);
997    }
998
999    #[rstest]
1000    fn test_parse_futures_fills() {
1001        let data = load_test_data("http_futures_fills.json");
1002        let response: FuturesFillsResponse =
1003            serde_json::from_str(&data).expect("Failed to parse futures fills");
1004
1005        assert_eq!(response.result, KrakenApiResult::Success);
1006        assert_eq!(response.fills.len(), 3);
1007
1008        let fill = &response.fills[0];
1009        assert_eq!(fill.fill_id, "cad76f07-814e-4dc6-8478-7867407b6bff");
1010        assert_eq!(fill.symbol, "PI_XBTUSD");
1011        assert_eq!(fill.side, KrakenOrderSide::Buy);
1012        assert_eq!(fill.size, dec!(5000));
1013        assert_eq!(fill.price, dec!(27937.5));
1014        assert_eq!(fill.fill_type, KrakenFillType::Maker);
1015        assert_eq!(fill.fee_currency, Some("BTC".to_string()));
1016        assert_eq!(response.fills[1].fill_type, KrakenFillType::Taker);
1017        assert_eq!(response.fills[2].fill_type, KrakenFillType::Assignee);
1018    }
1019
1020    #[rstest]
1021    fn test_parse_futures_open_positions() {
1022        let data = load_test_data("http_futures_open_positions.json");
1023        let response: FuturesOpenPositionsResponse =
1024            serde_json::from_str(&data).expect("Failed to parse futures open positions");
1025
1026        assert_eq!(response.result, KrakenApiResult::Success);
1027        assert_eq!(response.open_positions.len(), 2);
1028
1029        let position = &response.open_positions[0];
1030        assert_eq!(position.side, KrakenPositionSide::Short);
1031        assert_eq!(position.symbol, "PI_XBTUSD");
1032        assert_eq!(position.size, dec!(8000));
1033        assert!(position.unrealized_funding.is_some());
1034    }
1035
1036    #[rstest]
1037    fn test_parse_futures_orderbook() {
1038        let data = load_test_data("http_futures_orderbook.json");
1039        let response: FuturesOrderBookResponse =
1040            serde_json::from_str(&data).expect("Failed to parse futures orderbook");
1041
1042        assert_eq!(response.result, KrakenApiResult::Success);
1043        assert_eq!(response.order_book.bids.len(), 3);
1044        assert_eq!(response.order_book.asks.len(), 3);
1045
1046        let best_bid = &response.order_book.bids[0];
1047        assert_eq!(best_bid.price, dec!(105900));
1048        assert_eq!(best_bid.qty, dec!(0.5));
1049
1050        let best_ask = &response.order_book.asks[0];
1051        assert_eq!(best_ask.price, dec!(105950));
1052        assert_eq!(best_ask.qty, dec!(0.3));
1053    }
1054
1055    #[rstest]
1056    fn test_parse_futures_historical_funding_rates() {
1057        let data = load_test_data("http_futures_historical_funding_rates.json");
1058        let response: FuturesHistoricalFundingRatesResponse =
1059            serde_json::from_str(&data).expect("Failed to parse historical funding rates");
1060
1061        assert_eq!(response.result, KrakenApiResult::Success);
1062        assert_eq!(response.rates.len(), 3);
1063
1064        let rate = &response.rates[0];
1065        assert_eq!(rate.timestamp, "2025-07-11T08:00:00.000Z");
1066        assert_eq!(rate.relative_funding_rate, dec!(0.0001));
1067        assert_eq!(rate.funding_rate, dec!(0.00005));
1068
1069        let negative_rate = &response.rates[1];
1070        assert_eq!(negative_rate.relative_funding_rate, dec!(-0.00005));
1071    }
1072
1073    #[rstest]
1074    fn test_parse_futures_orderbook_preserves_decimal_precision() {
1075        let data = load_test_data("http_futures_orderbook_precision.json");
1076        let level: FuturesOrderBookLevel = serde_json::from_str(&data).unwrap();
1077
1078        assert_eq!(level.price, dec!(0.1234567890123456789012345678));
1079        assert_eq!(level.qty, dec!(123456789.123456789));
1080    }
1081
1082    #[rstest]
1083    fn test_parse_futures_order_events_uses_enum_event_type() {
1084        let data = load_test_data("http_futures_order_events.json");
1085        let response: FuturesOrderEventsResponse =
1086            serde_json::from_str(&data).expect("Failed to parse futures order events");
1087
1088        assert_eq!(response.order_events.len(), 3);
1089        assert_eq!(
1090            response.order_events[0].event_type,
1091            KrakenFuturesOrderEventType::Place
1092        );
1093        assert_eq!(
1094            response.order_events[1].event_type,
1095            KrakenFuturesOrderEventType::Fill
1096        );
1097        assert_eq!(
1098            response.order_events[2].event_type,
1099            KrakenFuturesOrderEventType::Cancel
1100        );
1101    }
1102
1103    #[rstest]
1104    fn test_parse_futures_order_events_tolerates_unknown_enum_values() {
1105        // Regression for Kraken Futures change log 2026-05-18: an `"unknown"`
1106        // value anywhere in the batch must not fail the surrounding response.
1107        let data = load_test_data("http_futures_order_events_unknown.json");
1108        let response: FuturesOrderEventsResponse =
1109            serde_json::from_str(&data).expect("Failed to parse futures order events with unknown");
1110
1111        assert_eq!(response.order_events.len(), 1);
1112        assert_eq!(
1113            response.order_events[0].order.order_type,
1114            KrakenFuturesOrderType::Unknown
1115        );
1116    }
1117
1118    #[rstest]
1119    fn test_parse_futures_order_trigger_data_tolerates_unknown_enum_values() {
1120        // Trigger payload uses non-optional enums, so an `"unknown"` on
1121        // triggerSide / triggerSignal must not fail the sendStatus batch.
1122        let data = load_test_data("http_send_order_futures_unknown_trigger.json");
1123        let response: FuturesSendOrderResponse =
1124            serde_json::from_str(&data).expect("Failed to parse send-order response with unknown");
1125
1126        let send_status = response.send_status.expect("sendStatus missing");
1127        let order_events = send_status.order_events.expect("orderEvents missing");
1128        let trigger = order_events[0]
1129            .order_trigger
1130            .as_ref()
1131            .expect("orderTrigger missing");
1132
1133        assert_eq!(trigger.order_type, KrakenFuturesOrderType::Unknown);
1134        assert_eq!(trigger.trigger_side, KrakenTriggerSide::Unknown);
1135        assert_eq!(trigger.trigger_signal, KrakenTriggerSignal::Unknown);
1136    }
1137
1138    #[rstest]
1139    fn test_parse_futures_send_order_execution_event_uses_enum_event_type() {
1140        let data = r#"
1141        {
1142          "result": "success",
1143          "sendStatus": {
1144            "status": "placed",
1145            "orderEvents": [
1146              {
1147                "type": "EXECUTION",
1148                "executionId": "c8a35168-8d52-4609-944f-3f32bb0d5c77",
1149                "price": 35000.5,
1150                "amount": 1.25,
1151                "orderPriorExecution": {
1152                  "orderId": "c8a35168-8d52-4609-944f-3f32bb0d5c77",
1153                  "cliOrdId": "test-order-001",
1154                  "type": "lmt",
1155                  "symbol": "PI_XBTUSD",
1156                  "side": "buy",
1157                  "quantity": 2.0,
1158                  "filled": 0.0,
1159                  "limitPrice": 35000.5,
1160                  "timestamp": "2024-01-15T10:30:45.123Z",
1161                  "lastUpdateTimestamp": "2024-01-15T10:30:45.123Z",
1162                  "reduceOnly": false
1163                }
1164              }
1165            ]
1166          }
1167        }
1168        "#;
1169        let response: FuturesSendOrderResponse =
1170            serde_json::from_str(data).expect("Failed to parse futures send order response");
1171
1172        let send_status = response.send_status.expect("sendStatus missing");
1173        let order_events = send_status.order_events.expect("orderEvents missing");
1174
1175        assert_eq!(order_events.len(), 1);
1176        assert_eq!(
1177            order_events[0].event_type,
1178            KrakenFuturesOrderEventType::Execution
1179        );
1180    }
1181}