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 #[builder(default)]
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub leverage: Option<String>,
101
102 #[builder(default)]
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub reduce_only: Option<bool>,
107}
108
109impl KrakenSpotAddOrderParamsBuilder {
110 fn validate(&self) -> Result<(), String> {
111 if let Some(
113 KrakenOrderType::Limit
114 | KrakenOrderType::StopLossLimit
115 | KrakenOrderType::TakeProfitLimit,
116 ) = self.order_type
117 && (self.price.is_none() || self.price.as_ref().unwrap().is_none())
118 {
119 return Err("price is required for limit orders".to_string());
120 }
121
122 if let Some(KrakenOrderType::StopLossLimit | KrakenOrderType::TakeProfitLimit) =
124 self.order_type
125 && (self.price2.is_none() || self.price2.as_ref().unwrap().is_none())
126 {
127 return Err(
128 "price2 (limit price) is required for stop-loss-limit and take-profit-limit orders"
129 .to_string(),
130 );
131 }
132 Ok(())
133 }
134}
135
136#[derive(Clone, Debug, Serialize, Deserialize)]
141pub struct KrakenSpotBatchOrderParams {
142 #[serde(rename = "type")]
144 pub side: KrakenOrderSide,
145
146 #[serde(rename = "ordertype")]
148 pub order_type: KrakenOrderType,
149
150 pub volume: String,
152
153 #[serde(skip_serializing_if = "Option::is_none")]
155 pub price: Option<String>,
156
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub price2: Option<String>,
160
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub cl_ord_id: Option<String>,
164
165 #[serde(skip_serializing_if = "Option::is_none")]
167 pub oflags: Option<String>,
168
169 #[serde(skip_serializing_if = "Option::is_none")]
171 pub timeinforce: Option<String>,
172
173 #[serde(skip_serializing_if = "Option::is_none")]
175 pub expiretm: Option<String>,
176
177 #[serde(skip_serializing_if = "Option::is_none")]
179 pub trigger: Option<String>,
180
181 #[serde(skip_serializing_if = "Option::is_none")]
183 pub displayvol: Option<String>,
184
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub leverage: Option<String>,
188
189 #[serde(skip_serializing_if = "Option::is_none")]
192 pub reduce_only: Option<bool>,
193}
194
195impl From<KrakenSpotAddOrderParams> for KrakenSpotBatchOrderParams {
196 fn from(params: KrakenSpotAddOrderParams) -> Self {
197 Self {
198 side: params.side,
199 order_type: params.order_type,
200 volume: params.volume,
201 price: params.price,
202 price2: params.price2,
203 cl_ord_id: params.cl_ord_id,
204 oflags: params.oflags,
205 timeinforce: params.timeinforce,
206 expiretm: params.expiretm,
207 trigger: params.trigger,
208 displayvol: params.displayvol,
209 leverage: params.leverage,
210 reduce_only: params.reduce_only,
211 }
212 }
213}
214
215#[derive(Clone, Debug, Serialize, Deserialize)]
220pub struct KrakenSpotAddOrderBatchParams {
221 pub pair: Ustr,
223
224 pub orders: Vec<KrakenSpotBatchOrderParams>,
226
227 #[serde(skip_serializing_if = "Option::is_none")]
229 pub asset_class: Option<KrakenAssetClass>,
230}
231
232#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
237#[builder(setter(into, strip_option))]
238pub struct KrakenSpotCancelOrderParams {
239 #[builder(default)]
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub txid: Option<String>,
244
245 #[builder(default)]
247 #[serde(skip_serializing_if = "Option::is_none")]
248 pub cl_ord_id: Option<String>,
249}
250
251#[derive(Clone, Debug, Serialize, Deserialize)]
256pub struct KrakenSpotCancelOrderBatchParams {
257 #[serde(default, skip_serializing_if = "Vec::is_empty")]
261 pub orders: Vec<String>,
262 #[serde(default, skip_serializing_if = "Vec::is_empty")]
264 pub cl_ord_ids: Vec<String>,
265}
266
267#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
275#[builder(setter(into, strip_option))]
276pub struct KrakenSpotEditOrderParams {
277 pub pair: Ustr,
279
280 #[builder(default)]
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub txid: Option<String>,
284
285 #[builder(default)]
287 #[serde(skip_serializing_if = "Option::is_none")]
288 pub cl_ord_id: Option<String>,
289
290 #[builder(default)]
292 #[serde(skip_serializing_if = "Option::is_none")]
293 pub volume: Option<String>,
294
295 #[builder(default)]
297 #[serde(skip_serializing_if = "Option::is_none")]
298 pub price: Option<String>,
299
300 #[builder(default)]
302 #[serde(skip_serializing_if = "Option::is_none")]
303 pub price2: Option<String>,
304}
305
306#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
314#[builder(setter(into, strip_option))]
315pub struct KrakenSpotAmendOrderParams {
316 #[builder(default)]
318 #[serde(skip_serializing_if = "Option::is_none")]
319 pub txid: Option<String>,
320
321 #[builder(default)]
323 #[serde(skip_serializing_if = "Option::is_none")]
324 pub cl_ord_id: Option<String>,
325
326 #[builder(default)]
328 #[serde(skip_serializing_if = "Option::is_none")]
329 pub order_qty: Option<String>,
330
331 #[builder(default)]
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub limit_price: Option<String>,
335
336 #[builder(default)]
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub trigger_price: Option<String>,
340}
341
342#[derive(Clone, Debug, Default, Serialize, Deserialize)]
344pub struct SpotOpenPositionsParams {
345 #[serde(skip_serializing_if = "Option::is_none")]
347 pub txid: Option<String>,
348
349 #[serde(skip_serializing_if = "Option::is_none")]
351 pub docalcs: Option<bool>,
352
353 #[serde(skip_serializing_if = "Option::is_none")]
355 pub consolidation: Option<String>,
356}
357
358#[derive(Clone, Debug, Default, Serialize, Deserialize)]
360pub struct SpotTradeBalanceParams {
361 #[serde(skip_serializing_if = "Option::is_none")]
363 pub asset: Option<String>,
364}
365
366#[cfg(test)]
367mod tests {
368 use rstest::rstest;
369 use serde_json;
370
371 use super::*;
372
373 #[rstest]
374 fn test_add_order_params_builder() {
375 let params = KrakenSpotAddOrderParamsBuilder::default()
376 .pair("XXBTZUSD")
377 .side(KrakenOrderSide::Buy)
378 .order_type(KrakenOrderType::Limit)
379 .volume("0.01")
380 .price("50000.0")
381 .cl_ord_id("my-order-123")
382 .broker("test-broker")
383 .build()
384 .unwrap();
385
386 assert_eq!(params.pair, Ustr::from("XXBTZUSD"));
387 assert_eq!(params.side, KrakenOrderSide::Buy);
388 assert_eq!(params.order_type, KrakenOrderType::Limit);
389 assert_eq!(params.volume, "0.01");
390 assert_eq!(params.price, Some("50000.0".to_string()));
391 assert_eq!(params.cl_ord_id, Some("my-order-123".to_string()));
392 assert_eq!(params.broker, Some(Ustr::from("test-broker")));
393 }
394
395 #[rstest]
396 fn test_add_order_params_serialization() {
397 let params = KrakenSpotAddOrderParamsBuilder::default()
398 .pair("XXBTZUSD")
399 .side(KrakenOrderSide::Buy)
400 .order_type(KrakenOrderType::Market)
401 .volume("0.01")
402 .broker("broker-id")
403 .build()
404 .unwrap();
405
406 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
407
408 assert!(encoded.contains("pair=XXBTZUSD"));
409 assert!(encoded.contains("type=buy"));
410 assert!(encoded.contains("ordertype=market"));
411 assert!(encoded.contains("volume=0.01"));
412 assert!(encoded.contains("broker=broker-id"));
413 assert!(!encoded.contains("price="));
414 }
415
416 #[rstest]
417 fn test_add_order_params_limit_requires_price() {
418 let result = KrakenSpotAddOrderParamsBuilder::default()
419 .pair("XXBTZUSD")
420 .side(KrakenOrderSide::Buy)
421 .order_type(KrakenOrderType::Limit)
422 .volume("0.01")
423 .build();
424
425 assert!(result.is_err());
426 assert!(
427 result
428 .unwrap_err()
429 .to_string()
430 .contains("price is required")
431 );
432 }
433
434 #[rstest]
435 fn test_cancel_order_params_builder() {
436 let params = KrakenSpotCancelOrderParamsBuilder::default()
437 .txid("TXID123")
438 .build()
439 .unwrap();
440
441 assert_eq!(params.txid, Some("TXID123".to_string()));
442 assert_eq!(params.cl_ord_id, None);
443 }
444
445 #[rstest]
446 fn test_cancel_order_params_serialization() {
447 let params = KrakenSpotCancelOrderParamsBuilder::default()
448 .cl_ord_id("my-order")
449 .build()
450 .unwrap();
451
452 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
453
454 assert!(encoded.contains("cl_ord_id=my-order"));
455 assert!(!encoded.contains("txid="));
456 }
457
458 #[rstest]
459 fn test_add_order_params_trailing_stop() {
460 let params = KrakenSpotAddOrderParamsBuilder::default()
461 .pair("XXBTZUSD")
462 .side(KrakenOrderSide::Buy)
463 .order_type(KrakenOrderType::TrailingStop)
464 .volume("0.01")
465 .price("500")
466 .build()
467 .unwrap();
468
469 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
470
471 assert!(encoded.contains("ordertype=trailing-stop"));
472 assert!(encoded.contains("price=500"));
473 }
474
475 #[rstest]
476 fn test_add_order_params_trailing_stop_limit() {
477 let params = KrakenSpotAddOrderParamsBuilder::default()
478 .pair("XXBTZUSD")
479 .side(KrakenOrderSide::Buy)
480 .order_type(KrakenOrderType::TrailingStopLimit)
481 .volume("0.01")
482 .price("500")
483 .price2("100")
484 .build()
485 .unwrap();
486
487 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
488
489 assert!(encoded.contains("ordertype=trailing-stop-limit"));
490 assert!(encoded.contains("price=500"));
491 assert!(encoded.contains("price2=100"));
492 }
493
494 #[rstest]
495 fn test_add_order_params_with_trigger() {
496 let params = KrakenSpotAddOrderParamsBuilder::default()
497 .pair("XXBTZUSD")
498 .side(KrakenOrderSide::Buy)
499 .order_type(KrakenOrderType::StopLoss)
500 .volume("0.01")
501 .price("50000")
502 .trigger("index")
503 .build()
504 .unwrap();
505
506 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
507
508 assert!(encoded.contains("trigger=index"));
509 }
510
511 #[rstest]
512 fn test_add_order_params_with_displayvol() {
513 let params = KrakenSpotAddOrderParamsBuilder::default()
514 .pair("XXBTZUSD")
515 .side(KrakenOrderSide::Buy)
516 .order_type(KrakenOrderType::Limit)
517 .volume("1.0")
518 .price("50000")
519 .displayvol("0.1")
520 .build()
521 .unwrap();
522
523 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
524
525 assert!(encoded.contains("displayvol=0.1"));
526 }
527
528 #[rstest]
529 fn test_add_order_params_with_viqc_flag() {
530 let params = KrakenSpotAddOrderParamsBuilder::default()
531 .pair("XXBTZUSD")
532 .side(KrakenOrderSide::Buy)
533 .order_type(KrakenOrderType::Market)
534 .volume("100")
535 .oflags("viqc")
536 .build()
537 .unwrap();
538
539 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
540
541 assert!(encoded.contains("oflags=viqc"));
542 }
543
544 #[rstest]
545 fn test_batch_order_params_from_preserves_leverage() {
546 let add_params = KrakenSpotAddOrderParamsBuilder::default()
547 .pair("XXBTZUSD")
548 .side(KrakenOrderSide::Buy)
549 .order_type(KrakenOrderType::Limit)
550 .volume("0.5")
551 .price("50000")
552 .leverage("3:1")
553 .build()
554 .unwrap();
555
556 let batch: KrakenSpotBatchOrderParams = add_params.into();
557
558 assert_eq!(
559 batch.leverage.as_deref(),
560 Some("3:1"),
561 "leverage must be preserved through the From conversion"
562 );
563 }
564
565 #[rstest]
566 fn test_batch_order_params_leverage_serializes_to_json() {
567 let batch = KrakenSpotBatchOrderParams {
568 side: KrakenOrderSide::Buy,
569 order_type: KrakenOrderType::Limit,
570 volume: "0.5".to_string(),
571 price: Some("50000".to_string()),
572 price2: None,
573 cl_ord_id: None,
574 oflags: None,
575 timeinforce: None,
576 expiretm: None,
577 trigger: None,
578 displayvol: None,
579 leverage: Some("3:1".to_string()),
580 reduce_only: None,
581 };
582
583 let json = serde_json::to_string(&batch).unwrap();
584 assert!(
585 json.contains(r#""leverage":"3:1""#),
586 "leverage missing from JSON: {json}"
587 );
588 }
589
590 #[rstest]
591 fn test_batch_order_params_no_leverage_omits_field() {
592 let batch = KrakenSpotBatchOrderParams {
594 side: KrakenOrderSide::Sell,
595 order_type: KrakenOrderType::Market,
596 volume: "0.1".to_string(),
597 price: None,
598 price2: None,
599 cl_ord_id: None,
600 oflags: None,
601 timeinforce: None,
602 expiretm: None,
603 trigger: None,
604 displayvol: None,
605 leverage: None,
606 reduce_only: None,
607 };
608
609 let json = serde_json::to_string(&batch).unwrap();
610 assert!(
611 !json.contains("leverage"),
612 "leverage should be absent for cash orders: {json}"
613 );
614 }
615}