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