nautilus_kraken/http/spot/
query.rs1use derive_builder::Builder;
19use serde::{Deserialize, Serialize};
20use ustr::Ustr;
21
22use crate::common::enums::{KrakenAssetClass, KrakenOrderSide, KrakenOrderType};
23
24#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
29#[builder(setter(into, strip_option), build_fn(validate = "Self::validate"))]
30pub struct KrakenSpotAddOrderParams {
31 pub pair: Ustr,
33
34 #[serde(rename = "type")]
36 pub side: KrakenOrderSide,
37
38 #[serde(rename = "ordertype")]
40 pub order_type: KrakenOrderType,
41
42 pub volume: String,
44
45 #[builder(default)]
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub price: Option<String>,
49
50 #[builder(default)]
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub price2: Option<String>,
54
55 #[builder(default)]
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub cl_ord_id: Option<String>,
59
60 #[builder(default)]
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub oflags: Option<String>,
64
65 #[builder(default)]
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub timeinforce: Option<String>,
69
70 #[builder(default)]
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub expiretm: Option<String>,
74
75 #[builder(default)]
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub trigger: Option<String>,
79
80 #[builder(default)]
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub displayvol: Option<String>,
84
85 #[builder(default)]
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub broker: Option<Ustr>,
89
90 #[builder(default)]
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub asset_class: Option<KrakenAssetClass>,
94}
95
96impl KrakenSpotAddOrderParamsBuilder {
97 fn validate(&self) -> Result<(), String> {
98 if let Some(
100 KrakenOrderType::Limit
101 | KrakenOrderType::StopLossLimit
102 | KrakenOrderType::TakeProfitLimit,
103 ) = self.order_type
104 && (self.price.is_none() || self.price.as_ref().unwrap().is_none())
105 {
106 return Err("price is required for limit orders".to_string());
107 }
108
109 if let Some(KrakenOrderType::StopLossLimit | KrakenOrderType::TakeProfitLimit) =
111 self.order_type
112 && (self.price2.is_none() || self.price2.as_ref().unwrap().is_none())
113 {
114 return Err(
115 "price2 (limit price) is required for stop-loss-limit and take-profit-limit orders"
116 .to_string(),
117 );
118 }
119 Ok(())
120 }
121}
122
123#[derive(Clone, Debug, Serialize, Deserialize)]
128pub struct KrakenSpotBatchOrderParams {
129 #[serde(rename = "type")]
131 pub side: KrakenOrderSide,
132
133 #[serde(rename = "ordertype")]
135 pub order_type: KrakenOrderType,
136
137 pub volume: String,
139
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub price: Option<String>,
143
144 #[serde(skip_serializing_if = "Option::is_none")]
146 pub price2: Option<String>,
147
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub cl_ord_id: Option<String>,
151
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub oflags: Option<String>,
155
156 #[serde(skip_serializing_if = "Option::is_none")]
158 pub timeinforce: Option<String>,
159
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub expiretm: Option<String>,
163
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub trigger: Option<String>,
167
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub displayvol: Option<String>,
171}
172
173impl From<KrakenSpotAddOrderParams> for KrakenSpotBatchOrderParams {
174 fn from(params: KrakenSpotAddOrderParams) -> Self {
175 Self {
176 side: params.side,
177 order_type: params.order_type,
178 volume: params.volume,
179 price: params.price,
180 price2: params.price2,
181 cl_ord_id: params.cl_ord_id,
182 oflags: params.oflags,
183 timeinforce: params.timeinforce,
184 expiretm: params.expiretm,
185 trigger: params.trigger,
186 displayvol: params.displayvol,
187 }
188 }
189}
190
191#[derive(Clone, Debug, Serialize, Deserialize)]
196pub struct KrakenSpotAddOrderBatchParams {
197 pub pair: Ustr,
199
200 pub orders: Vec<KrakenSpotBatchOrderParams>,
202
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub asset_class: Option<KrakenAssetClass>,
206}
207
208#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
213#[builder(setter(into, strip_option))]
214pub struct KrakenSpotCancelOrderParams {
215 #[builder(default)]
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub txid: Option<String>,
220
221 #[builder(default)]
223 #[serde(skip_serializing_if = "Option::is_none")]
224 pub cl_ord_id: Option<String>,
225}
226
227#[derive(Clone, Debug, Serialize, Deserialize)]
232pub struct KrakenSpotCancelOrderBatchParams {
233 pub orders: Vec<String>,
236}
237
238#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
246#[builder(setter(into, strip_option))]
247pub struct KrakenSpotEditOrderParams {
248 pub pair: Ustr,
250
251 #[builder(default)]
253 #[serde(skip_serializing_if = "Option::is_none")]
254 pub txid: Option<String>,
255
256 #[builder(default)]
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub cl_ord_id: Option<String>,
260
261 #[builder(default)]
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub volume: Option<String>,
265
266 #[builder(default)]
268 #[serde(skip_serializing_if = "Option::is_none")]
269 pub price: Option<String>,
270
271 #[builder(default)]
273 #[serde(skip_serializing_if = "Option::is_none")]
274 pub price2: Option<String>,
275}
276
277#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
285#[builder(setter(into, strip_option))]
286pub struct KrakenSpotAmendOrderParams {
287 #[builder(default)]
289 #[serde(skip_serializing_if = "Option::is_none")]
290 pub txid: Option<String>,
291
292 #[builder(default)]
294 #[serde(skip_serializing_if = "Option::is_none")]
295 pub cl_ord_id: Option<String>,
296
297 #[builder(default)]
299 #[serde(skip_serializing_if = "Option::is_none")]
300 pub order_qty: Option<String>,
301
302 #[builder(default)]
304 #[serde(skip_serializing_if = "Option::is_none")]
305 pub limit_price: Option<String>,
306
307 #[builder(default)]
309 #[serde(skip_serializing_if = "Option::is_none")]
310 pub trigger_price: Option<String>,
311}
312
313#[cfg(test)]
314mod tests {
315 use rstest::rstest;
316
317 use super::*;
318
319 #[rstest]
320 fn test_add_order_params_builder() {
321 let params = KrakenSpotAddOrderParamsBuilder::default()
322 .pair("XXBTZUSD")
323 .side(KrakenOrderSide::Buy)
324 .order_type(KrakenOrderType::Limit)
325 .volume("0.01")
326 .price("50000.0")
327 .cl_ord_id("my-order-123")
328 .broker("test-broker")
329 .build()
330 .unwrap();
331
332 assert_eq!(params.pair, Ustr::from("XXBTZUSD"));
333 assert_eq!(params.side, KrakenOrderSide::Buy);
334 assert_eq!(params.order_type, KrakenOrderType::Limit);
335 assert_eq!(params.volume, "0.01");
336 assert_eq!(params.price, Some("50000.0".to_string()));
337 assert_eq!(params.cl_ord_id, Some("my-order-123".to_string()));
338 assert_eq!(params.broker, Some(Ustr::from("test-broker")));
339 }
340
341 #[rstest]
342 fn test_add_order_params_serialization() {
343 let params = KrakenSpotAddOrderParamsBuilder::default()
344 .pair("XXBTZUSD")
345 .side(KrakenOrderSide::Buy)
346 .order_type(KrakenOrderType::Market)
347 .volume("0.01")
348 .broker("broker-id")
349 .build()
350 .unwrap();
351
352 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
353
354 assert!(encoded.contains("pair=XXBTZUSD"));
355 assert!(encoded.contains("type=buy"));
356 assert!(encoded.contains("ordertype=market"));
357 assert!(encoded.contains("volume=0.01"));
358 assert!(encoded.contains("broker=broker-id"));
359 assert!(!encoded.contains("price="));
360 }
361
362 #[rstest]
363 fn test_add_order_params_limit_requires_price() {
364 let result = KrakenSpotAddOrderParamsBuilder::default()
365 .pair("XXBTZUSD")
366 .side(KrakenOrderSide::Buy)
367 .order_type(KrakenOrderType::Limit)
368 .volume("0.01")
369 .build();
370
371 assert!(result.is_err());
372 assert!(
373 result
374 .unwrap_err()
375 .to_string()
376 .contains("price is required")
377 );
378 }
379
380 #[rstest]
381 fn test_cancel_order_params_builder() {
382 let params = KrakenSpotCancelOrderParamsBuilder::default()
383 .txid("TXID123")
384 .build()
385 .unwrap();
386
387 assert_eq!(params.txid, Some("TXID123".to_string()));
388 assert_eq!(params.cl_ord_id, None);
389 }
390
391 #[rstest]
392 fn test_cancel_order_params_serialization() {
393 let params = KrakenSpotCancelOrderParamsBuilder::default()
394 .cl_ord_id("my-order")
395 .build()
396 .unwrap();
397
398 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
399
400 assert!(encoded.contains("cl_ord_id=my-order"));
401 assert!(!encoded.contains("txid="));
402 }
403
404 #[rstest]
405 fn test_add_order_params_trailing_stop() {
406 let params = KrakenSpotAddOrderParamsBuilder::default()
407 .pair("XXBTZUSD")
408 .side(KrakenOrderSide::Buy)
409 .order_type(KrakenOrderType::TrailingStop)
410 .volume("0.01")
411 .price("500")
412 .build()
413 .unwrap();
414
415 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
416
417 assert!(encoded.contains("ordertype=trailing-stop"));
418 assert!(encoded.contains("price=500"));
419 }
420
421 #[rstest]
422 fn test_add_order_params_trailing_stop_limit() {
423 let params = KrakenSpotAddOrderParamsBuilder::default()
424 .pair("XXBTZUSD")
425 .side(KrakenOrderSide::Buy)
426 .order_type(KrakenOrderType::TrailingStopLimit)
427 .volume("0.01")
428 .price("500")
429 .price2("100")
430 .build()
431 .unwrap();
432
433 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
434
435 assert!(encoded.contains("ordertype=trailing-stop-limit"));
436 assert!(encoded.contains("price=500"));
437 assert!(encoded.contains("price2=100"));
438 }
439
440 #[rstest]
441 fn test_add_order_params_with_trigger() {
442 let params = KrakenSpotAddOrderParamsBuilder::default()
443 .pair("XXBTZUSD")
444 .side(KrakenOrderSide::Buy)
445 .order_type(KrakenOrderType::StopLoss)
446 .volume("0.01")
447 .price("50000")
448 .trigger("index")
449 .build()
450 .unwrap();
451
452 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
453
454 assert!(encoded.contains("trigger=index"));
455 }
456
457 #[rstest]
458 fn test_add_order_params_with_displayvol() {
459 let params = KrakenSpotAddOrderParamsBuilder::default()
460 .pair("XXBTZUSD")
461 .side(KrakenOrderSide::Buy)
462 .order_type(KrakenOrderType::Limit)
463 .volume("1.0")
464 .price("50000")
465 .displayvol("0.1")
466 .build()
467 .unwrap();
468
469 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
470
471 assert!(encoded.contains("displayvol=0.1"));
472 }
473
474 #[rstest]
475 fn test_add_order_params_with_viqc_flag() {
476 let params = KrakenSpotAddOrderParamsBuilder::default()
477 .pair("XXBTZUSD")
478 .side(KrakenOrderSide::Buy)
479 .order_type(KrakenOrderType::Market)
480 .volume("100")
481 .oflags("viqc")
482 .build()
483 .unwrap();
484
485 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
486
487 assert!(encoded.contains("oflags=viqc"));
488 }
489}