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