nautilus_kraken/http/futures/
query.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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 index.
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() || self.limit_price.as_ref().unwrap().is_none() {
94                        return Err("limit_price is required for limit orders".to_string());
95                    }
96                }
97                KrakenFuturesOrderType::Stop | KrakenFuturesOrderType::StopLoss => {
98                    if self.stop_price.is_none() || self.stop_price.as_ref().unwrap().is_none() {
99                        return Err("stop_price is required for stop orders".to_string());
100                    }
101                }
102                _ => {}
103            }
104        }
105        Ok(())
106    }
107}
108
109/// Parameters for canceling an order via `POST /api/v3/cancelorder`.
110///
111/// # References
112/// - <https://docs.kraken.com/api/docs/futures-api/trading/cancel-order/>
113#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
114#[serde(rename_all = "camelCase")]
115#[builder(setter(into, strip_option))]
116pub struct KrakenFuturesCancelOrderParams {
117    /// The venue order ID to cancel.
118    #[builder(default)]
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub order_id: Option<String>,
121
122    /// The client order ID to cancel.
123    #[builder(default)]
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub cli_ord_id: Option<String>,
126}
127
128/// A batch cancel item for `POST /derivatives/api/v3/batchorder`.
129///
130/// # References
131/// - <https://docs.kraken.com/api/docs/futures-api/trading/send-batch-order/>
132#[derive(Clone, Debug, Serialize, Deserialize)]
133pub struct KrakenFuturesBatchCancelItem {
134    /// The operation type, always "cancel" for this item.
135    pub order: String,
136
137    /// The venue order ID to cancel.
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub order_id: Option<String>,
140
141    /// The client order ID to cancel (alternative to order_id).
142    #[serde(rename = "cliOrdId", skip_serializing_if = "Option::is_none")]
143    pub cli_ord_id: Option<String>,
144}
145
146impl KrakenFuturesBatchCancelItem {
147    /// Create a batch cancel item from a venue order ID.
148    #[must_use]
149    pub fn from_order_id(order_id: impl Into<String>) -> Self {
150        Self {
151            order: "cancel".to_string(),
152            order_id: Some(order_id.into()),
153            cli_ord_id: None,
154        }
155    }
156
157    /// Create a batch cancel item from a client order ID.
158    #[must_use]
159    pub fn from_client_order_id(cli_ord_id: impl Into<String>) -> Self {
160        Self {
161            order: "cancel".to_string(),
162            order_id: None,
163            cli_ord_id: Some(cli_ord_id.into()),
164        }
165    }
166}
167
168/// Parameters for batch order operations via `POST /derivatives/api/v3/batchorder`.
169///
170/// The batchorder endpoint uses a special body format: `json={"batchOrder": [...]}`
171/// where the JSON is NOT URL-encoded.
172///
173/// # References
174/// - <https://docs.kraken.com/api/docs/futures-api/trading/send-batch-order/>
175#[derive(Clone, Debug, Serialize, Deserialize)]
176#[serde(rename_all = "camelCase")]
177pub struct KrakenFuturesBatchOrderParams<T: Serialize> {
178    /// List of batch order operations.
179    pub batch_order: Vec<T>,
180}
181
182impl<T: Serialize> KrakenFuturesBatchOrderParams<T> {
183    /// Create new batch order params.
184    #[must_use]
185    pub fn new(batch_order: Vec<T>) -> Self {
186        Self { batch_order }
187    }
188
189    /// Serialize to the special `json=...` body format required by this endpoint.
190    pub fn to_body(&self) -> Result<String, serde_json::Error> {
191        let json_str = serde_json::to_string(self)?;
192        Ok(format!("json={json_str}"))
193    }
194}
195
196/// Parameters for editing an order via `POST /api/v3/editorder`.
197///
198/// # References
199/// - <https://docs.kraken.com/api/docs/futures-api/trading/edit-order/>
200#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
201#[serde(rename_all = "camelCase")]
202#[builder(setter(into, strip_option))]
203pub struct KrakenFuturesEditOrderParams {
204    /// The venue order ID to edit.
205    #[builder(default)]
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub order_id: Option<String>,
208
209    /// The client order ID to edit.
210    #[builder(default)]
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub cli_ord_id: Option<String>,
213
214    /// New order size.
215    #[builder(default)]
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub size: Option<String>,
218
219    /// New limit price.
220    #[builder(default)]
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub limit_price: Option<String>,
223
224    /// New stop price.
225    #[builder(default)]
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub stop_price: Option<String>,
228}
229
230/// Parameters for canceling all orders via `POST /api/v3/cancelallorders`.
231///
232/// # References
233/// - <https://docs.kraken.com/api/docs/futures-api/trading/cancel-all-orders/>
234#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
235#[serde(rename_all = "camelCase")]
236#[builder(setter(into, strip_option), default)]
237pub struct KrakenFuturesCancelAllOrdersParams {
238    /// Optional symbol filter - only cancel orders for this symbol.
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub symbol: Option<Ustr>,
241}
242
243/// Parameters for getting open orders via `GET /api/v3/openorders`.
244///
245/// # References
246/// - <https://docs.kraken.com/api/docs/futures-api/trading/get-open-orders/>
247#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
248#[serde(rename_all = "camelCase")]
249#[builder(setter(into, strip_option), default)]
250pub struct KrakenFuturesOpenOrdersParams {
251    // Currently no parameters, but kept for future extensibility
252}
253
254/// Parameters for getting fills via `GET /api/v3/fills`.
255///
256/// # References
257/// - <https://docs.kraken.com/api/docs/futures-api/trading/get-fills/>
258#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
259#[serde(rename_all = "camelCase")]
260#[builder(setter(into, strip_option), default)]
261pub struct KrakenFuturesFillsParams {
262    /// Filter fills after this timestamp (milliseconds).
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub last_fill_time: Option<String>,
265}
266
267/// Parameters for getting open positions via `GET /api/v3/openpositions`.
268///
269/// # References
270/// - <https://docs.kraken.com/api/docs/futures-api/trading/get-open-positions/>
271#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
272#[serde(rename_all = "camelCase")]
273#[builder(setter(into, strip_option), default)]
274pub struct KrakenFuturesOpenPositionsParams {
275    // Currently no parameters, but kept for future extensibility
276}
277
278#[cfg(test)]
279mod tests {
280    use rstest::rstest;
281
282    use super::*;
283
284    #[rstest]
285    fn test_send_order_params_builder() {
286        let params = KrakenFuturesSendOrderParamsBuilder::default()
287            .symbol("PI_XBTUSD")
288            .side(KrakenOrderSide::Buy)
289            .order_type(KrakenFuturesOrderType::Limit)
290            .size("1000")
291            .limit_price("50000.0")
292            .cli_ord_id("test-order-123")
293            .reduce_only(false)
294            .build()
295            .unwrap();
296
297        assert_eq!(params.symbol, Ustr::from("PI_XBTUSD"));
298        assert_eq!(params.side, KrakenOrderSide::Buy);
299        assert_eq!(params.order_type, KrakenFuturesOrderType::Limit);
300        assert_eq!(params.size, "1000");
301        assert_eq!(params.limit_price, Some("50000.0".to_string()));
302        assert_eq!(params.cli_ord_id, Some("test-order-123".to_string()));
303    }
304
305    #[rstest]
306    fn test_send_order_params_serialization() {
307        let params = KrakenFuturesSendOrderParamsBuilder::default()
308            .symbol("PI_XBTUSD")
309            .side(KrakenOrderSide::Buy)
310            .order_type(KrakenFuturesOrderType::Ioc)
311            .size("500")
312            .limit_price("48000.0")
313            .build()
314            .unwrap();
315
316        let json = serde_json::to_string(&params).unwrap();
317        assert!(json.contains("\"orderType\":\"ioc\""));
318        assert!(json.contains("\"limitPrice\":\"48000.0\""));
319    }
320
321    #[rstest]
322    fn test_send_order_params_missing_limit_price() {
323        let result = KrakenFuturesSendOrderParamsBuilder::default()
324            .symbol("PI_XBTUSD")
325            .side(KrakenOrderSide::Buy)
326            .order_type(KrakenFuturesOrderType::Limit)
327            .size("1000")
328            .build();
329
330        assert!(result.is_err());
331        assert!(result.unwrap_err().to_string().contains("limit_price"));
332    }
333
334    #[rstest]
335    fn test_cancel_order_params_builder() {
336        let params = KrakenFuturesCancelOrderParamsBuilder::default()
337            .order_id("abc-123")
338            .build()
339            .unwrap();
340
341        assert_eq!(params.order_id, Some("abc-123".to_string()));
342    }
343
344    #[rstest]
345    fn test_edit_order_params_builder() {
346        let params = KrakenFuturesEditOrderParamsBuilder::default()
347            .order_id("abc-123")
348            .size("2000")
349            .limit_price("51000.0")
350            .build()
351            .unwrap();
352
353        assert_eq!(params.order_id, Some("abc-123".to_string()));
354        assert_eq!(params.size, Some("2000".to_string()));
355        assert_eq!(params.limit_price, Some("51000.0".to_string()));
356    }
357}