1use derive_builder::Builder;
19use serde::{Deserialize, Serialize};
20use ustr::Ustr;
21
22use crate::common::enums::{
23 KrakenFuturesOrderType, KrakenFuturesQtyMode, KrakenOrderSide, KrakenTriggerSignal,
24};
25
26#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
31#[serde(rename_all = "camelCase")]
32#[builder(setter(into, strip_option), build_fn(validate = "Self::validate"))]
33pub struct KrakenFuturesSendOrderParams {
34 pub symbol: Ustr,
36
37 pub side: KrakenOrderSide,
39
40 pub order_type: KrakenFuturesOrderType,
42
43 pub size: String,
45
46 #[builder(default)]
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub cli_ord_id: Option<String>,
50
51 #[builder(default)]
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub limit_price: Option<String>,
55
56 #[builder(default)]
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub stop_price: Option<String>,
60
61 #[builder(default)]
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub reduce_only: Option<bool>,
65
66 #[builder(default)]
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub trigger_signal: Option<KrakenTriggerSignal>,
70
71 #[builder(default)]
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub trailing_stop_deviation_unit: Option<String>,
75
76 #[builder(default)]
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub trailing_stop_max_deviation: Option<String>,
80
81 #[builder(default)]
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub broker: Option<Ustr>,
85}
86
87impl KrakenFuturesSendOrderParamsBuilder {
88 fn validate(&self) -> Result<(), String> {
89 if let Some(ref order_type) = self.order_type {
91 match order_type {
92 KrakenFuturesOrderType::Limit
93 | KrakenFuturesOrderType::Ioc
94 | KrakenFuturesOrderType::Post
95 if (self.limit_price.is_none()
96 || self.limit_price.as_ref().unwrap().is_none()) =>
97 {
98 return Err("limit_price is required for limit orders".to_string());
99 }
100 KrakenFuturesOrderType::Stop | KrakenFuturesOrderType::StopLoss
101 if (self.stop_price.is_none()
102 || self.stop_price.as_ref().unwrap().is_none()) =>
103 {
104 return Err("stop_price is required for stop orders".to_string());
105 }
106 _ => {}
107 }
108 }
109 Ok(())
110 }
111}
112
113#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
118#[serde(rename_all = "camelCase")]
119#[builder(setter(into, strip_option))]
120pub struct KrakenFuturesCancelOrderParams {
121 #[builder(default)]
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub order_id: Option<String>,
125
126 #[builder(default)]
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub cli_ord_id: Option<String>,
130}
131
132#[derive(Clone, Debug, Serialize, Deserialize)]
137pub struct KrakenFuturesBatchCancelItem {
138 pub order: String,
140
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub order_id: Option<String>,
144
145 #[serde(rename = "cliOrdId", skip_serializing_if = "Option::is_none")]
147 pub cli_ord_id: Option<String>,
148}
149
150impl KrakenFuturesBatchCancelItem {
151 #[must_use]
153 pub fn from_order_id(order_id: impl Into<String>) -> Self {
154 Self {
155 order: "cancel".to_string(),
156 order_id: Some(order_id.into()),
157 cli_ord_id: None,
158 }
159 }
160
161 #[must_use]
163 pub fn from_client_order_id(cli_ord_id: impl Into<String>) -> Self {
164 Self {
165 order: "cancel".to_string(),
166 order_id: None,
167 cli_ord_id: Some(cli_ord_id.into()),
168 }
169 }
170}
171
172#[derive(Clone, Debug, Serialize, Deserialize)]
177#[serde(rename_all = "camelCase")]
178pub struct KrakenFuturesBatchSendItem {
179 pub order: String,
181
182 #[serde(rename = "order_tag")]
184 pub order_tag: String,
185
186 pub symbol: Ustr,
188
189 pub side: KrakenOrderSide,
191
192 pub order_type: KrakenFuturesOrderType,
194
195 pub size: String,
197
198 #[serde(rename = "cliOrdId", skip_serializing_if = "Option::is_none")]
200 pub cli_ord_id: Option<String>,
201
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub limit_price: Option<String>,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub stop_price: Option<String>,
209
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub reduce_only: Option<bool>,
213
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub trigger_signal: Option<KrakenTriggerSignal>,
217}
218
219impl KrakenFuturesBatchSendItem {
220 #[must_use]
222 pub fn from_params(params: KrakenFuturesSendOrderParams, order_tag: impl Into<String>) -> Self {
223 Self {
224 order: "send".to_string(),
225 order_tag: order_tag.into(),
226 symbol: params.symbol,
227 side: params.side,
228 order_type: params.order_type,
229 size: params.size,
230 cli_ord_id: params.cli_ord_id,
231 limit_price: params.limit_price,
232 stop_price: params.stop_price,
233 reduce_only: params.reduce_only,
234 trigger_signal: params.trigger_signal,
235 }
236 }
237}
238
239#[derive(Clone, Debug, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase")]
245pub struct KrakenFuturesBatchEditItem {
246 pub order: String,
248
249 #[serde(rename = "order_tag")]
251 pub order_tag: String,
252
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub order_id: Option<String>,
256
257 #[serde(rename = "cliOrdId", skip_serializing_if = "Option::is_none")]
259 pub cli_ord_id: Option<String>,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub size: Option<String>,
264
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub limit_price: Option<String>,
268
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub stop_price: Option<String>,
272
273 #[serde(skip_serializing_if = "Option::is_none")]
275 pub qty_mode: Option<KrakenFuturesQtyMode>,
276}
277
278impl KrakenFuturesBatchEditItem {
279 #[must_use]
281 pub fn from_params(params: KrakenFuturesEditOrderParams, order_tag: impl Into<String>) -> Self {
282 Self {
283 order: "edit".to_string(),
284 order_tag: order_tag.into(),
285 order_id: params.order_id,
286 cli_ord_id: params.cli_ord_id,
287 size: params.size,
288 limit_price: params.limit_price,
289 stop_price: params.stop_price,
290 qty_mode: params.qty_mode,
291 }
292 }
293}
294
295#[derive(Clone, Debug, Serialize, Deserialize)]
303#[serde(rename_all = "camelCase")]
304pub struct KrakenFuturesBatchOrderParams<T: Serialize> {
305 pub batch_order: Vec<T>,
307}
308
309impl<T: Serialize> KrakenFuturesBatchOrderParams<T> {
310 #[must_use]
312 pub fn new(batch_order: Vec<T>) -> Self {
313 Self { batch_order }
314 }
315
316 pub fn to_body(&self) -> Result<String, serde_json::Error> {
318 let json_str = serde_json::to_string(self)?;
319 Ok(format!("json={json_str}"))
320 }
321}
322
323#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
328#[serde(rename_all = "camelCase")]
329#[builder(setter(into, strip_option))]
330pub struct KrakenFuturesEditOrderParams {
331 #[builder(default)]
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub order_id: Option<String>,
335
336 #[builder(default)]
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub cli_ord_id: Option<String>,
340
341 #[builder(default)]
343 #[serde(skip_serializing_if = "Option::is_none")]
344 pub size: Option<String>,
345
346 #[builder(default)]
348 #[serde(skip_serializing_if = "Option::is_none")]
349 pub limit_price: Option<String>,
350
351 #[builder(default)]
353 #[serde(skip_serializing_if = "Option::is_none")]
354 pub stop_price: Option<String>,
355
356 #[builder(default)]
358 #[serde(skip_serializing_if = "Option::is_none")]
359 pub qty_mode: Option<KrakenFuturesQtyMode>,
360}
361
362#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
367#[serde(rename_all = "camelCase")]
368#[builder(setter(into, strip_option), default)]
369pub struct KrakenFuturesCancelAllOrdersParams {
370 #[serde(skip_serializing_if = "Option::is_none")]
372 pub symbol: Option<Ustr>,
373}
374
375#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
380#[serde(rename_all = "camelCase")]
381#[builder(setter(into, strip_option), default)]
382pub struct KrakenFuturesOpenOrdersParams {
383 }
385
386#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
391#[serde(rename_all = "camelCase")]
392#[builder(setter(into, strip_option), default)]
393pub struct KrakenFuturesFillsParams {
394 #[serde(skip_serializing_if = "Option::is_none")]
396 pub last_fill_time: Option<String>,
397}
398
399#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
404#[serde(rename_all = "camelCase")]
405#[builder(setter(into, strip_option), default)]
406pub struct KrakenFuturesOpenPositionsParams {
407 }
409
410#[cfg(test)]
411mod tests {
412 use rstest::rstest;
413
414 use super::*;
415
416 #[rstest]
417 fn test_send_order_params_builder() {
418 let params = KrakenFuturesSendOrderParamsBuilder::default()
419 .symbol("PI_XBTUSD")
420 .side(KrakenOrderSide::Buy)
421 .order_type(KrakenFuturesOrderType::Limit)
422 .size("1000")
423 .limit_price("50000.0")
424 .cli_ord_id("test-order-123")
425 .reduce_only(false)
426 .build()
427 .unwrap();
428
429 assert_eq!(params.symbol, Ustr::from("PI_XBTUSD"));
430 assert_eq!(params.side, KrakenOrderSide::Buy);
431 assert_eq!(params.order_type, KrakenFuturesOrderType::Limit);
432 assert_eq!(params.size, "1000");
433 assert_eq!(params.limit_price, Some("50000.0".to_string()));
434 assert_eq!(params.cli_ord_id, Some("test-order-123".to_string()));
435 }
436
437 #[rstest]
438 fn test_send_order_params_serialization() {
439 let params = KrakenFuturesSendOrderParamsBuilder::default()
440 .symbol("PI_XBTUSD")
441 .side(KrakenOrderSide::Buy)
442 .order_type(KrakenFuturesOrderType::Ioc)
443 .size("500")
444 .limit_price("48000.0")
445 .build()
446 .unwrap();
447
448 let json = serde_json::to_string(¶ms).unwrap();
449 assert!(json.contains("\"orderType\":\"ioc\""));
450 assert!(json.contains("\"limitPrice\":\"48000.0\""));
451 }
452
453 #[rstest]
454 fn test_send_order_params_serialization_with_trigger_signal() {
455 let params = KrakenFuturesSendOrderParamsBuilder::default()
456 .symbol("PI_XBTUSD")
457 .side(KrakenOrderSide::Buy)
458 .order_type(KrakenFuturesOrderType::Stop)
459 .size("500")
460 .stop_price("47000.0")
461 .trigger_signal(KrakenTriggerSignal::Mark)
462 .build()
463 .unwrap();
464
465 let json = serde_json::to_string(¶ms).unwrap();
466 assert!(json.contains("\"triggerSignal\":\"mark\""));
467 assert!(json.contains("\"stopPrice\":\"47000.0\""));
468 }
469
470 #[rstest]
471 fn test_send_order_params_serialization_with_index_trigger_signal() {
472 let params = KrakenFuturesSendOrderParamsBuilder::default()
473 .symbol("PI_XBTUSD")
474 .side(KrakenOrderSide::Buy)
475 .order_type(KrakenFuturesOrderType::Stop)
476 .size("500")
477 .stop_price("47000.0")
478 .trigger_signal(KrakenTriggerSignal::Index)
479 .build()
480 .unwrap();
481
482 let json = serde_json::to_string(¶ms).unwrap();
483 assert!(json.contains("\"triggerSignal\":\"spot\""));
484 assert!(json.contains("\"stopPrice\":\"47000.0\""));
485 }
486
487 #[rstest]
488 fn test_send_order_params_missing_limit_price() {
489 let result = KrakenFuturesSendOrderParamsBuilder::default()
490 .symbol("PI_XBTUSD")
491 .side(KrakenOrderSide::Buy)
492 .order_type(KrakenFuturesOrderType::Limit)
493 .size("1000")
494 .build();
495
496 assert!(result.is_err());
497 assert!(result.unwrap_err().to_string().contains("limit_price"));
498 }
499
500 #[rstest]
501 fn test_cancel_order_params_builder() {
502 let params = KrakenFuturesCancelOrderParamsBuilder::default()
503 .order_id("abc-123")
504 .build()
505 .unwrap();
506
507 assert_eq!(params.order_id, Some("abc-123".to_string()));
508 }
509
510 #[rstest]
511 fn test_edit_order_params_builder() {
512 let params = KrakenFuturesEditOrderParamsBuilder::default()
513 .order_id("abc-123")
514 .size("2000")
515 .limit_price("51000.0")
516 .build()
517 .unwrap();
518
519 assert_eq!(params.order_id, Some("abc-123".to_string()));
520 assert_eq!(params.size, Some("2000".to_string()));
521 assert_eq!(params.limit_price, Some("51000.0".to_string()));
522 }
523
524 #[rstest]
525 fn test_batch_send_item_from_params() {
526 let params = KrakenFuturesSendOrderParamsBuilder::default()
527 .symbol("PI_XBTUSD")
528 .side(KrakenOrderSide::Buy)
529 .order_type(KrakenFuturesOrderType::Limit)
530 .size("1000")
531 .limit_price("50000.0")
532 .cli_ord_id("test-batch-1")
533 .build()
534 .unwrap();
535
536 let item = KrakenFuturesBatchSendItem::from_params(params, "0");
537
538 assert_eq!(item.order, "send");
539 assert_eq!(item.order_tag, "0");
540 assert_eq!(item.symbol, Ustr::from("PI_XBTUSD"));
541 assert_eq!(item.side, KrakenOrderSide::Buy);
542 assert_eq!(item.order_type, KrakenFuturesOrderType::Limit);
543 assert_eq!(item.size, "1000");
544 assert_eq!(item.limit_price, Some("50000.0".to_string()));
545 assert_eq!(item.cli_ord_id, Some("test-batch-1".to_string()));
546 }
547
548 #[rstest]
549 fn test_batch_send_item_serialization() {
550 let params = KrakenFuturesSendOrderParamsBuilder::default()
551 .symbol("PI_XBTUSD")
552 .side(KrakenOrderSide::Sell)
553 .order_type(KrakenFuturesOrderType::Market)
554 .size("500")
555 .reduce_only(true)
556 .build()
557 .unwrap();
558
559 let item = KrakenFuturesBatchSendItem::from_params(params, "1");
560 let value = serde_json::to_value(&item).unwrap();
561
562 assert_eq!(
563 value,
564 serde_json::json!({
565 "order": "send",
566 "order_tag": "1",
567 "symbol": "PI_XBTUSD",
568 "side": "sell",
569 "orderType": "mkt",
570 "size": "500",
571 "reduceOnly": true,
572 }),
573 );
574 }
575
576 #[rstest]
577 fn test_batch_edit_item_from_params() {
578 let params = KrakenFuturesEditOrderParamsBuilder::default()
579 .order_id("order-123")
580 .size("2000")
581 .limit_price("51000.0")
582 .build()
583 .unwrap();
584
585 let item = KrakenFuturesBatchEditItem::from_params(params, "0");
586
587 assert_eq!(item.order, "edit");
588 assert_eq!(item.order_tag, "0");
589 assert_eq!(item.order_id, Some("order-123".to_string()));
590 assert_eq!(item.size, Some("2000".to_string()));
591 assert_eq!(item.limit_price, Some("51000.0".to_string()));
592 }
593
594 #[rstest]
595 fn test_batch_edit_item_serialization() {
596 let params = KrakenFuturesEditOrderParamsBuilder::default()
597 .cli_ord_id("my-order")
598 .limit_price("55000.0")
599 .build()
600 .unwrap();
601
602 let item = KrakenFuturesBatchEditItem::from_params(params, "2");
603 let value = serde_json::to_value(&item).unwrap();
604
605 assert_eq!(
606 value,
607 serde_json::json!({
608 "order": "edit",
609 "order_tag": "2",
610 "cliOrdId": "my-order",
611 "limitPrice": "55000.0",
612 }),
613 );
614 }
615
616 #[rstest]
617 fn test_batch_order_params_to_body() {
618 let params = KrakenFuturesSendOrderParamsBuilder::default()
619 .symbol("PI_XBTUSD")
620 .side(KrakenOrderSide::Buy)
621 .order_type(KrakenFuturesOrderType::Limit)
622 .size("100")
623 .limit_price("50000.0")
624 .build()
625 .unwrap();
626
627 let item = KrakenFuturesBatchSendItem::from_params(params, "0");
628 let batch = KrakenFuturesBatchOrderParams::new(vec![item]);
629 let body = batch.to_body().unwrap();
630
631 assert!(body.starts_with("json="));
632 assert!(body.contains("\"batchOrder\""));
633 assert!(body.contains("\"order\":\"send\""));
634 }
635}