nautilus_kraken/http/spot/
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 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        Ok(())
94    }
95}
96
97/// Parameters for cancelling an order via `POST /0/private/CancelOrder`.
98///
99/// # References
100/// - <https://docs.kraken.com/api/docs/rest-api/cancel-order>
101#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
102#[builder(setter(into, strip_option))]
103pub struct KrakenSpotCancelOrderParams {
104    /// Transaction ID (venue order ID) to cancel.
105    /// Note: The Kraken v0 API uses `txid` as the parameter name.
106    #[builder(default)]
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub txid: Option<String>,
109
110    /// Client order ID to cancel.
111    #[builder(default)]
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub cl_ord_id: Option<String>,
114}
115
116/// Parameters for batch cancelling orders via `POST /0/private/CancelOrderBatch`.
117///
118/// # References
119/// - <https://docs.kraken.com/api/docs/rest-api/cancel-order-batch>
120#[derive(Clone, Debug, Serialize, Deserialize)]
121pub struct KrakenSpotCancelOrderBatchParams {
122    /// List of transaction IDs (venue order IDs) or client order IDs to cancel.
123    /// Maximum 50 IDs.
124    pub orders: Vec<String>,
125}
126
127/// Parameters for editing an order via `POST /0/private/EditOrder`.
128///
129/// Note: Consider using `KrakenSpotAmendOrderParams` with `AmendOrder` instead,
130/// which is faster and keeps queue priority.
131///
132/// # References
133/// - <https://docs.kraken.com/api/docs/rest-api/edit-order>
134#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
135#[builder(setter(into, strip_option))]
136pub struct KrakenSpotEditOrderParams {
137    /// Asset pair (e.g., "XXBTZUSD"). Required.
138    pub pair: Ustr,
139
140    /// Transaction ID (venue order ID) of the order to edit.
141    #[builder(default)]
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub txid: Option<String>,
144
145    /// Client order ID of the order to edit. Note: Not supported by Kraken EditOrder.
146    #[builder(default)]
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub cl_ord_id: Option<String>,
149
150    /// New order quantity in base currency.
151    #[builder(default)]
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub volume: Option<String>,
154
155    /// New limit price.
156    #[builder(default)]
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub price: Option<String>,
159
160    /// New secondary price for stop-loss-limit and take-profit-limit.
161    #[builder(default)]
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub price2: Option<String>,
164}
165
166/// Parameters for amending an order via `POST /0/private/AmendOrder`.
167///
168/// This is Kraken's atomic amend endpoint which modifies order parameters
169/// in-place without cancelling the original order. Faster and keeps queue priority.
170///
171/// # References
172/// - <https://docs.kraken.com/api/docs/rest-api/amend-order>
173#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
174#[builder(setter(into, strip_option))]
175pub struct KrakenSpotAmendOrderParams {
176    /// Transaction ID (venue order ID) of the order to amend.
177    #[builder(default)]
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub txid: Option<String>,
180
181    /// Client order ID of the order to amend.
182    #[builder(default)]
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub cl_ord_id: Option<String>,
185
186    /// New order quantity in base currency.
187    #[builder(default)]
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub order_qty: Option<String>,
190
191    /// New limit price.
192    #[builder(default)]
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub limit_price: Option<String>,
195
196    /// New trigger price for stop/conditional orders.
197    #[builder(default)]
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub trigger_price: Option<String>,
200}
201
202#[cfg(test)]
203mod tests {
204    use rstest::rstest;
205
206    use super::*;
207
208    #[rstest]
209    fn test_add_order_params_builder() {
210        let params = KrakenSpotAddOrderParamsBuilder::default()
211            .pair("XXBTZUSD")
212            .side(KrakenOrderSide::Buy)
213            .order_type(KrakenOrderType::Limit)
214            .volume("0.01")
215            .price("50000.0")
216            .cl_ord_id("my-order-123")
217            .broker("test-broker")
218            .build()
219            .unwrap();
220
221        assert_eq!(params.pair, Ustr::from("XXBTZUSD"));
222        assert_eq!(params.side, KrakenOrderSide::Buy);
223        assert_eq!(params.order_type, KrakenOrderType::Limit);
224        assert_eq!(params.volume, "0.01");
225        assert_eq!(params.price, Some("50000.0".to_string()));
226        assert_eq!(params.cl_ord_id, Some("my-order-123".to_string()));
227        assert_eq!(params.broker, Some(Ustr::from("test-broker")));
228    }
229
230    #[rstest]
231    fn test_add_order_params_serialization() {
232        let params = KrakenSpotAddOrderParamsBuilder::default()
233            .pair("XXBTZUSD")
234            .side(KrakenOrderSide::Buy)
235            .order_type(KrakenOrderType::Market)
236            .volume("0.01")
237            .broker("broker-id")
238            .build()
239            .unwrap();
240
241        let encoded = serde_urlencoded::to_string(&params).unwrap();
242
243        assert!(encoded.contains("pair=XXBTZUSD"));
244        assert!(encoded.contains("type=buy"));
245        assert!(encoded.contains("ordertype=market"));
246        assert!(encoded.contains("volume=0.01"));
247        assert!(encoded.contains("broker=broker-id"));
248        assert!(!encoded.contains("price="));
249    }
250
251    #[rstest]
252    fn test_add_order_params_limit_requires_price() {
253        let result = KrakenSpotAddOrderParamsBuilder::default()
254            .pair("XXBTZUSD")
255            .side(KrakenOrderSide::Buy)
256            .order_type(KrakenOrderType::Limit)
257            .volume("0.01")
258            .build();
259
260        assert!(result.is_err());
261        assert!(
262            result
263                .unwrap_err()
264                .to_string()
265                .contains("price is required")
266        );
267    }
268
269    #[rstest]
270    fn test_cancel_order_params_builder() {
271        let params = KrakenSpotCancelOrderParamsBuilder::default()
272            .txid("TXID123")
273            .build()
274            .unwrap();
275
276        assert_eq!(params.txid, Some("TXID123".to_string()));
277        assert_eq!(params.cl_ord_id, None);
278    }
279
280    #[rstest]
281    fn test_cancel_order_params_serialization() {
282        let params = KrakenSpotCancelOrderParamsBuilder::default()
283            .cl_ord_id("my-order")
284            .build()
285            .unwrap();
286
287        let encoded = serde_urlencoded::to_string(&params).unwrap();
288
289        assert!(encoded.contains("cl_ord_id=my-order"));
290        assert!(!encoded.contains("txid="));
291    }
292}