nautilus_kraken/http/futures/
query.rs1use derive_builder::Builder;
19use serde::{Deserialize, Serialize};
20use ustr::Ustr;
21
22use crate::common::enums::{KrakenFuturesOrderType, KrakenOrderSide, KrakenTriggerSignal};
23
24#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
29#[serde(rename_all = "camelCase")]
30#[builder(setter(into, strip_option), build_fn(validate = "Self::validate"))]
31pub struct KrakenFuturesSendOrderParams {
32 pub symbol: Ustr,
34
35 pub side: KrakenOrderSide,
37
38 pub order_type: KrakenFuturesOrderType,
40
41 pub size: String,
43
44 #[builder(default)]
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub cli_ord_id: Option<String>,
48
49 #[builder(default)]
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub limit_price: Option<String>,
53
54 #[builder(default)]
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub stop_price: Option<String>,
58
59 #[builder(default)]
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub reduce_only: Option<bool>,
63
64 #[builder(default)]
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub trigger_signal: Option<KrakenTriggerSignal>,
68
69 #[builder(default)]
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub trailing_stop_deviation_unit: Option<String>,
73
74 #[builder(default)]
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub trailing_stop_max_deviation: Option<String>,
78
79 #[builder(default)]
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub broker: Option<Ustr>,
83}
84
85impl KrakenFuturesSendOrderParamsBuilder {
86 fn validate(&self) -> Result<(), String> {
87 if let Some(ref order_type) = self.order_type {
89 match order_type {
90 KrakenFuturesOrderType::Limit
91 | KrakenFuturesOrderType::Ioc
92 | KrakenFuturesOrderType::Post
93 if (self.limit_price.is_none()
94 || self.limit_price.as_ref().unwrap().is_none()) =>
95 {
96 return Err("limit_price is required for limit orders".to_string());
97 }
98 KrakenFuturesOrderType::Stop | KrakenFuturesOrderType::StopLoss
99 if (self.stop_price.is_none()
100 || self.stop_price.as_ref().unwrap().is_none()) =>
101 {
102 return Err("stop_price is required for stop orders".to_string());
103 }
104 _ => {}
105 }
106 }
107 Ok(())
108 }
109}
110
111#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
116#[serde(rename_all = "camelCase")]
117#[builder(setter(into, strip_option))]
118pub struct KrakenFuturesCancelOrderParams {
119 #[builder(default)]
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub order_id: Option<String>,
123
124 #[builder(default)]
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub cli_ord_id: Option<String>,
128}
129
130#[derive(Clone, Debug, Serialize, Deserialize)]
135pub struct KrakenFuturesBatchCancelItem {
136 pub order: String,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub order_id: Option<String>,
142
143 #[serde(rename = "cliOrdId", skip_serializing_if = "Option::is_none")]
145 pub cli_ord_id: Option<String>,
146}
147
148impl KrakenFuturesBatchCancelItem {
149 #[must_use]
151 pub fn from_order_id(order_id: impl Into<String>) -> Self {
152 Self {
153 order: "cancel".to_string(),
154 order_id: Some(order_id.into()),
155 cli_ord_id: None,
156 }
157 }
158
159 #[must_use]
161 pub fn from_client_order_id(cli_ord_id: impl Into<String>) -> Self {
162 Self {
163 order: "cancel".to_string(),
164 order_id: None,
165 cli_ord_id: Some(cli_ord_id.into()),
166 }
167 }
168}
169
170#[derive(Clone, Debug, Serialize, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct KrakenFuturesBatchSendItem {
177 pub order: String,
179
180 pub order_tag: String,
182
183 pub symbol: Ustr,
185
186 pub side: KrakenOrderSide,
188
189 pub order_type: KrakenFuturesOrderType,
191
192 pub size: String,
194
195 #[serde(rename = "cliOrdId", skip_serializing_if = "Option::is_none")]
197 pub cli_ord_id: Option<String>,
198
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub limit_price: Option<String>,
202
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub stop_price: Option<String>,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub reduce_only: Option<bool>,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub trigger_signal: Option<KrakenTriggerSignal>,
214}
215
216impl KrakenFuturesBatchSendItem {
217 #[must_use]
219 pub fn from_params(params: KrakenFuturesSendOrderParams, order_tag: impl Into<String>) -> Self {
220 Self {
221 order: "send".to_string(),
222 order_tag: order_tag.into(),
223 symbol: params.symbol,
224 side: params.side,
225 order_type: params.order_type,
226 size: params.size,
227 cli_ord_id: params.cli_ord_id,
228 limit_price: params.limit_price,
229 stop_price: params.stop_price,
230 reduce_only: params.reduce_only,
231 trigger_signal: params.trigger_signal,
232 }
233 }
234}
235
236#[derive(Clone, Debug, Serialize, Deserialize)]
241#[serde(rename_all = "camelCase")]
242pub struct KrakenFuturesBatchEditItem {
243 pub order: String,
245
246 pub order_tag: String,
248
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub order_id: Option<String>,
252
253 #[serde(rename = "cliOrdId", skip_serializing_if = "Option::is_none")]
255 pub cli_ord_id: Option<String>,
256
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub size: Option<String>,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub limit_price: Option<String>,
264
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub stop_price: Option<String>,
268}
269
270impl KrakenFuturesBatchEditItem {
271 #[must_use]
273 pub fn from_params(params: KrakenFuturesEditOrderParams, order_tag: impl Into<String>) -> Self {
274 Self {
275 order: "edit".to_string(),
276 order_tag: order_tag.into(),
277 order_id: params.order_id,
278 cli_ord_id: params.cli_ord_id,
279 size: params.size,
280 limit_price: params.limit_price,
281 stop_price: params.stop_price,
282 }
283 }
284}
285
286#[derive(Clone, Debug, Serialize, Deserialize)]
294#[serde(rename_all = "camelCase")]
295pub struct KrakenFuturesBatchOrderParams<T: Serialize> {
296 pub batch_order: Vec<T>,
298}
299
300impl<T: Serialize> KrakenFuturesBatchOrderParams<T> {
301 #[must_use]
303 pub fn new(batch_order: Vec<T>) -> Self {
304 Self { batch_order }
305 }
306
307 pub fn to_body(&self) -> Result<String, serde_json::Error> {
309 let json_str = serde_json::to_string(self)?;
310 Ok(format!("json={json_str}"))
311 }
312}
313
314#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
319#[serde(rename_all = "camelCase")]
320#[builder(setter(into, strip_option))]
321pub struct KrakenFuturesEditOrderParams {
322 #[builder(default)]
324 #[serde(skip_serializing_if = "Option::is_none")]
325 pub order_id: Option<String>,
326
327 #[builder(default)]
329 #[serde(skip_serializing_if = "Option::is_none")]
330 pub cli_ord_id: Option<String>,
331
332 #[builder(default)]
334 #[serde(skip_serializing_if = "Option::is_none")]
335 pub size: Option<String>,
336
337 #[builder(default)]
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub limit_price: Option<String>,
341
342 #[builder(default)]
344 #[serde(skip_serializing_if = "Option::is_none")]
345 pub stop_price: Option<String>,
346}
347
348#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
353#[serde(rename_all = "camelCase")]
354#[builder(setter(into, strip_option), default)]
355pub struct KrakenFuturesCancelAllOrdersParams {
356 #[serde(skip_serializing_if = "Option::is_none")]
358 pub symbol: Option<Ustr>,
359}
360
361#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
366#[serde(rename_all = "camelCase")]
367#[builder(setter(into, strip_option), default)]
368pub struct KrakenFuturesOpenOrdersParams {
369 }
371
372#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
377#[serde(rename_all = "camelCase")]
378#[builder(setter(into, strip_option), default)]
379pub struct KrakenFuturesFillsParams {
380 #[serde(skip_serializing_if = "Option::is_none")]
382 pub last_fill_time: Option<String>,
383}
384
385#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
390#[serde(rename_all = "camelCase")]
391#[builder(setter(into, strip_option), default)]
392pub struct KrakenFuturesOpenPositionsParams {
393 }
395
396#[cfg(test)]
397mod tests {
398 use rstest::rstest;
399
400 use super::*;
401
402 #[rstest]
403 fn test_send_order_params_builder() {
404 let params = KrakenFuturesSendOrderParamsBuilder::default()
405 .symbol("PI_XBTUSD")
406 .side(KrakenOrderSide::Buy)
407 .order_type(KrakenFuturesOrderType::Limit)
408 .size("1000")
409 .limit_price("50000.0")
410 .cli_ord_id("test-order-123")
411 .reduce_only(false)
412 .build()
413 .unwrap();
414
415 assert_eq!(params.symbol, Ustr::from("PI_XBTUSD"));
416 assert_eq!(params.side, KrakenOrderSide::Buy);
417 assert_eq!(params.order_type, KrakenFuturesOrderType::Limit);
418 assert_eq!(params.size, "1000");
419 assert_eq!(params.limit_price, Some("50000.0".to_string()));
420 assert_eq!(params.cli_ord_id, Some("test-order-123".to_string()));
421 }
422
423 #[rstest]
424 fn test_send_order_params_serialization() {
425 let params = KrakenFuturesSendOrderParamsBuilder::default()
426 .symbol("PI_XBTUSD")
427 .side(KrakenOrderSide::Buy)
428 .order_type(KrakenFuturesOrderType::Ioc)
429 .size("500")
430 .limit_price("48000.0")
431 .build()
432 .unwrap();
433
434 let json = serde_json::to_string(¶ms).unwrap();
435 assert!(json.contains("\"orderType\":\"ioc\""));
436 assert!(json.contains("\"limitPrice\":\"48000.0\""));
437 }
438
439 #[rstest]
440 fn test_send_order_params_serialization_with_trigger_signal() {
441 let params = KrakenFuturesSendOrderParamsBuilder::default()
442 .symbol("PI_XBTUSD")
443 .side(KrakenOrderSide::Buy)
444 .order_type(KrakenFuturesOrderType::Stop)
445 .size("500")
446 .stop_price("47000.0")
447 .trigger_signal(KrakenTriggerSignal::Mark)
448 .build()
449 .unwrap();
450
451 let json = serde_json::to_string(¶ms).unwrap();
452 assert!(json.contains("\"triggerSignal\":\"mark\""));
453 assert!(json.contains("\"stopPrice\":\"47000.0\""));
454 }
455
456 #[rstest]
457 fn test_send_order_params_serialization_with_index_trigger_signal() {
458 let params = KrakenFuturesSendOrderParamsBuilder::default()
459 .symbol("PI_XBTUSD")
460 .side(KrakenOrderSide::Buy)
461 .order_type(KrakenFuturesOrderType::Stop)
462 .size("500")
463 .stop_price("47000.0")
464 .trigger_signal(KrakenTriggerSignal::Index)
465 .build()
466 .unwrap();
467
468 let json = serde_json::to_string(¶ms).unwrap();
469 assert!(json.contains("\"triggerSignal\":\"spot\""));
470 assert!(json.contains("\"stopPrice\":\"47000.0\""));
471 }
472
473 #[rstest]
474 fn test_send_order_params_missing_limit_price() {
475 let result = KrakenFuturesSendOrderParamsBuilder::default()
476 .symbol("PI_XBTUSD")
477 .side(KrakenOrderSide::Buy)
478 .order_type(KrakenFuturesOrderType::Limit)
479 .size("1000")
480 .build();
481
482 assert!(result.is_err());
483 assert!(result.unwrap_err().to_string().contains("limit_price"));
484 }
485
486 #[rstest]
487 fn test_cancel_order_params_builder() {
488 let params = KrakenFuturesCancelOrderParamsBuilder::default()
489 .order_id("abc-123")
490 .build()
491 .unwrap();
492
493 assert_eq!(params.order_id, Some("abc-123".to_string()));
494 }
495
496 #[rstest]
497 fn test_edit_order_params_builder() {
498 let params = KrakenFuturesEditOrderParamsBuilder::default()
499 .order_id("abc-123")
500 .size("2000")
501 .limit_price("51000.0")
502 .build()
503 .unwrap();
504
505 assert_eq!(params.order_id, Some("abc-123".to_string()));
506 assert_eq!(params.size, Some("2000".to_string()));
507 assert_eq!(params.limit_price, Some("51000.0".to_string()));
508 }
509
510 #[rstest]
511 fn test_batch_send_item_from_params() {
512 let params = KrakenFuturesSendOrderParamsBuilder::default()
513 .symbol("PI_XBTUSD")
514 .side(KrakenOrderSide::Buy)
515 .order_type(KrakenFuturesOrderType::Limit)
516 .size("1000")
517 .limit_price("50000.0")
518 .cli_ord_id("test-batch-1")
519 .build()
520 .unwrap();
521
522 let item = KrakenFuturesBatchSendItem::from_params(params, "0");
523
524 assert_eq!(item.order, "send");
525 assert_eq!(item.order_tag, "0");
526 assert_eq!(item.symbol, Ustr::from("PI_XBTUSD"));
527 assert_eq!(item.side, KrakenOrderSide::Buy);
528 assert_eq!(item.order_type, KrakenFuturesOrderType::Limit);
529 assert_eq!(item.size, "1000");
530 assert_eq!(item.limit_price, Some("50000.0".to_string()));
531 assert_eq!(item.cli_ord_id, Some("test-batch-1".to_string()));
532 }
533
534 #[rstest]
535 fn test_batch_send_item_serialization() {
536 let params = KrakenFuturesSendOrderParamsBuilder::default()
537 .symbol("PI_XBTUSD")
538 .side(KrakenOrderSide::Sell)
539 .order_type(KrakenFuturesOrderType::Market)
540 .size("500")
541 .reduce_only(true)
542 .build()
543 .unwrap();
544
545 let item = KrakenFuturesBatchSendItem::from_params(params, "1");
546 let json = serde_json::to_string(&item).unwrap();
547
548 assert!(json.contains("\"order\":\"send\""));
549 assert!(json.contains("\"orderTag\":\"1\""));
550 assert!(json.contains("\"orderType\":\"mkt\""));
551 assert!(json.contains("\"reduceOnly\":true"));
552 }
553
554 #[rstest]
555 fn test_batch_edit_item_from_params() {
556 let params = KrakenFuturesEditOrderParamsBuilder::default()
557 .order_id("order-123")
558 .size("2000")
559 .limit_price("51000.0")
560 .build()
561 .unwrap();
562
563 let item = KrakenFuturesBatchEditItem::from_params(params, "0");
564
565 assert_eq!(item.order, "edit");
566 assert_eq!(item.order_tag, "0");
567 assert_eq!(item.order_id, Some("order-123".to_string()));
568 assert_eq!(item.size, Some("2000".to_string()));
569 assert_eq!(item.limit_price, Some("51000.0".to_string()));
570 }
571
572 #[rstest]
573 fn test_batch_edit_item_serialization() {
574 let params = KrakenFuturesEditOrderParamsBuilder::default()
575 .cli_ord_id("my-order")
576 .limit_price("55000.0")
577 .build()
578 .unwrap();
579
580 let item = KrakenFuturesBatchEditItem::from_params(params, "2");
581 let json = serde_json::to_string(&item).unwrap();
582
583 assert!(json.contains("\"order\":\"edit\""));
584 assert!(json.contains("\"orderTag\":\"2\""));
585 assert!(json.contains("\"cliOrdId\":\"my-order\""));
586 assert!(json.contains("\"limitPrice\":\"55000.0\""));
587 }
588
589 #[rstest]
590 fn test_batch_order_params_to_body() {
591 let params = KrakenFuturesSendOrderParamsBuilder::default()
592 .symbol("PI_XBTUSD")
593 .side(KrakenOrderSide::Buy)
594 .order_type(KrakenFuturesOrderType::Limit)
595 .size("100")
596 .limit_price("50000.0")
597 .build()
598 .unwrap();
599
600 let item = KrakenFuturesBatchSendItem::from_params(params, "0");
601 let batch = KrakenFuturesBatchOrderParams::new(vec![item]);
602 let body = batch.to_body().unwrap();
603
604 assert!(body.starts_with("json="));
605 assert!(body.contains("\"batchOrder\""));
606 assert!(body.contains("\"order\":\"send\""));
607 }
608}