1use 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 #[serde(rename = "order_tag")]
182 pub order_tag: String,
183
184 pub symbol: Ustr,
186
187 pub side: KrakenOrderSide,
189
190 pub order_type: KrakenFuturesOrderType,
192
193 pub size: String,
195
196 #[serde(rename = "cliOrdId", skip_serializing_if = "Option::is_none")]
198 pub cli_ord_id: Option<String>,
199
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub limit_price: Option<String>,
203
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub stop_price: Option<String>,
207
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub reduce_only: Option<bool>,
211
212 #[serde(skip_serializing_if = "Option::is_none")]
214 pub trigger_signal: Option<KrakenTriggerSignal>,
215}
216
217impl KrakenFuturesBatchSendItem {
218 #[must_use]
220 pub fn from_params(params: KrakenFuturesSendOrderParams, order_tag: impl Into<String>) -> Self {
221 Self {
222 order: "send".to_string(),
223 order_tag: order_tag.into(),
224 symbol: params.symbol,
225 side: params.side,
226 order_type: params.order_type,
227 size: params.size,
228 cli_ord_id: params.cli_ord_id,
229 limit_price: params.limit_price,
230 stop_price: params.stop_price,
231 reduce_only: params.reduce_only,
232 trigger_signal: params.trigger_signal,
233 }
234 }
235}
236
237#[derive(Clone, Debug, Serialize, Deserialize)]
242#[serde(rename_all = "camelCase")]
243pub struct KrakenFuturesBatchEditItem {
244 pub order: String,
246
247 #[serde(rename = "order_tag")]
249 pub order_tag: String,
250
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub order_id: Option<String>,
254
255 #[serde(rename = "cliOrdId", skip_serializing_if = "Option::is_none")]
257 pub cli_ord_id: Option<String>,
258
259 #[serde(skip_serializing_if = "Option::is_none")]
261 pub size: Option<String>,
262
263 #[serde(skip_serializing_if = "Option::is_none")]
265 pub limit_price: Option<String>,
266
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub stop_price: Option<String>,
270}
271
272impl KrakenFuturesBatchEditItem {
273 #[must_use]
275 pub fn from_params(params: KrakenFuturesEditOrderParams, order_tag: impl Into<String>) -> Self {
276 Self {
277 order: "edit".to_string(),
278 order_tag: order_tag.into(),
279 order_id: params.order_id,
280 cli_ord_id: params.cli_ord_id,
281 size: params.size,
282 limit_price: params.limit_price,
283 stop_price: params.stop_price,
284 }
285 }
286}
287
288#[derive(Clone, Debug, Serialize, Deserialize)]
296#[serde(rename_all = "camelCase")]
297pub struct KrakenFuturesBatchOrderParams<T: Serialize> {
298 pub batch_order: Vec<T>,
300}
301
302impl<T: Serialize> KrakenFuturesBatchOrderParams<T> {
303 #[must_use]
305 pub fn new(batch_order: Vec<T>) -> Self {
306 Self { batch_order }
307 }
308
309 pub fn to_body(&self) -> Result<String, serde_json::Error> {
311 let json_str = serde_json::to_string(self)?;
312 Ok(format!("json={json_str}"))
313 }
314}
315
316#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
321#[serde(rename_all = "camelCase")]
322#[builder(setter(into, strip_option))]
323pub struct KrakenFuturesEditOrderParams {
324 #[builder(default)]
326 #[serde(skip_serializing_if = "Option::is_none")]
327 pub order_id: Option<String>,
328
329 #[builder(default)]
331 #[serde(skip_serializing_if = "Option::is_none")]
332 pub cli_ord_id: Option<String>,
333
334 #[builder(default)]
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub size: Option<String>,
338
339 #[builder(default)]
341 #[serde(skip_serializing_if = "Option::is_none")]
342 pub limit_price: Option<String>,
343
344 #[builder(default)]
346 #[serde(skip_serializing_if = "Option::is_none")]
347 pub stop_price: Option<String>,
348}
349
350#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
355#[serde(rename_all = "camelCase")]
356#[builder(setter(into, strip_option), default)]
357pub struct KrakenFuturesCancelAllOrdersParams {
358 #[serde(skip_serializing_if = "Option::is_none")]
360 pub symbol: Option<Ustr>,
361}
362
363#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
368#[serde(rename_all = "camelCase")]
369#[builder(setter(into, strip_option), default)]
370pub struct KrakenFuturesOpenOrdersParams {
371 }
373
374#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
379#[serde(rename_all = "camelCase")]
380#[builder(setter(into, strip_option), default)]
381pub struct KrakenFuturesFillsParams {
382 #[serde(skip_serializing_if = "Option::is_none")]
384 pub last_fill_time: Option<String>,
385}
386
387#[derive(Clone, Debug, Default, Serialize, Deserialize, Builder)]
392#[serde(rename_all = "camelCase")]
393#[builder(setter(into, strip_option), default)]
394pub struct KrakenFuturesOpenPositionsParams {
395 }
397
398#[cfg(test)]
399mod tests {
400 use rstest::rstest;
401
402 use super::*;
403
404 #[rstest]
405 fn test_send_order_params_builder() {
406 let params = KrakenFuturesSendOrderParamsBuilder::default()
407 .symbol("PI_XBTUSD")
408 .side(KrakenOrderSide::Buy)
409 .order_type(KrakenFuturesOrderType::Limit)
410 .size("1000")
411 .limit_price("50000.0")
412 .cli_ord_id("test-order-123")
413 .reduce_only(false)
414 .build()
415 .unwrap();
416
417 assert_eq!(params.symbol, Ustr::from("PI_XBTUSD"));
418 assert_eq!(params.side, KrakenOrderSide::Buy);
419 assert_eq!(params.order_type, KrakenFuturesOrderType::Limit);
420 assert_eq!(params.size, "1000");
421 assert_eq!(params.limit_price, Some("50000.0".to_string()));
422 assert_eq!(params.cli_ord_id, Some("test-order-123".to_string()));
423 }
424
425 #[rstest]
426 fn test_send_order_params_serialization() {
427 let params = KrakenFuturesSendOrderParamsBuilder::default()
428 .symbol("PI_XBTUSD")
429 .side(KrakenOrderSide::Buy)
430 .order_type(KrakenFuturesOrderType::Ioc)
431 .size("500")
432 .limit_price("48000.0")
433 .build()
434 .unwrap();
435
436 let json = serde_json::to_string(¶ms).unwrap();
437 assert!(json.contains("\"orderType\":\"ioc\""));
438 assert!(json.contains("\"limitPrice\":\"48000.0\""));
439 }
440
441 #[rstest]
442 fn test_send_order_params_serialization_with_trigger_signal() {
443 let params = KrakenFuturesSendOrderParamsBuilder::default()
444 .symbol("PI_XBTUSD")
445 .side(KrakenOrderSide::Buy)
446 .order_type(KrakenFuturesOrderType::Stop)
447 .size("500")
448 .stop_price("47000.0")
449 .trigger_signal(KrakenTriggerSignal::Mark)
450 .build()
451 .unwrap();
452
453 let json = serde_json::to_string(¶ms).unwrap();
454 assert!(json.contains("\"triggerSignal\":\"mark\""));
455 assert!(json.contains("\"stopPrice\":\"47000.0\""));
456 }
457
458 #[rstest]
459 fn test_send_order_params_serialization_with_index_trigger_signal() {
460 let params = KrakenFuturesSendOrderParamsBuilder::default()
461 .symbol("PI_XBTUSD")
462 .side(KrakenOrderSide::Buy)
463 .order_type(KrakenFuturesOrderType::Stop)
464 .size("500")
465 .stop_price("47000.0")
466 .trigger_signal(KrakenTriggerSignal::Index)
467 .build()
468 .unwrap();
469
470 let json = serde_json::to_string(¶ms).unwrap();
471 assert!(json.contains("\"triggerSignal\":\"spot\""));
472 assert!(json.contains("\"stopPrice\":\"47000.0\""));
473 }
474
475 #[rstest]
476 fn test_send_order_params_missing_limit_price() {
477 let result = KrakenFuturesSendOrderParamsBuilder::default()
478 .symbol("PI_XBTUSD")
479 .side(KrakenOrderSide::Buy)
480 .order_type(KrakenFuturesOrderType::Limit)
481 .size("1000")
482 .build();
483
484 assert!(result.is_err());
485 assert!(result.unwrap_err().to_string().contains("limit_price"));
486 }
487
488 #[rstest]
489 fn test_cancel_order_params_builder() {
490 let params = KrakenFuturesCancelOrderParamsBuilder::default()
491 .order_id("abc-123")
492 .build()
493 .unwrap();
494
495 assert_eq!(params.order_id, Some("abc-123".to_string()));
496 }
497
498 #[rstest]
499 fn test_edit_order_params_builder() {
500 let params = KrakenFuturesEditOrderParamsBuilder::default()
501 .order_id("abc-123")
502 .size("2000")
503 .limit_price("51000.0")
504 .build()
505 .unwrap();
506
507 assert_eq!(params.order_id, Some("abc-123".to_string()));
508 assert_eq!(params.size, Some("2000".to_string()));
509 assert_eq!(params.limit_price, Some("51000.0".to_string()));
510 }
511
512 #[rstest]
513 fn test_batch_send_item_from_params() {
514 let params = KrakenFuturesSendOrderParamsBuilder::default()
515 .symbol("PI_XBTUSD")
516 .side(KrakenOrderSide::Buy)
517 .order_type(KrakenFuturesOrderType::Limit)
518 .size("1000")
519 .limit_price("50000.0")
520 .cli_ord_id("test-batch-1")
521 .build()
522 .unwrap();
523
524 let item = KrakenFuturesBatchSendItem::from_params(params, "0");
525
526 assert_eq!(item.order, "send");
527 assert_eq!(item.order_tag, "0");
528 assert_eq!(item.symbol, Ustr::from("PI_XBTUSD"));
529 assert_eq!(item.side, KrakenOrderSide::Buy);
530 assert_eq!(item.order_type, KrakenFuturesOrderType::Limit);
531 assert_eq!(item.size, "1000");
532 assert_eq!(item.limit_price, Some("50000.0".to_string()));
533 assert_eq!(item.cli_ord_id, Some("test-batch-1".to_string()));
534 }
535
536 #[rstest]
537 fn test_batch_send_item_serialization() {
538 let params = KrakenFuturesSendOrderParamsBuilder::default()
539 .symbol("PI_XBTUSD")
540 .side(KrakenOrderSide::Sell)
541 .order_type(KrakenFuturesOrderType::Market)
542 .size("500")
543 .reduce_only(true)
544 .build()
545 .unwrap();
546
547 let item = KrakenFuturesBatchSendItem::from_params(params, "1");
548 let value = serde_json::to_value(&item).unwrap();
549
550 assert_eq!(
551 value,
552 serde_json::json!({
553 "order": "send",
554 "order_tag": "1",
555 "symbol": "PI_XBTUSD",
556 "side": "sell",
557 "orderType": "mkt",
558 "size": "500",
559 "reduceOnly": true,
560 }),
561 );
562 }
563
564 #[rstest]
565 fn test_batch_edit_item_from_params() {
566 let params = KrakenFuturesEditOrderParamsBuilder::default()
567 .order_id("order-123")
568 .size("2000")
569 .limit_price("51000.0")
570 .build()
571 .unwrap();
572
573 let item = KrakenFuturesBatchEditItem::from_params(params, "0");
574
575 assert_eq!(item.order, "edit");
576 assert_eq!(item.order_tag, "0");
577 assert_eq!(item.order_id, Some("order-123".to_string()));
578 assert_eq!(item.size, Some("2000".to_string()));
579 assert_eq!(item.limit_price, Some("51000.0".to_string()));
580 }
581
582 #[rstest]
583 fn test_batch_edit_item_serialization() {
584 let params = KrakenFuturesEditOrderParamsBuilder::default()
585 .cli_ord_id("my-order")
586 .limit_price("55000.0")
587 .build()
588 .unwrap();
589
590 let item = KrakenFuturesBatchEditItem::from_params(params, "2");
591 let value = serde_json::to_value(&item).unwrap();
592
593 assert_eq!(
594 value,
595 serde_json::json!({
596 "order": "edit",
597 "order_tag": "2",
598 "cliOrdId": "my-order",
599 "limitPrice": "55000.0",
600 }),
601 );
602 }
603
604 #[rstest]
605 fn test_batch_order_params_to_body() {
606 let params = KrakenFuturesSendOrderParamsBuilder::default()
607 .symbol("PI_XBTUSD")
608 .side(KrakenOrderSide::Buy)
609 .order_type(KrakenFuturesOrderType::Limit)
610 .size("100")
611 .limit_price("50000.0")
612 .build()
613 .unwrap();
614
615 let item = KrakenFuturesBatchSendItem::from_params(params, "0");
616 let batch = KrakenFuturesBatchOrderParams::new(vec![item]);
617 let body = batch.to_body().unwrap();
618
619 assert!(body.starts_with("json="));
620 assert!(body.contains("\"batchOrder\""));
621 assert!(body.contains("\"order\":\"send\""));
622 }
623}