Skip to main content

nautilus_kraken/http/spot/
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 Spot HTTP API requests.
17
18use derive_builder::Builder;
19use serde::{Deserialize, Serialize};
20use ustr::Ustr;
21
22use crate::common::enums::{KrakenOrderSide, KrakenOrderType};
23
24/// Parameters for adding an order via `POST /0/private/AddOrder`.
25///
26/// # References
27/// - <https://docs.kraken.com/api/docs/rest-api/add-order>
28#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
29#[builder(setter(into, strip_option), build_fn(validate = "Self::validate"))]
30pub struct KrakenSpotAddOrderParams {
31    /// Asset pair (e.g., "XXBTZUSD").
32    pub pair: Ustr,
33
34    /// Order side: "buy" or "sell".
35    #[serde(rename = "type")]
36    pub side: KrakenOrderSide,
37
38    /// Order type: market, limit, stop-loss, etc.
39    #[serde(rename = "ordertype")]
40    pub order_type: KrakenOrderType,
41
42    /// Order quantity in base currency.
43    pub volume: String,
44
45    /// Limit price (required for limit orders).
46    #[builder(default)]
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub price: Option<String>,
49
50    /// Secondary price for stop-loss-limit and take-profit-limit.
51    #[builder(default)]
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub price2: Option<String>,
54
55    /// Client order ID (must be UUID format).
56    #[builder(default)]
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub cl_ord_id: Option<String>,
59
60    /// Order flags (comma-separated: post, fcib, fciq, nompp, viqc).
61    #[builder(default)]
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub oflags: Option<String>,
64
65    /// Time in force: GTC, IOC, GTD.
66    #[builder(default)]
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub timeinforce: Option<String>,
69
70    /// Expiration time for GTD orders (Unix timestamp or `+<seconds>`).
71    #[builder(default)]
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub expiretm: Option<String>,
74
75    /// Partner/broker attribution ID.
76    #[builder(default)]
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub broker: Option<Ustr>,
79}
80
81impl KrakenSpotAddOrderParamsBuilder {
82    fn validate(&self) -> Result<(), String> {
83        // Validate price is present for limit-type orders
84        if let Some(
85            KrakenOrderType::Limit
86            | KrakenOrderType::StopLossLimit
87            | KrakenOrderType::TakeProfitLimit,
88        ) = self.order_type
89            && (self.price.is_none() || self.price.as_ref().unwrap().is_none())
90        {
91            return Err("price is required for limit orders".to_string());
92        }
93
94        // Validate price2 (limit price) is present for stop-loss-limit and take-profit-limit
95        if let Some(KrakenOrderType::StopLossLimit | KrakenOrderType::TakeProfitLimit) =
96            self.order_type
97            && (self.price2.is_none() || self.price2.as_ref().unwrap().is_none())
98        {
99            return Err(
100                "price2 (limit price) is required for stop-loss-limit and take-profit-limit orders"
101                    .to_string(),
102            );
103        }
104        Ok(())
105    }
106}
107
108/// Parameters for cancelling an order via `POST /0/private/CancelOrder`.
109///
110/// # References
111/// - <https://docs.kraken.com/api/docs/rest-api/cancel-order>
112#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
113#[builder(setter(into, strip_option))]
114pub struct KrakenSpotCancelOrderParams {
115    /// Transaction ID (venue order ID) to cancel.
116    /// Note: The Kraken v0 API uses `txid` as the parameter name.
117    #[builder(default)]
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub txid: Option<String>,
120
121    /// Client order ID to cancel.
122    #[builder(default)]
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub cl_ord_id: Option<String>,
125}
126
127/// Parameters for batch cancelling orders via `POST /0/private/CancelOrderBatch`.
128///
129/// # References
130/// - <https://docs.kraken.com/api/docs/rest-api/cancel-order-batch>
131#[derive(Clone, Debug, Serialize, Deserialize)]
132pub struct KrakenSpotCancelOrderBatchParams {
133    /// List of transaction IDs (venue order IDs) or client order IDs to cancel.
134    /// Maximum 50 IDs.
135    pub orders: Vec<String>,
136}
137
138/// Parameters for editing an order via `POST /0/private/EditOrder`.
139///
140/// Note: Consider using `KrakenSpotAmendOrderParams` with `AmendOrder` instead,
141/// which is faster and keeps queue priority.
142///
143/// # References
144/// - <https://docs.kraken.com/api/docs/rest-api/edit-order>
145#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
146#[builder(setter(into, strip_option))]
147pub struct KrakenSpotEditOrderParams {
148    /// Asset pair (e.g., "XXBTZUSD"). Required.
149    pub pair: Ustr,
150
151    /// Transaction ID (venue order ID) of the order to edit.
152    #[builder(default)]
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub txid: Option<String>,
155
156    /// Client order ID of the order to edit. Note: Not supported by Kraken EditOrder.
157    #[builder(default)]
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub cl_ord_id: Option<String>,
160
161    /// New order quantity in base currency.
162    #[builder(default)]
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub volume: Option<String>,
165
166    /// New limit price.
167    #[builder(default)]
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub price: Option<String>,
170
171    /// New secondary price for stop-loss-limit and take-profit-limit.
172    #[builder(default)]
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub price2: Option<String>,
175}
176
177/// Parameters for amending an order via `POST /0/private/AmendOrder`.
178///
179/// This is Kraken's atomic amend endpoint which modifies order parameters
180/// in-place without cancelling the original order. Faster and keeps queue priority.
181///
182/// # References
183/// - <https://docs.kraken.com/api/docs/rest-api/amend-order>
184#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
185#[builder(setter(into, strip_option))]
186pub struct KrakenSpotAmendOrderParams {
187    /// Transaction ID (venue order ID) of the order to amend.
188    #[builder(default)]
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub txid: Option<String>,
191
192    /// Client order ID of the order to amend.
193    #[builder(default)]
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub cl_ord_id: Option<String>,
196
197    /// New order quantity in base currency.
198    #[builder(default)]
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub order_qty: Option<String>,
201
202    /// New limit price.
203    #[builder(default)]
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub limit_price: Option<String>,
206
207    /// New trigger price for stop/conditional orders.
208    #[builder(default)]
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub trigger_price: Option<String>,
211}
212
213#[cfg(test)]
214mod tests {
215    use rstest::rstest;
216
217    use super::*;
218
219    #[rstest]
220    fn test_add_order_params_builder() {
221        let params = KrakenSpotAddOrderParamsBuilder::default()
222            .pair("XXBTZUSD")
223            .side(KrakenOrderSide::Buy)
224            .order_type(KrakenOrderType::Limit)
225            .volume("0.01")
226            .price("50000.0")
227            .cl_ord_id("my-order-123")
228            .broker("test-broker")
229            .build()
230            .unwrap();
231
232        assert_eq!(params.pair, Ustr::from("XXBTZUSD"));
233        assert_eq!(params.side, KrakenOrderSide::Buy);
234        assert_eq!(params.order_type, KrakenOrderType::Limit);
235        assert_eq!(params.volume, "0.01");
236        assert_eq!(params.price, Some("50000.0".to_string()));
237        assert_eq!(params.cl_ord_id, Some("my-order-123".to_string()));
238        assert_eq!(params.broker, Some(Ustr::from("test-broker")));
239    }
240
241    #[rstest]
242    fn test_add_order_params_serialization() {
243        let params = KrakenSpotAddOrderParamsBuilder::default()
244            .pair("XXBTZUSD")
245            .side(KrakenOrderSide::Buy)
246            .order_type(KrakenOrderType::Market)
247            .volume("0.01")
248            .broker("broker-id")
249            .build()
250            .unwrap();
251
252        let encoded = serde_urlencoded::to_string(&params).unwrap();
253
254        assert!(encoded.contains("pair=XXBTZUSD"));
255        assert!(encoded.contains("type=buy"));
256        assert!(encoded.contains("ordertype=market"));
257        assert!(encoded.contains("volume=0.01"));
258        assert!(encoded.contains("broker=broker-id"));
259        assert!(!encoded.contains("price="));
260    }
261
262    #[rstest]
263    fn test_add_order_params_limit_requires_price() {
264        let result = KrakenSpotAddOrderParamsBuilder::default()
265            .pair("XXBTZUSD")
266            .side(KrakenOrderSide::Buy)
267            .order_type(KrakenOrderType::Limit)
268            .volume("0.01")
269            .build();
270
271        assert!(result.is_err());
272        assert!(
273            result
274                .unwrap_err()
275                .to_string()
276                .contains("price is required")
277        );
278    }
279
280    #[rstest]
281    fn test_cancel_order_params_builder() {
282        let params = KrakenSpotCancelOrderParamsBuilder::default()
283            .txid("TXID123")
284            .build()
285            .unwrap();
286
287        assert_eq!(params.txid, Some("TXID123".to_string()));
288        assert_eq!(params.cl_ord_id, None);
289    }
290
291    #[rstest]
292    fn test_cancel_order_params_serialization() {
293        let params = KrakenSpotCancelOrderParamsBuilder::default()
294            .cl_ord_id("my-order")
295            .build()
296            .unwrap();
297
298        let encoded = serde_urlencoded::to_string(&params).unwrap();
299
300        assert!(encoded.contains("cl_ord_id=my-order"));
301        assert!(!encoded.contains("txid="));
302    }
303}