Skip to main content

nautilus_kraken/http/futures/
query.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//! Query parameter structs for Kraken Futures HTTP API requests.
17
18use derive_builder::Builder;
19use serde::{Deserialize, Serialize};
20use ustr::Ustr;
21
22use crate::common::enums::{
23    KrakenFuturesOrderType, KrakenFuturesQtyMode, KrakenOrderSide, KrakenTriggerSignal,
24};
25
26/// Parameters for sending an order via `POST /api/v3/sendorder`.
27///
28/// # References
29/// - <https://docs.kraken.com/api/docs/futures-api/trading/send-order/>
30#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
31#[serde(rename_all = "camelCase")]
32#[builder(setter(into, strip_option), build_fn(validate = "Self::validate"))]
33pub struct KrakenFuturesSendOrderParams {
34    /// The symbol of the futures contract (e.g., "PI_XBTUSD").
35    pub symbol: Ustr,
36
37    /// The order side: "buy" or "sell".
38    pub side: KrakenOrderSide,
39
40    /// The order type: lmt, ioc, post, mkt, stp, take_profit, stop_loss.
41    pub order_type: KrakenFuturesOrderType,
42
43    /// The order size in contracts.
44    pub size: String,
45
46    /// Optional client order ID for tracking.
47    #[builder(default)]
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub cli_ord_id: Option<String>,
50
51    /// Limit price (required for limit orders).
52    #[builder(default)]
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub limit_price: Option<String>,
55
56    /// Stop/trigger price (required for stop orders).
57    #[builder(default)]
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub stop_price: Option<String>,
60
61    /// If true, the order will only reduce an existing position.
62    #[builder(default)]
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub reduce_only: Option<bool>,
65
66    /// Trigger signal for stop orders: last, mark, or spot.
67    #[builder(default)]
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub trigger_signal: Option<KrakenTriggerSignal>,
70
71    /// Trailing stop offset value.
72    #[builder(default)]
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub trailing_stop_deviation_unit: Option<String>,
75
76    /// Trailing stop max deviation.
77    #[builder(default)]
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub trailing_stop_max_deviation: Option<String>,
80
81    /// Partner/broker attribution ID.
82    #[builder(default)]
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub broker: Option<Ustr>,
85}
86
87impl KrakenFuturesSendOrderParamsBuilder {
88    fn validate(&self) -> Result<(), String> {
89        // Validate limit price is present for limit-type orders
90        if let Some(ref order_type) = self.order_type {
91            match order_type {
92                KrakenFuturesOrderType::Limit
93                | KrakenFuturesOrderType::Ioc
94                | KrakenFuturesOrderType::Post
95                    if (self.limit_price.is_none()
96                        || self.limit_price.as_ref().unwrap().is_none()) =>
97                {
98                    return Err("limit_price is required for limit orders".to_string());
99                }
100                KrakenFuturesOrderType::Stop | KrakenFuturesOrderType::StopLoss
101                    if (self.stop_price.is_none()
102                        || self.stop_price.as_ref().unwrap().is_none()) =>
103                {
104                    return Err("stop_price is required for stop orders".to_string());
105                }
106                _ => {}
107            }
108        }
109        Ok(())
110    }
111}
112
113/// Parameters for canceling an order via `POST /api/v3/cancelorder`.
114///
115/// # References
116/// - <https://docs.kraken.com/api/docs/futures-api/trading/cancel-order/>
117#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
118#[serde(rename_all = "camelCase")]
119#[builder(setter(into, strip_option))]
120pub struct KrakenFuturesCancelOrderParams {
121    /// The venue order ID to cancel.
122    #[builder(default)]
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub order_id: Option<String>,
125
126    /// The client order ID to cancel.
127    #[builder(default)]
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub cli_ord_id: Option<String>,
130}
131
132/// A batch cancel item for `POST /derivatives/api/v3/batchorder`.
133///
134/// # References
135/// - <https://docs.kraken.com/api/docs/futures-api/trading/send-batch-order/>
136#[derive(Clone, Debug, Serialize, Deserialize)]
137pub struct KrakenFuturesBatchCancelItem {
138    /// The operation type, always "cancel" for this item.
139    pub order: String,
140
141    /// The venue order ID to cancel.
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub order_id: Option<String>,
144
145    /// The client order ID to cancel (alternative to order_id).
146    #[serde(rename = "cliOrdId", skip_serializing_if = "Option::is_none")]
147    pub cli_ord_id: Option<String>,
148}
149
150impl KrakenFuturesBatchCancelItem {
151    /// Create a batch cancel item from a venue order ID.
152    #[must_use]
153    pub fn from_order_id(order_id: impl Into<String>) -> Self {
154        Self {
155            order: "cancel".to_string(),
156            order_id: Some(order_id.into()),
157            cli_ord_id: None,
158        }
159    }
160
161    /// Create a batch cancel item from a client order ID.
162    #[must_use]
163    pub fn from_client_order_id(cli_ord_id: impl Into<String>) -> Self {
164        Self {
165            order: "cancel".to_string(),
166            order_id: None,
167            cli_ord_id: Some(cli_ord_id.into()),
168        }
169    }
170}
171
172/// A batch send item for `POST /derivatives/api/v3/batchorder`.
173///
174/// # References
175/// - <https://docs.kraken.com/api/docs/futures-api/trading/send-batch-order/>
176#[derive(Clone, Debug, Serialize, Deserialize)]
177#[serde(rename_all = "camelCase")]
178pub struct KrakenFuturesBatchSendItem {
179    /// The operation type, always "send" for this item.
180    pub order: String,
181
182    /// An order tag to correlate batch responses with requests.
183    #[serde(rename = "order_tag")]
184    pub order_tag: String,
185
186    /// The symbol of the futures contract.
187    pub symbol: Ustr,
188
189    /// The order side.
190    pub side: KrakenOrderSide,
191
192    /// The order type.
193    pub order_type: KrakenFuturesOrderType,
194
195    /// The order size in contracts.
196    pub size: String,
197
198    /// Optional client order ID for tracking.
199    #[serde(rename = "cliOrdId", skip_serializing_if = "Option::is_none")]
200    pub cli_ord_id: Option<String>,
201
202    /// Limit price (required for limit orders).
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub limit_price: Option<String>,
205
206    /// Stop/trigger price (required for stop orders).
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub stop_price: Option<String>,
209
210    /// If true, the order will only reduce an existing position.
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub reduce_only: Option<bool>,
213
214    /// Trigger signal for stop orders.
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub trigger_signal: Option<KrakenTriggerSignal>,
217}
218
219impl KrakenFuturesBatchSendItem {
220    /// Creates a batch send item from send order params.
221    #[must_use]
222    pub fn from_params(params: KrakenFuturesSendOrderParams, order_tag: impl Into<String>) -> Self {
223        Self {
224            order: "send".to_string(),
225            order_tag: order_tag.into(),
226            symbol: params.symbol,
227            side: params.side,
228            order_type: params.order_type,
229            size: params.size,
230            cli_ord_id: params.cli_ord_id,
231            limit_price: params.limit_price,
232            stop_price: params.stop_price,
233            reduce_only: params.reduce_only,
234            trigger_signal: params.trigger_signal,
235        }
236    }
237}
238
239/// A batch edit item for `POST /derivatives/api/v3/batchorder`.
240///
241/// # References
242/// - <https://docs.kraken.com/api/docs/futures-api/trading/send-batch-order/>
243#[derive(Clone, Debug, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase")]
245pub struct KrakenFuturesBatchEditItem {
246    /// The operation type, always "edit" for this item.
247    pub order: String,
248
249    /// An order tag to correlate batch responses with requests.
250    #[serde(rename = "order_tag")]
251    pub order_tag: String,
252
253    /// The venue order ID to edit.
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub order_id: Option<String>,
256
257    /// The client order ID to edit.
258    #[serde(rename = "cliOrdId", skip_serializing_if = "Option::is_none")]
259    pub cli_ord_id: Option<String>,
260
261    /// New order size.
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub size: Option<String>,
264
265    /// New limit price.
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub limit_price: Option<String>,
268
269    /// New stop price.
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub stop_price: Option<String>,
272
273    /// Whether `size` sets the order's total size or its open size.
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub qty_mode: Option<KrakenFuturesQtyMode>,
276}
277
278impl KrakenFuturesBatchEditItem {
279    /// Creates a batch edit item from edit order params.
280    #[must_use]
281    pub fn from_params(params: KrakenFuturesEditOrderParams, order_tag: impl Into<String>) -> Self {
282        Self {
283            order: "edit".to_string(),
284            order_tag: order_tag.into(),
285            order_id: params.order_id,
286            cli_ord_id: params.cli_ord_id,
287            size: params.size,
288            limit_price: params.limit_price,
289            stop_price: params.stop_price,
290            qty_mode: params.qty_mode,
291        }
292    }
293}
294
295/// Parameters for batch order operations via `POST /derivatives/api/v3/batchorder`.
296///
297/// The batchorder endpoint uses a special body format: `json={"batchOrder": [...]}`
298/// where the JSON is NOT URL-encoded.
299///
300/// # References
301/// - <https://docs.kraken.com/api/docs/futures-api/trading/send-batch-order/>
302#[derive(Clone, Debug, Serialize, Deserialize)]
303#[serde(rename_all = "camelCase")]
304pub struct KrakenFuturesBatchOrderParams<T: Serialize> {
305    /// List of batch order operations.
306    pub batch_order: Vec<T>,
307}
308
309impl<T: Serialize> KrakenFuturesBatchOrderParams<T> {
310    /// Create new batch order params.
311    #[must_use]
312    pub fn new(batch_order: Vec<T>) -> Self {
313        Self { batch_order }
314    }
315
316    /// Serialize to the special `json=...` body format required by this endpoint.
317    pub fn to_body(&self) -> Result<String, serde_json::Error> {
318        let json_str = serde_json::to_string(self)?;
319        Ok(format!("json={json_str}"))
320    }
321}
322
323/// Parameters for editing an order via `POST /api/v3/editorder`.
324///
325/// # References
326/// - <https://docs.kraken.com/api/docs/futures-api/trading/edit-order/>
327#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
328#[serde(rename_all = "camelCase")]
329#[builder(setter(into, strip_option))]
330pub struct KrakenFuturesEditOrderParams {
331    /// The venue order ID to edit.
332    #[builder(default)]
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub order_id: Option<String>,
335
336    /// The client order ID to edit.
337    #[builder(default)]
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub cli_ord_id: Option<String>,
340
341    /// New order size.
342    #[builder(default)]
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub size: Option<String>,
345
346    /// New limit price.
347    #[builder(default)]
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub limit_price: Option<String>,
350
351    /// New stop price.
352    #[builder(default)]
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub stop_price: Option<String>,
355
356    /// Whether `size` sets the order's total size or its open size.
357    #[builder(default)]
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub qty_mode: Option<KrakenFuturesQtyMode>,
360}
361
362/// Parameters for canceling all orders via `POST /api/v3/cancelallorders`.
363///
364/// # References
365/// - <https://docs.kraken.com/api/docs/futures-api/trading/cancel-all-orders/>
366#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
367#[serde(rename_all = "camelCase")]
368#[builder(setter(into, strip_option), default)]
369pub struct KrakenFuturesCancelAllOrdersParams {
370    /// Optional symbol filter - only cancel orders for this symbol.
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub symbol: Option<Ustr>,
373}
374
375/// Parameters for getting open orders via `GET /api/v3/openorders`.
376///
377/// # References
378/// - <https://docs.kraken.com/api/docs/futures-api/trading/get-open-orders/>
379#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
380#[serde(rename_all = "camelCase")]
381#[builder(setter(into, strip_option), default)]
382pub struct KrakenFuturesOpenOrdersParams {
383    // Currently no parameters, but kept for future extensibility
384}
385
386/// Parameters for getting fills via `GET /api/v3/fills`.
387///
388/// # References
389/// - <https://docs.kraken.com/api/docs/futures-api/trading/get-fills/>
390#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
391#[serde(rename_all = "camelCase")]
392#[builder(setter(into, strip_option), default)]
393pub struct KrakenFuturesFillsParams {
394    /// Filter fills after this timestamp (milliseconds).
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub last_fill_time: Option<String>,
397}
398
399/// Parameters for getting open positions via `GET /api/v3/openpositions`.
400///
401/// # References
402/// - <https://docs.kraken.com/api/docs/futures-api/trading/get-open-positions/>
403#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
404#[serde(rename_all = "camelCase")]
405#[builder(setter(into, strip_option), default)]
406pub struct KrakenFuturesOpenPositionsParams {
407    // Currently no parameters, but kept for future extensibility
408}
409
410#[cfg(test)]
411mod tests {
412    use rstest::rstest;
413
414    use super::*;
415
416    #[rstest]
417    fn test_send_order_params_builder() {
418        let params = KrakenFuturesSendOrderParamsBuilder::default()
419            .symbol("PI_XBTUSD")
420            .side(KrakenOrderSide::Buy)
421            .order_type(KrakenFuturesOrderType::Limit)
422            .size("1000")
423            .limit_price("50000.0")
424            .cli_ord_id("test-order-123")
425            .reduce_only(false)
426            .build()
427            .unwrap();
428
429        assert_eq!(params.symbol, Ustr::from("PI_XBTUSD"));
430        assert_eq!(params.side, KrakenOrderSide::Buy);
431        assert_eq!(params.order_type, KrakenFuturesOrderType::Limit);
432        assert_eq!(params.size, "1000");
433        assert_eq!(params.limit_price, Some("50000.0".to_string()));
434        assert_eq!(params.cli_ord_id, Some("test-order-123".to_string()));
435    }
436
437    #[rstest]
438    fn test_send_order_params_serialization() {
439        let params = KrakenFuturesSendOrderParamsBuilder::default()
440            .symbol("PI_XBTUSD")
441            .side(KrakenOrderSide::Buy)
442            .order_type(KrakenFuturesOrderType::Ioc)
443            .size("500")
444            .limit_price("48000.0")
445            .build()
446            .unwrap();
447
448        let json = serde_json::to_string(&params).unwrap();
449        assert!(json.contains("\"orderType\":\"ioc\""));
450        assert!(json.contains("\"limitPrice\":\"48000.0\""));
451    }
452
453    #[rstest]
454    fn test_send_order_params_serialization_with_trigger_signal() {
455        let params = KrakenFuturesSendOrderParamsBuilder::default()
456            .symbol("PI_XBTUSD")
457            .side(KrakenOrderSide::Buy)
458            .order_type(KrakenFuturesOrderType::Stop)
459            .size("500")
460            .stop_price("47000.0")
461            .trigger_signal(KrakenTriggerSignal::Mark)
462            .build()
463            .unwrap();
464
465        let json = serde_json::to_string(&params).unwrap();
466        assert!(json.contains("\"triggerSignal\":\"mark\""));
467        assert!(json.contains("\"stopPrice\":\"47000.0\""));
468    }
469
470    #[rstest]
471    fn test_send_order_params_serialization_with_index_trigger_signal() {
472        let params = KrakenFuturesSendOrderParamsBuilder::default()
473            .symbol("PI_XBTUSD")
474            .side(KrakenOrderSide::Buy)
475            .order_type(KrakenFuturesOrderType::Stop)
476            .size("500")
477            .stop_price("47000.0")
478            .trigger_signal(KrakenTriggerSignal::Index)
479            .build()
480            .unwrap();
481
482        let json = serde_json::to_string(&params).unwrap();
483        assert!(json.contains("\"triggerSignal\":\"spot\""));
484        assert!(json.contains("\"stopPrice\":\"47000.0\""));
485    }
486
487    #[rstest]
488    fn test_send_order_params_missing_limit_price() {
489        let result = KrakenFuturesSendOrderParamsBuilder::default()
490            .symbol("PI_XBTUSD")
491            .side(KrakenOrderSide::Buy)
492            .order_type(KrakenFuturesOrderType::Limit)
493            .size("1000")
494            .build();
495
496        assert!(result.is_err());
497        assert!(result.unwrap_err().to_string().contains("limit_price"));
498    }
499
500    #[rstest]
501    fn test_cancel_order_params_builder() {
502        let params = KrakenFuturesCancelOrderParamsBuilder::default()
503            .order_id("abc-123")
504            .build()
505            .unwrap();
506
507        assert_eq!(params.order_id, Some("abc-123".to_string()));
508    }
509
510    #[rstest]
511    fn test_edit_order_params_builder() {
512        let params = KrakenFuturesEditOrderParamsBuilder::default()
513            .order_id("abc-123")
514            .size("2000")
515            .limit_price("51000.0")
516            .build()
517            .unwrap();
518
519        assert_eq!(params.order_id, Some("abc-123".to_string()));
520        assert_eq!(params.size, Some("2000".to_string()));
521        assert_eq!(params.limit_price, Some("51000.0".to_string()));
522    }
523
524    #[rstest]
525    fn test_batch_send_item_from_params() {
526        let params = KrakenFuturesSendOrderParamsBuilder::default()
527            .symbol("PI_XBTUSD")
528            .side(KrakenOrderSide::Buy)
529            .order_type(KrakenFuturesOrderType::Limit)
530            .size("1000")
531            .limit_price("50000.0")
532            .cli_ord_id("test-batch-1")
533            .build()
534            .unwrap();
535
536        let item = KrakenFuturesBatchSendItem::from_params(params, "0");
537
538        assert_eq!(item.order, "send");
539        assert_eq!(item.order_tag, "0");
540        assert_eq!(item.symbol, Ustr::from("PI_XBTUSD"));
541        assert_eq!(item.side, KrakenOrderSide::Buy);
542        assert_eq!(item.order_type, KrakenFuturesOrderType::Limit);
543        assert_eq!(item.size, "1000");
544        assert_eq!(item.limit_price, Some("50000.0".to_string()));
545        assert_eq!(item.cli_ord_id, Some("test-batch-1".to_string()));
546    }
547
548    #[rstest]
549    fn test_batch_send_item_serialization() {
550        let params = KrakenFuturesSendOrderParamsBuilder::default()
551            .symbol("PI_XBTUSD")
552            .side(KrakenOrderSide::Sell)
553            .order_type(KrakenFuturesOrderType::Market)
554            .size("500")
555            .reduce_only(true)
556            .build()
557            .unwrap();
558
559        let item = KrakenFuturesBatchSendItem::from_params(params, "1");
560        let value = serde_json::to_value(&item).unwrap();
561
562        assert_eq!(
563            value,
564            serde_json::json!({
565                "order": "send",
566                "order_tag": "1",
567                "symbol": "PI_XBTUSD",
568                "side": "sell",
569                "orderType": "mkt",
570                "size": "500",
571                "reduceOnly": true,
572            }),
573        );
574    }
575
576    #[rstest]
577    fn test_batch_edit_item_from_params() {
578        let params = KrakenFuturesEditOrderParamsBuilder::default()
579            .order_id("order-123")
580            .size("2000")
581            .limit_price("51000.0")
582            .build()
583            .unwrap();
584
585        let item = KrakenFuturesBatchEditItem::from_params(params, "0");
586
587        assert_eq!(item.order, "edit");
588        assert_eq!(item.order_tag, "0");
589        assert_eq!(item.order_id, Some("order-123".to_string()));
590        assert_eq!(item.size, Some("2000".to_string()));
591        assert_eq!(item.limit_price, Some("51000.0".to_string()));
592    }
593
594    #[rstest]
595    fn test_batch_edit_item_serialization() {
596        let params = KrakenFuturesEditOrderParamsBuilder::default()
597            .cli_ord_id("my-order")
598            .limit_price("55000.0")
599            .build()
600            .unwrap();
601
602        let item = KrakenFuturesBatchEditItem::from_params(params, "2");
603        let value = serde_json::to_value(&item).unwrap();
604
605        assert_eq!(
606            value,
607            serde_json::json!({
608                "order": "edit",
609                "order_tag": "2",
610                "cliOrdId": "my-order",
611                "limitPrice": "55000.0",
612            }),
613        );
614    }
615
616    #[rstest]
617    fn test_batch_order_params_to_body() {
618        let params = KrakenFuturesSendOrderParamsBuilder::default()
619            .symbol("PI_XBTUSD")
620            .side(KrakenOrderSide::Buy)
621            .order_type(KrakenFuturesOrderType::Limit)
622            .size("100")
623            .limit_price("50000.0")
624            .build()
625            .unwrap();
626
627        let item = KrakenFuturesBatchSendItem::from_params(params, "0");
628        let batch = KrakenFuturesBatchOrderParams::new(vec![item]);
629        let body = batch.to_body().unwrap();
630
631        assert!(body.starts_with("json="));
632        assert!(body.contains("\"batchOrder\""));
633        assert!(body.contains("\"order\":\"send\""));
634    }
635}