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