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