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    #[serde(rename = "order_tag")]
182    pub order_tag: String,
183
184    /// The symbol of the futures contract.
185    pub symbol: Ustr,
186
187    /// The order side.
188    pub side: KrakenOrderSide,
189
190    /// The order type.
191    pub order_type: KrakenFuturesOrderType,
192
193    /// The order size in contracts.
194    pub size: String,
195
196    /// Optional client order ID for tracking.
197    #[serde(rename = "cliOrdId", skip_serializing_if = "Option::is_none")]
198    pub cli_ord_id: Option<String>,
199
200    /// Limit price (required for limit orders).
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub limit_price: Option<String>,
203
204    /// Stop/trigger price (required for stop orders).
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub stop_price: Option<String>,
207
208    /// If true, the order will only reduce an existing position.
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub reduce_only: Option<bool>,
211
212    /// Trigger signal for stop orders.
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub trigger_signal: Option<KrakenTriggerSignal>,
215}
216
217impl KrakenFuturesBatchSendItem {
218    /// Creates a batch send item from send order params.
219    #[must_use]
220    pub fn from_params(params: KrakenFuturesSendOrderParams, order_tag: impl Into<String>) -> Self {
221        Self {
222            order: "send".to_string(),
223            order_tag: order_tag.into(),
224            symbol: params.symbol,
225            side: params.side,
226            order_type: params.order_type,
227            size: params.size,
228            cli_ord_id: params.cli_ord_id,
229            limit_price: params.limit_price,
230            stop_price: params.stop_price,
231            reduce_only: params.reduce_only,
232            trigger_signal: params.trigger_signal,
233        }
234    }
235}
236
237/// A batch edit item for `POST /derivatives/api/v3/batchorder`.
238///
239/// # References
240/// - <https://docs.kraken.com/api/docs/futures-api/trading/send-batch-order/>
241#[derive(Clone, Debug, Serialize, Deserialize)]
242#[serde(rename_all = "camelCase")]
243pub struct KrakenFuturesBatchEditItem {
244    /// The operation type, always "edit" for this item.
245    pub order: String,
246
247    /// An order tag to correlate batch responses with requests.
248    #[serde(rename = "order_tag")]
249    pub order_tag: String,
250
251    /// The venue order ID to edit.
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub order_id: Option<String>,
254
255    /// The client order ID to edit.
256    #[serde(rename = "cliOrdId", skip_serializing_if = "Option::is_none")]
257    pub cli_ord_id: Option<String>,
258
259    /// New order size.
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub size: Option<String>,
262
263    /// New limit price.
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub limit_price: Option<String>,
266
267    /// New stop price.
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub stop_price: Option<String>,
270}
271
272impl KrakenFuturesBatchEditItem {
273    /// Creates a batch edit item from edit order params.
274    #[must_use]
275    pub fn from_params(params: KrakenFuturesEditOrderParams, order_tag: impl Into<String>) -> Self {
276        Self {
277            order: "edit".to_string(),
278            order_tag: order_tag.into(),
279            order_id: params.order_id,
280            cli_ord_id: params.cli_ord_id,
281            size: params.size,
282            limit_price: params.limit_price,
283            stop_price: params.stop_price,
284        }
285    }
286}
287
288/// Parameters for batch order operations via `POST /derivatives/api/v3/batchorder`.
289///
290/// The batchorder endpoint uses a special body format: `json={"batchOrder": [...]}`
291/// where the JSON is NOT URL-encoded.
292///
293/// # References
294/// - <https://docs.kraken.com/api/docs/futures-api/trading/send-batch-order/>
295#[derive(Clone, Debug, Serialize, Deserialize)]
296#[serde(rename_all = "camelCase")]
297pub struct KrakenFuturesBatchOrderParams<T: Serialize> {
298    /// List of batch order operations.
299    pub batch_order: Vec<T>,
300}
301
302impl<T: Serialize> KrakenFuturesBatchOrderParams<T> {
303    /// Create new batch order params.
304    #[must_use]
305    pub fn new(batch_order: Vec<T>) -> Self {
306        Self { batch_order }
307    }
308
309    /// Serialize to the special `json=...` body format required by this endpoint.
310    pub fn to_body(&self) -> Result<String, serde_json::Error> {
311        let json_str = serde_json::to_string(self)?;
312        Ok(format!("json={json_str}"))
313    }
314}
315
316/// Parameters for editing an order via `POST /api/v3/editorder`.
317///
318/// # References
319/// - <https://docs.kraken.com/api/docs/futures-api/trading/edit-order/>
320#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
321#[serde(rename_all = "camelCase")]
322#[builder(setter(into, strip_option))]
323pub struct KrakenFuturesEditOrderParams {
324    /// The venue order ID to edit.
325    #[builder(default)]
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub order_id: Option<String>,
328
329    /// The client order ID to edit.
330    #[builder(default)]
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub cli_ord_id: Option<String>,
333
334    /// New order size.
335    #[builder(default)]
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub size: Option<String>,
338
339    /// New limit price.
340    #[builder(default)]
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub limit_price: Option<String>,
343
344    /// New stop price.
345    #[builder(default)]
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub stop_price: Option<String>,
348}
349
350/// Parameters for canceling all orders via `POST /api/v3/cancelallorders`.
351///
352/// # References
353/// - <https://docs.kraken.com/api/docs/futures-api/trading/cancel-all-orders/>
354#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
355#[serde(rename_all = "camelCase")]
356#[builder(setter(into, strip_option), default)]
357pub struct KrakenFuturesCancelAllOrdersParams {
358    /// Optional symbol filter - only cancel orders for this symbol.
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub symbol: Option<Ustr>,
361}
362
363/// Parameters for getting open orders via `GET /api/v3/openorders`.
364///
365/// # References
366/// - <https://docs.kraken.com/api/docs/futures-api/trading/get-open-orders/>
367#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
368#[serde(rename_all = "camelCase")]
369#[builder(setter(into, strip_option), default)]
370pub struct KrakenFuturesOpenOrdersParams {
371    // Currently no parameters, but kept for future extensibility
372}
373
374/// Parameters for getting fills via `GET /api/v3/fills`.
375///
376/// # References
377/// - <https://docs.kraken.com/api/docs/futures-api/trading/get-fills/>
378#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
379#[serde(rename_all = "camelCase")]
380#[builder(setter(into, strip_option), default)]
381pub struct KrakenFuturesFillsParams {
382    /// Filter fills after this timestamp (milliseconds).
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub last_fill_time: Option<String>,
385}
386
387/// Parameters for getting open positions via `GET /api/v3/openpositions`.
388///
389/// # References
390/// - <https://docs.kraken.com/api/docs/futures-api/trading/get-open-positions/>
391#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
392#[serde(rename_all = "camelCase")]
393#[builder(setter(into, strip_option), default)]
394pub struct KrakenFuturesOpenPositionsParams {
395    // Currently no parameters, but kept for future extensibility
396}
397
398#[cfg(test)]
399mod tests {
400    use rstest::rstest;
401
402    use super::*;
403
404    #[rstest]
405    fn test_send_order_params_builder() {
406        let params = KrakenFuturesSendOrderParamsBuilder::default()
407            .symbol("PI_XBTUSD")
408            .side(KrakenOrderSide::Buy)
409            .order_type(KrakenFuturesOrderType::Limit)
410            .size("1000")
411            .limit_price("50000.0")
412            .cli_ord_id("test-order-123")
413            .reduce_only(false)
414            .build()
415            .unwrap();
416
417        assert_eq!(params.symbol, Ustr::from("PI_XBTUSD"));
418        assert_eq!(params.side, KrakenOrderSide::Buy);
419        assert_eq!(params.order_type, KrakenFuturesOrderType::Limit);
420        assert_eq!(params.size, "1000");
421        assert_eq!(params.limit_price, Some("50000.0".to_string()));
422        assert_eq!(params.cli_ord_id, Some("test-order-123".to_string()));
423    }
424
425    #[rstest]
426    fn test_send_order_params_serialization() {
427        let params = KrakenFuturesSendOrderParamsBuilder::default()
428            .symbol("PI_XBTUSD")
429            .side(KrakenOrderSide::Buy)
430            .order_type(KrakenFuturesOrderType::Ioc)
431            .size("500")
432            .limit_price("48000.0")
433            .build()
434            .unwrap();
435
436        let json = serde_json::to_string(&params).unwrap();
437        assert!(json.contains("\"orderType\":\"ioc\""));
438        assert!(json.contains("\"limitPrice\":\"48000.0\""));
439    }
440
441    #[rstest]
442    fn test_send_order_params_serialization_with_trigger_signal() {
443        let params = KrakenFuturesSendOrderParamsBuilder::default()
444            .symbol("PI_XBTUSD")
445            .side(KrakenOrderSide::Buy)
446            .order_type(KrakenFuturesOrderType::Stop)
447            .size("500")
448            .stop_price("47000.0")
449            .trigger_signal(KrakenTriggerSignal::Mark)
450            .build()
451            .unwrap();
452
453        let json = serde_json::to_string(&params).unwrap();
454        assert!(json.contains("\"triggerSignal\":\"mark\""));
455        assert!(json.contains("\"stopPrice\":\"47000.0\""));
456    }
457
458    #[rstest]
459    fn test_send_order_params_serialization_with_index_trigger_signal() {
460        let params = KrakenFuturesSendOrderParamsBuilder::default()
461            .symbol("PI_XBTUSD")
462            .side(KrakenOrderSide::Buy)
463            .order_type(KrakenFuturesOrderType::Stop)
464            .size("500")
465            .stop_price("47000.0")
466            .trigger_signal(KrakenTriggerSignal::Index)
467            .build()
468            .unwrap();
469
470        let json = serde_json::to_string(&params).unwrap();
471        assert!(json.contains("\"triggerSignal\":\"spot\""));
472        assert!(json.contains("\"stopPrice\":\"47000.0\""));
473    }
474
475    #[rstest]
476    fn test_send_order_params_missing_limit_price() {
477        let result = KrakenFuturesSendOrderParamsBuilder::default()
478            .symbol("PI_XBTUSD")
479            .side(KrakenOrderSide::Buy)
480            .order_type(KrakenFuturesOrderType::Limit)
481            .size("1000")
482            .build();
483
484        assert!(result.is_err());
485        assert!(result.unwrap_err().to_string().contains("limit_price"));
486    }
487
488    #[rstest]
489    fn test_cancel_order_params_builder() {
490        let params = KrakenFuturesCancelOrderParamsBuilder::default()
491            .order_id("abc-123")
492            .build()
493            .unwrap();
494
495        assert_eq!(params.order_id, Some("abc-123".to_string()));
496    }
497
498    #[rstest]
499    fn test_edit_order_params_builder() {
500        let params = KrakenFuturesEditOrderParamsBuilder::default()
501            .order_id("abc-123")
502            .size("2000")
503            .limit_price("51000.0")
504            .build()
505            .unwrap();
506
507        assert_eq!(params.order_id, Some("abc-123".to_string()));
508        assert_eq!(params.size, Some("2000".to_string()));
509        assert_eq!(params.limit_price, Some("51000.0".to_string()));
510    }
511
512    #[rstest]
513    fn test_batch_send_item_from_params() {
514        let params = KrakenFuturesSendOrderParamsBuilder::default()
515            .symbol("PI_XBTUSD")
516            .side(KrakenOrderSide::Buy)
517            .order_type(KrakenFuturesOrderType::Limit)
518            .size("1000")
519            .limit_price("50000.0")
520            .cli_ord_id("test-batch-1")
521            .build()
522            .unwrap();
523
524        let item = KrakenFuturesBatchSendItem::from_params(params, "0");
525
526        assert_eq!(item.order, "send");
527        assert_eq!(item.order_tag, "0");
528        assert_eq!(item.symbol, Ustr::from("PI_XBTUSD"));
529        assert_eq!(item.side, KrakenOrderSide::Buy);
530        assert_eq!(item.order_type, KrakenFuturesOrderType::Limit);
531        assert_eq!(item.size, "1000");
532        assert_eq!(item.limit_price, Some("50000.0".to_string()));
533        assert_eq!(item.cli_ord_id, Some("test-batch-1".to_string()));
534    }
535
536    #[rstest]
537    fn test_batch_send_item_serialization() {
538        let params = KrakenFuturesSendOrderParamsBuilder::default()
539            .symbol("PI_XBTUSD")
540            .side(KrakenOrderSide::Sell)
541            .order_type(KrakenFuturesOrderType::Market)
542            .size("500")
543            .reduce_only(true)
544            .build()
545            .unwrap();
546
547        let item = KrakenFuturesBatchSendItem::from_params(params, "1");
548        let value = serde_json::to_value(&item).unwrap();
549
550        assert_eq!(
551            value,
552            serde_json::json!({
553                "order": "send",
554                "order_tag": "1",
555                "symbol": "PI_XBTUSD",
556                "side": "sell",
557                "orderType": "mkt",
558                "size": "500",
559                "reduceOnly": true,
560            }),
561        );
562    }
563
564    #[rstest]
565    fn test_batch_edit_item_from_params() {
566        let params = KrakenFuturesEditOrderParamsBuilder::default()
567            .order_id("order-123")
568            .size("2000")
569            .limit_price("51000.0")
570            .build()
571            .unwrap();
572
573        let item = KrakenFuturesBatchEditItem::from_params(params, "0");
574
575        assert_eq!(item.order, "edit");
576        assert_eq!(item.order_tag, "0");
577        assert_eq!(item.order_id, Some("order-123".to_string()));
578        assert_eq!(item.size, Some("2000".to_string()));
579        assert_eq!(item.limit_price, Some("51000.0".to_string()));
580    }
581
582    #[rstest]
583    fn test_batch_edit_item_serialization() {
584        let params = KrakenFuturesEditOrderParamsBuilder::default()
585            .cli_ord_id("my-order")
586            .limit_price("55000.0")
587            .build()
588            .unwrap();
589
590        let item = KrakenFuturesBatchEditItem::from_params(params, "2");
591        let value = serde_json::to_value(&item).unwrap();
592
593        assert_eq!(
594            value,
595            serde_json::json!({
596                "order": "edit",
597                "order_tag": "2",
598                "cliOrdId": "my-order",
599                "limitPrice": "55000.0",
600            }),
601        );
602    }
603
604    #[rstest]
605    fn test_batch_order_params_to_body() {
606        let params = KrakenFuturesSendOrderParamsBuilder::default()
607            .symbol("PI_XBTUSD")
608            .side(KrakenOrderSide::Buy)
609            .order_type(KrakenFuturesOrderType::Limit)
610            .size("100")
611            .limit_price("50000.0")
612            .build()
613            .unwrap();
614
615        let item = KrakenFuturesBatchSendItem::from_params(params, "0");
616        let batch = KrakenFuturesBatchOrderParams::new(vec![item]);
617        let body = batch.to_body().unwrap();
618
619        assert!(body.starts_with("json="));
620        assert!(body.contains("\"batchOrder\""));
621        assert!(body.contains("\"order\":\"send\""));
622    }
623}