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 pub orders: Vec<String>,
260}
261
262#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
270#[builder(setter(into, strip_option))]
271pub struct KrakenSpotEditOrderParams {
272 pub pair: Ustr,
274
275 #[builder(default)]
277 #[serde(skip_serializing_if = "Option::is_none")]
278 pub txid: Option<String>,
279
280 #[builder(default)]
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub cl_ord_id: Option<String>,
284
285 #[builder(default)]
287 #[serde(skip_serializing_if = "Option::is_none")]
288 pub volume: Option<String>,
289
290 #[builder(default)]
292 #[serde(skip_serializing_if = "Option::is_none")]
293 pub price: Option<String>,
294
295 #[builder(default)]
297 #[serde(skip_serializing_if = "Option::is_none")]
298 pub price2: Option<String>,
299}
300
301#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
309#[builder(setter(into, strip_option))]
310pub struct KrakenSpotAmendOrderParams {
311 #[builder(default)]
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub txid: Option<String>,
315
316 #[builder(default)]
318 #[serde(skip_serializing_if = "Option::is_none")]
319 pub cl_ord_id: Option<String>,
320
321 #[builder(default)]
323 #[serde(skip_serializing_if = "Option::is_none")]
324 pub order_qty: Option<String>,
325
326 #[builder(default)]
328 #[serde(skip_serializing_if = "Option::is_none")]
329 pub limit_price: Option<String>,
330
331 #[builder(default)]
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub trigger_price: Option<String>,
335}
336
337#[derive(Clone, Debug, Default, Serialize, Deserialize)]
339pub struct SpotOpenPositionsParams {
340 #[serde(skip_serializing_if = "Option::is_none")]
342 pub txid: Option<String>,
343
344 #[serde(skip_serializing_if = "Option::is_none")]
346 pub docalcs: Option<bool>,
347
348 #[serde(skip_serializing_if = "Option::is_none")]
350 pub consolidation: Option<String>,
351}
352
353#[derive(Clone, Debug, Default, Serialize, Deserialize)]
355pub struct SpotTradeBalanceParams {
356 #[serde(skip_serializing_if = "Option::is_none")]
358 pub asset: Option<String>,
359}
360
361#[cfg(test)]
362mod tests {
363 use rstest::rstest;
364 use serde_json;
365
366 use super::*;
367
368 #[rstest]
369 fn test_add_order_params_builder() {
370 let params = KrakenSpotAddOrderParamsBuilder::default()
371 .pair("XXBTZUSD")
372 .side(KrakenOrderSide::Buy)
373 .order_type(KrakenOrderType::Limit)
374 .volume("0.01")
375 .price("50000.0")
376 .cl_ord_id("my-order-123")
377 .broker("test-broker")
378 .build()
379 .unwrap();
380
381 assert_eq!(params.pair, Ustr::from("XXBTZUSD"));
382 assert_eq!(params.side, KrakenOrderSide::Buy);
383 assert_eq!(params.order_type, KrakenOrderType::Limit);
384 assert_eq!(params.volume, "0.01");
385 assert_eq!(params.price, Some("50000.0".to_string()));
386 assert_eq!(params.cl_ord_id, Some("my-order-123".to_string()));
387 assert_eq!(params.broker, Some(Ustr::from("test-broker")));
388 }
389
390 #[rstest]
391 fn test_add_order_params_serialization() {
392 let params = KrakenSpotAddOrderParamsBuilder::default()
393 .pair("XXBTZUSD")
394 .side(KrakenOrderSide::Buy)
395 .order_type(KrakenOrderType::Market)
396 .volume("0.01")
397 .broker("broker-id")
398 .build()
399 .unwrap();
400
401 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
402
403 assert!(encoded.contains("pair=XXBTZUSD"));
404 assert!(encoded.contains("type=buy"));
405 assert!(encoded.contains("ordertype=market"));
406 assert!(encoded.contains("volume=0.01"));
407 assert!(encoded.contains("broker=broker-id"));
408 assert!(!encoded.contains("price="));
409 }
410
411 #[rstest]
412 fn test_add_order_params_limit_requires_price() {
413 let result = KrakenSpotAddOrderParamsBuilder::default()
414 .pair("XXBTZUSD")
415 .side(KrakenOrderSide::Buy)
416 .order_type(KrakenOrderType::Limit)
417 .volume("0.01")
418 .build();
419
420 assert!(result.is_err());
421 assert!(
422 result
423 .unwrap_err()
424 .to_string()
425 .contains("price is required")
426 );
427 }
428
429 #[rstest]
430 fn test_cancel_order_params_builder() {
431 let params = KrakenSpotCancelOrderParamsBuilder::default()
432 .txid("TXID123")
433 .build()
434 .unwrap();
435
436 assert_eq!(params.txid, Some("TXID123".to_string()));
437 assert_eq!(params.cl_ord_id, None);
438 }
439
440 #[rstest]
441 fn test_cancel_order_params_serialization() {
442 let params = KrakenSpotCancelOrderParamsBuilder::default()
443 .cl_ord_id("my-order")
444 .build()
445 .unwrap();
446
447 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
448
449 assert!(encoded.contains("cl_ord_id=my-order"));
450 assert!(!encoded.contains("txid="));
451 }
452
453 #[rstest]
454 fn test_add_order_params_trailing_stop() {
455 let params = KrakenSpotAddOrderParamsBuilder::default()
456 .pair("XXBTZUSD")
457 .side(KrakenOrderSide::Buy)
458 .order_type(KrakenOrderType::TrailingStop)
459 .volume("0.01")
460 .price("500")
461 .build()
462 .unwrap();
463
464 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
465
466 assert!(encoded.contains("ordertype=trailing-stop"));
467 assert!(encoded.contains("price=500"));
468 }
469
470 #[rstest]
471 fn test_add_order_params_trailing_stop_limit() {
472 let params = KrakenSpotAddOrderParamsBuilder::default()
473 .pair("XXBTZUSD")
474 .side(KrakenOrderSide::Buy)
475 .order_type(KrakenOrderType::TrailingStopLimit)
476 .volume("0.01")
477 .price("500")
478 .price2("100")
479 .build()
480 .unwrap();
481
482 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
483
484 assert!(encoded.contains("ordertype=trailing-stop-limit"));
485 assert!(encoded.contains("price=500"));
486 assert!(encoded.contains("price2=100"));
487 }
488
489 #[rstest]
490 fn test_add_order_params_with_trigger() {
491 let params = KrakenSpotAddOrderParamsBuilder::default()
492 .pair("XXBTZUSD")
493 .side(KrakenOrderSide::Buy)
494 .order_type(KrakenOrderType::StopLoss)
495 .volume("0.01")
496 .price("50000")
497 .trigger("index")
498 .build()
499 .unwrap();
500
501 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
502
503 assert!(encoded.contains("trigger=index"));
504 }
505
506 #[rstest]
507 fn test_add_order_params_with_displayvol() {
508 let params = KrakenSpotAddOrderParamsBuilder::default()
509 .pair("XXBTZUSD")
510 .side(KrakenOrderSide::Buy)
511 .order_type(KrakenOrderType::Limit)
512 .volume("1.0")
513 .price("50000")
514 .displayvol("0.1")
515 .build()
516 .unwrap();
517
518 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
519
520 assert!(encoded.contains("displayvol=0.1"));
521 }
522
523 #[rstest]
524 fn test_add_order_params_with_viqc_flag() {
525 let params = KrakenSpotAddOrderParamsBuilder::default()
526 .pair("XXBTZUSD")
527 .side(KrakenOrderSide::Buy)
528 .order_type(KrakenOrderType::Market)
529 .volume("100")
530 .oflags("viqc")
531 .build()
532 .unwrap();
533
534 let encoded = serde_urlencoded::to_string(¶ms).unwrap();
535
536 assert!(encoded.contains("oflags=viqc"));
537 }
538
539 #[rstest]
540 fn test_batch_order_params_from_preserves_leverage() {
541 let add_params = KrakenSpotAddOrderParamsBuilder::default()
542 .pair("XXBTZUSD")
543 .side(KrakenOrderSide::Buy)
544 .order_type(KrakenOrderType::Limit)
545 .volume("0.5")
546 .price("50000")
547 .leverage("3:1")
548 .build()
549 .unwrap();
550
551 let batch: KrakenSpotBatchOrderParams = add_params.into();
552
553 assert_eq!(
554 batch.leverage.as_deref(),
555 Some("3:1"),
556 "leverage must be preserved through the From conversion"
557 );
558 }
559
560 #[rstest]
561 fn test_batch_order_params_leverage_serializes_to_json() {
562 let batch = KrakenSpotBatchOrderParams {
563 side: KrakenOrderSide::Buy,
564 order_type: KrakenOrderType::Limit,
565 volume: "0.5".to_string(),
566 price: Some("50000".to_string()),
567 price2: None,
568 cl_ord_id: None,
569 oflags: None,
570 timeinforce: None,
571 expiretm: None,
572 trigger: None,
573 displayvol: None,
574 leverage: Some("3:1".to_string()),
575 reduce_only: None,
576 };
577
578 let json = serde_json::to_string(&batch).unwrap();
579 assert!(
580 json.contains(r#""leverage":"3:1""#),
581 "leverage missing from JSON: {json}"
582 );
583 }
584
585 #[rstest]
586 fn test_batch_order_params_no_leverage_omits_field() {
587 let batch = KrakenSpotBatchOrderParams {
589 side: KrakenOrderSide::Sell,
590 order_type: KrakenOrderType::Market,
591 volume: "0.1".to_string(),
592 price: None,
593 price2: None,
594 cl_ord_id: None,
595 oflags: None,
596 timeinforce: None,
597 expiretm: None,
598 trigger: None,
599 displayvol: None,
600 leverage: None,
601 reduce_only: None,
602 };
603
604 let json = serde_json::to_string(&batch).unwrap();
605 assert!(
606 !json.contains("leverage"),
607 "leverage should be absent for cash orders: {json}"
608 );
609 }
610}