1use std::fmt::Debug;
19
20use indexmap::IndexMap;
21use nautilus_core::string::secret::SecretString;
22use rust_decimal::Decimal;
23use serde::{
24 Deserialize, Deserializer, Serialize,
25 de::{MapAccess, SeqAccess, Visitor},
26};
27use ustr::Ustr;
28use zeroize::{Zeroize, ZeroizeOnDrop};
29
30use crate::common::{
31 enums::{
32 KrakenAssetClass, KrakenOrderSide, KrakenOrderStatus, KrakenOrderType, KrakenPairStatus,
33 KrakenSpotTrigger, KrakenSystemStatus,
34 },
35 serialization::{decimal, decimal_pairs},
36};
37
38#[derive(Debug, Clone, serde::Deserialize)]
40pub struct KrakenResponse<T> {
41 pub error: Vec<String>,
42 pub result: Option<T>,
43}
44
45pub type BalanceResponse = IndexMap<String, String>;
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct BalanceExEntry {
58 pub balance: String,
60 pub hold_trade: String,
62 #[serde(default)]
64 pub credit: Option<String>,
65 #[serde(default)]
67 pub credit_used: Option<String>,
68}
69
70pub type BalanceExResponse = IndexMap<String, BalanceExEntry>;
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct TradeBalanceResponse {
80 pub eb: String, pub tb: String, pub m: String, pub uv: String, pub n: String, pub c: String, pub v: String, pub e: String, pub mf: String, #[serde(default)]
90 pub ml: Option<String>, }
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct SpotOpenPosition {
96 pub ordertxid: String,
97 pub pair: String,
98 pub time: f64,
99 #[serde(rename = "type")]
100 pub side: KrakenOrderSide,
101 pub ordertype: KrakenOrderType,
102 pub cost: String,
103 pub fee: String,
104 pub vol: String,
105 pub vol_closed: String,
106 pub margin: String,
107 #[serde(default)]
108 pub posstatus: Option<String>,
109 #[serde(default)]
110 pub value: Option<String>, #[serde(default)]
112 pub net: Option<String>, #[serde(default)]
114 pub terms: Option<String>,
115 #[serde(default)]
116 pub rollovertm: Option<String>,
117 #[serde(default)]
118 pub misc: Option<String>,
119 #[serde(default)]
120 pub oflags: Option<String>,
121}
122
123#[derive(Debug, Clone, Default)]
128pub struct SpotOpenPositionsResponse(IndexMap<String, SpotOpenPosition>);
129
130impl std::ops::Deref for SpotOpenPositionsResponse {
131 type Target = IndexMap<String, SpotOpenPosition>;
132 fn deref(&self) -> &Self::Target {
133 &self.0
134 }
135}
136
137impl<'de> Deserialize<'de> for SpotOpenPositionsResponse {
138 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
139 struct V;
140 impl<'de> Visitor<'de> for V {
141 type Value = SpotOpenPositionsResponse;
142 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
143 write!(f, "a map of open positions or an empty array")
144 }
145 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
146 let mut out = IndexMap::new();
147 while let Some((k, v)) = map.next_entry::<String, SpotOpenPosition>()? {
148 out.insert(k, v);
149 }
150 Ok(SpotOpenPositionsResponse(out))
151 }
152 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
153 if seq.next_element::<serde::de::IgnoredAny>()?.is_some() {
154 return Err(serde::de::Error::custom(
155 "OpenPositions: expected empty array or object map, received non-empty array",
156 ));
157 }
158 Ok(SpotOpenPositionsResponse(IndexMap::new()))
159 }
160 }
161 deserializer.deserialize_any(V)
162 }
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct AssetPairInfo {
169 pub altname: Ustr,
170 pub wsname: Option<Ustr>,
171 pub aclass_base: KrakenAssetClass,
172 pub base: Ustr,
173 pub aclass_quote: KrakenAssetClass,
174 pub quote: Ustr,
175 pub cost_decimals: u8,
176 pub pair_decimals: u8,
177 pub lot_decimals: u8,
178 pub lot_multiplier: i32,
179 #[serde(default)]
180 pub leverage_buy: Vec<i32>,
181 #[serde(default)]
182 pub leverage_sell: Vec<i32>,
183 #[serde(default, with = "decimal_pairs")]
184 pub fees: Vec<(i32, Decimal)>,
185 #[serde(default, with = "decimal_pairs")]
186 pub fees_maker: Vec<(i32, Decimal)>,
187 pub fee_volume_currency: Option<Ustr>,
188 pub margin_call: Option<i32>,
189 pub margin_stop: Option<i32>,
190 pub ordermin: Option<String>,
191 pub costmin: Option<String>,
192 pub tick_size: Option<String>,
193 pub status: Option<KrakenPairStatus>,
194 #[serde(default)]
195 pub long_position_limit: Option<i64>,
196 #[serde(default)]
197 pub short_position_limit: Option<i64>,
198}
199
200pub type AssetPairsResponse = IndexMap<String, AssetPairInfo>;
201
202#[derive(Debug, Clone, Deserialize)]
203pub(crate) struct SpotTradeVolumeFee {
204 #[serde(with = "decimal")]
205 pub fee: Decimal,
206}
207
208#[derive(Debug, Clone, Deserialize)]
209pub(crate) struct SpotTradeVolumeResponse {
210 pub fees: IndexMap<String, SpotTradeVolumeFee>,
211 #[serde(default)]
212 pub fees_maker: IndexMap<String, SpotTradeVolumeFee>,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct TickerInfo {
219 #[serde(rename = "a")]
220 pub ask: Vec<String>,
221 #[serde(rename = "b")]
222 pub bid: Vec<String>,
223 #[serde(rename = "c")]
224 pub last: Vec<String>,
225 #[serde(rename = "v")]
226 pub volume: Vec<String>,
227 #[serde(rename = "p")]
228 pub vwap: Vec<String>,
229 #[serde(rename = "t")]
230 pub trades: Vec<i64>,
231 #[serde(rename = "l")]
232 pub low: Vec<String>,
233 #[serde(rename = "h")]
234 pub high: Vec<String>,
235 #[serde(rename = "o")]
236 pub open: String,
237}
238
239pub type TickerResponse = IndexMap<String, TickerInfo>;
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct OhlcData {
245 pub time: i64,
246 pub open: String,
247 pub high: String,
248 pub low: String,
249 pub close: String,
250 pub vwap: String,
251 pub volume: String,
252 pub count: i64,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct OhlcResponse {
257 pub last: i64,
258 #[serde(flatten)]
259 pub data: IndexMap<String, Vec<Vec<serde_json::Value>>>,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct TradeData {
266 pub price: String,
267 pub volume: String,
268 pub time: f64,
269 pub side: KrakenOrderSide,
270 pub order_type: KrakenOrderType,
271 pub misc: String,
272 #[serde(default)]
273 pub trade_id: Option<i64>,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct TradesResponse {
278 pub last: String,
279 #[serde(flatten)]
280 pub data: IndexMap<String, Vec<Vec<serde_json::Value>>>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct OrderBookLevel {
287 pub price: String,
288 pub volume: String,
289 pub timestamp: i64,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct OrderBookData {
294 pub asks: Vec<Vec<serde_json::Value>>,
295 pub bids: Vec<Vec<serde_json::Value>>,
296}
297
298pub type OrderBookResponse = IndexMap<String, OrderBookData>;
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct SystemStatus {
304 pub status: KrakenSystemStatus,
305 pub timestamp: String,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct ServerTime {
312 pub unixtime: i64,
313 pub rfc1123: String,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
319pub struct WebSocketToken {
320 pub token: SecretString,
321 pub expires: i32,
322}
323
324impl WebSocketToken {
325 #[must_use]
327 pub fn into_token(mut self) -> SecretString {
328 std::mem::take(&mut self.token)
329 }
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct OrderDescription {
337 pub pair: String,
338 #[serde(rename = "type")]
339 pub order_side: KrakenOrderSide,
340 pub ordertype: KrakenOrderType,
341 pub price: String,
342 pub price2: String,
343 pub leverage: String,
344 pub order: String,
345 pub close: Option<String>,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct AddOrderDescription {
351 #[serde(default)]
352 pub order: Option<String>,
353 #[serde(default)]
354 pub close: Option<String>,
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct SpotOrder {
359 pub refid: Option<String>,
360 pub userref: Option<i64>,
361 pub status: KrakenOrderStatus,
362 pub opentm: f64,
363 pub starttm: Option<f64>,
364 pub expiretm: Option<f64>,
365 pub descr: OrderDescription,
366 pub vol: String,
367 pub vol_exec: String,
368 pub cost: String,
369 pub fee: String,
370 pub price: String,
371 pub stopprice: Option<String>,
372 pub limitprice: Option<String>,
373 pub trigger: Option<KrakenSpotTrigger>,
374 pub misc: String,
375 pub oflags: String,
376 #[serde(default)]
377 pub trades: Option<Vec<String>>,
378 #[serde(default)]
379 pub closetm: Option<f64>,
380 #[serde(default)]
381 pub reason: Option<String>,
382 #[serde(default)]
383 pub ratecount: Option<i32>,
384 #[serde(default)]
385 pub cl_ord_id: Option<String>,
386 #[serde(default)]
387 pub amended: Option<bool>,
388 #[serde(default)]
390 pub avg_price: Option<String>,
391}
392
393#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct SpotOpenOrdersResult {
395 pub open: IndexMap<String, SpotOrder>,
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct SpotClosedOrdersResult {
400 pub closed: IndexMap<String, SpotOrder>,
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct SpotTrade {
405 pub ordertxid: String,
406 pub postxid: String,
407 pub pair: String,
408 pub time: f64,
409 #[serde(rename = "type")]
410 pub trade_type: KrakenOrderSide,
411 pub ordertype: KrakenOrderType,
412 pub price: String,
413 pub cost: String,
414 pub fee: String,
415 pub vol: String,
416 pub margin: String,
417 pub leverage: Option<String>,
418 pub misc: String,
419 #[serde(default)]
420 pub trade_id: Option<i64>,
421 #[serde(default)]
422 pub maker: Option<bool>,
423 #[serde(default)]
424 pub ledgers: Option<Vec<String>>,
425 #[serde(default)]
426 pub posstatus: Option<String>,
427 #[serde(default)]
428 pub cprice: Option<String>,
429 #[serde(default)]
430 pub ccost: Option<String>,
431 #[serde(default)]
432 pub cfee: Option<String>,
433 #[serde(default)]
434 pub cvol: Option<String>,
435 #[serde(default)]
436 pub cmargin: Option<String>,
437 #[serde(default)]
438 pub net: Option<String>,
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize)]
442pub struct SpotTradesHistoryResult {
443 pub trades: IndexMap<String, SpotTrade>,
444 pub count: i32,
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct SpotAddOrderResponse {
451 pub descr: Option<AddOrderDescription>,
452 #[serde(default)]
453 pub txid: Vec<String>,
454 #[serde(default)]
455 pub cl_ord_id: Option<String>,
456}
457
458#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct SpotBatchOrderResponse {
460 #[serde(default)]
461 pub descr: Option<AddOrderDescription>,
462 #[serde(default)]
463 pub error: Option<String>,
464 #[serde(default)]
465 pub txid: Option<String>,
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize)]
469pub struct SpotAddOrderBatchResponse {
470 #[serde(default)]
471 pub orders: Vec<SpotBatchOrderResponse>,
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize)]
475pub struct SpotCancelOrderResponse {
476 pub count: i32,
477 #[serde(default)]
478 pub pending: Option<bool>,
479}
480
481#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct SpotCancelOrderBatchResponse {
483 pub count: i32,
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
487pub struct SpotEditOrderResponse {
488 pub descr: Option<AddOrderDescription>,
489 pub txid: Option<String>,
490 #[serde(default)]
491 pub originaltxid: Option<String>,
492 #[serde(default)]
493 pub volume: Option<String>,
494 #[serde(default)]
495 pub price: Option<String>,
496 #[serde(default)]
497 pub price2: Option<String>,
498 #[serde(default)]
499 pub orders_cancelled: Option<i32>,
500}
501
502#[derive(Debug, Clone, Serialize, Deserialize)]
504pub struct SpotAmendOrderResponse {
505 pub amend_id: String,
507}
508
509#[cfg(test)]
510mod tests {
511 use rstest::rstest;
512
513 use super::*;
514
515 fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
516
517 fn load_test_data(filename: &str) -> String {
518 let path = format!("test_data/{filename}");
519 std::fs::read_to_string(&path)
520 .unwrap_or_else(|e| panic!("Failed to load test data from {path}: {e}"))
521 }
522
523 #[rstest]
524 fn test_websocket_token_zeroizes_on_drop() {
525 assert_zeroize_on_drop::<WebSocketToken>();
526
527 let token = WebSocketToken {
528 token: SecretString::from("websocket-token-value"),
529 expires: 900,
530 };
531 let formatted = format!("{token:?}");
532
533 assert_eq!(
534 formatted,
535 "WebSocketToken { token: <redacted>, expires: 900 }"
536 );
537 assert!(!formatted.contains(token.token.expose_secret()));
538 }
539
540 #[rstest]
541 fn test_parse_server_time() {
542 let data = load_test_data("http_server_time.json");
543 let response: KrakenResponse<ServerTime> =
544 serde_json::from_str(&data).expect("Failed to parse server time");
545
546 assert!(response.error.is_empty());
547 let result = response.result.expect("Missing result");
548 assert!(result.unixtime > 0);
549 assert!(!result.rfc1123.is_empty());
550 }
551
552 #[rstest]
553 fn test_parse_system_status() {
554 let data = load_test_data("http_system_status.json");
555 let response: KrakenResponse<SystemStatus> =
556 serde_json::from_str(&data).expect("Failed to parse system status");
557
558 assert!(response.error.is_empty());
559 let result = response.result.expect("Missing result");
560 assert!(!result.timestamp.is_empty());
561 }
562
563 #[rstest]
564 fn test_parse_asset_pairs() {
565 let data = load_test_data("http_asset_pairs.json");
566 let response: KrakenResponse<AssetPairsResponse> =
567 serde_json::from_str(&data).expect("Failed to parse asset pairs");
568
569 assert!(response.error.is_empty());
570 let result = response.result.expect("Missing result");
571 assert!(!result.is_empty());
572
573 let pair = result.get("XBTUSDT").expect("XBTUSDT pair not found");
574 assert_eq!(pair.altname, "XBTUSDT");
575 assert_eq!(pair.base, "XXBT");
576 assert_eq!(pair.quote, "USDT");
577 assert!(pair.wsname.is_some());
578 }
579
580 #[rstest]
581 fn test_parse_ticker() {
582 let data = load_test_data("http_ticker.json");
583 let response: KrakenResponse<TickerResponse> =
584 serde_json::from_str(&data).expect("Failed to parse ticker");
585
586 assert!(response.error.is_empty());
587 let result = response.result.expect("Missing result");
588 assert!(!result.is_empty());
589
590 let ticker = result.get("XBTUSDT").expect("XBTUSDT ticker not found");
591 assert_eq!(ticker.ask.len(), 3);
592 assert_eq!(ticker.bid.len(), 3);
593 assert_eq!(ticker.last.len(), 2);
594 }
595
596 #[rstest]
597 fn test_parse_ohlc() {
598 let data = load_test_data("http_ohlc.json");
599 let response: KrakenResponse<serde_json::Value> =
600 serde_json::from_str(&data).expect("Failed to parse OHLC");
601
602 assert!(response.error.is_empty());
603 assert!(response.result.is_some());
604 }
605
606 #[rstest]
607 fn test_parse_order_book() {
608 let data = load_test_data("http_order_book.json");
609 let response: KrakenResponse<OrderBookResponse> =
610 serde_json::from_str(&data).expect("Failed to parse order book");
611
612 assert!(response.error.is_empty());
613 let result = response.result.expect("Missing result");
614 assert!(!result.is_empty());
615
616 let book = result.get("XBTUSDT").expect("XBTUSDT order book not found");
617 assert!(!book.asks.is_empty());
618 assert!(!book.bids.is_empty());
619 }
620
621 #[rstest]
622 fn test_parse_trades() {
623 let data = load_test_data("http_trades.json");
624 let response: KrakenResponse<TradesResponse> =
625 serde_json::from_str(&data).expect("Failed to parse trades");
626
627 assert!(response.error.is_empty());
628 let result = response.result.expect("Missing result");
629 assert!(!result.data.is_empty());
630 }
631
632 #[rstest]
633 fn test_open_positions_empty_array() {
634 let result: SpotOpenPositionsResponse = serde_json::from_str("[]").unwrap();
635 assert!(result.is_empty());
636 }
637
638 #[rstest]
639 fn test_open_positions_empty_object() {
640 let result: SpotOpenPositionsResponse = serde_json::from_str("{}").unwrap();
641 assert!(result.is_empty());
642 }
643
644 #[rstest]
645 fn test_open_positions_non_empty_array_errors() {
646 let err =
647 serde_json::from_str::<SpotOpenPositionsResponse>(r#"[{"posid": "123"}]"#).unwrap_err();
648 assert!(
649 err.to_string()
650 .contains("OpenPositions: expected empty array or object map"),
651 "unexpected error: {err}"
652 );
653 }
654}