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)]
203#[allow(
204 dead_code,
205 reason = "TradeVolume response retained for account fee-rate follow-up"
206)]
207pub(crate) struct SpotTradeVolumeFee {
208 #[serde(with = "decimal")]
209 pub fee: Decimal,
210}
211
212#[derive(Debug, Clone, Deserialize)]
213#[allow(
214 dead_code,
215 reason = "TradeVolume response retained for account fee-rate follow-up"
216)]
217pub(crate) struct SpotTradeVolumeResponse {
218 pub fees: IndexMap<String, SpotTradeVolumeFee>,
219 #[serde(default)]
220 pub fees_maker: IndexMap<String, SpotTradeVolumeFee>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct TickerInfo {
227 #[serde(rename = "a")]
228 pub ask: Vec<String>,
229 #[serde(rename = "b")]
230 pub bid: Vec<String>,
231 #[serde(rename = "c")]
232 pub last: Vec<String>,
233 #[serde(rename = "v")]
234 pub volume: Vec<String>,
235 #[serde(rename = "p")]
236 pub vwap: Vec<String>,
237 #[serde(rename = "t")]
238 pub trades: Vec<i64>,
239 #[serde(rename = "l")]
240 pub low: Vec<String>,
241 #[serde(rename = "h")]
242 pub high: Vec<String>,
243 #[serde(rename = "o")]
244 pub open: String,
245}
246
247pub type TickerResponse = IndexMap<String, TickerInfo>;
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct OhlcData {
253 pub time: i64,
254 pub open: String,
255 pub high: String,
256 pub low: String,
257 pub close: String,
258 pub vwap: String,
259 pub volume: String,
260 pub count: i64,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct OhlcResponse {
265 pub last: i64,
266 #[serde(flatten)]
267 pub data: IndexMap<String, Vec<Vec<serde_json::Value>>>,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct TradeData {
274 pub price: String,
275 pub volume: String,
276 pub time: f64,
277 pub side: KrakenOrderSide,
278 pub order_type: KrakenOrderType,
279 pub misc: String,
280 #[serde(default)]
281 pub trade_id: Option<i64>,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct TradesResponse {
286 pub last: String,
287 #[serde(flatten)]
288 pub data: IndexMap<String, Vec<Vec<serde_json::Value>>>,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct OrderBookLevel {
295 pub price: String,
296 pub volume: String,
297 pub timestamp: i64,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct OrderBookData {
302 pub asks: Vec<Vec<serde_json::Value>>,
303 pub bids: Vec<Vec<serde_json::Value>>,
304}
305
306pub type OrderBookResponse = IndexMap<String, OrderBookData>;
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct SystemStatus {
312 pub status: KrakenSystemStatus,
313 pub timestamp: String,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct ServerTime {
320 pub unixtime: i64,
321 pub rfc1123: String,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
327pub struct WebSocketToken {
328 pub token: SecretString,
329 pub expires: i32,
330}
331
332impl WebSocketToken {
333 #[must_use]
335 pub fn into_token(mut self) -> SecretString {
336 std::mem::take(&mut self.token)
337 }
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct OrderDescription {
345 pub pair: String,
346 #[serde(rename = "type")]
347 pub order_side: KrakenOrderSide,
348 pub ordertype: KrakenOrderType,
349 pub price: String,
350 pub price2: String,
351 pub leverage: String,
352 pub order: String,
353 pub close: Option<String>,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct AddOrderDescription {
359 #[serde(default)]
360 pub order: Option<String>,
361 #[serde(default)]
362 pub close: Option<String>,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct SpotOrder {
367 pub refid: Option<String>,
368 pub userref: Option<i64>,
369 pub status: KrakenOrderStatus,
370 pub opentm: f64,
371 pub starttm: Option<f64>,
372 pub expiretm: Option<f64>,
373 pub descr: OrderDescription,
374 pub vol: String,
375 pub vol_exec: String,
376 pub cost: String,
377 pub fee: String,
378 pub price: String,
379 pub stopprice: Option<String>,
380 pub limitprice: Option<String>,
381 pub trigger: Option<KrakenSpotTrigger>,
382 pub misc: String,
383 pub oflags: String,
384 #[serde(default)]
385 pub trades: Option<Vec<String>>,
386 #[serde(default)]
387 pub closetm: Option<f64>,
388 #[serde(default)]
389 pub reason: Option<String>,
390 #[serde(default)]
391 pub ratecount: Option<i32>,
392 #[serde(default)]
393 pub cl_ord_id: Option<String>,
394 #[serde(default)]
395 pub amended: Option<bool>,
396 #[serde(default)]
398 pub avg_price: Option<String>,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct SpotOpenOrdersResult {
403 pub open: IndexMap<String, SpotOrder>,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct SpotClosedOrdersResult {
408 pub closed: IndexMap<String, SpotOrder>,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize)]
412pub struct SpotTrade {
413 pub ordertxid: String,
414 pub postxid: String,
415 pub pair: String,
416 pub time: f64,
417 #[serde(rename = "type")]
418 pub trade_type: KrakenOrderSide,
419 pub ordertype: KrakenOrderType,
420 pub price: String,
421 pub cost: String,
422 pub fee: String,
423 pub vol: String,
424 pub margin: String,
425 pub leverage: Option<String>,
426 pub misc: String,
427 #[serde(default)]
428 pub trade_id: Option<i64>,
429 #[serde(default)]
430 pub maker: Option<bool>,
431 #[serde(default)]
432 pub ledgers: Option<Vec<String>>,
433 #[serde(default)]
434 pub posstatus: Option<String>,
435 #[serde(default)]
436 pub cprice: Option<String>,
437 #[serde(default)]
438 pub ccost: Option<String>,
439 #[serde(default)]
440 pub cfee: Option<String>,
441 #[serde(default)]
442 pub cvol: Option<String>,
443 #[serde(default)]
444 pub cmargin: Option<String>,
445 #[serde(default)]
446 pub net: Option<String>,
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct SpotTradesHistoryResult {
451 pub trades: IndexMap<String, SpotTrade>,
452 pub count: i32,
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct SpotAddOrderResponse {
459 pub descr: Option<AddOrderDescription>,
460 #[serde(default)]
461 pub txid: Vec<String>,
462 #[serde(default)]
463 pub cl_ord_id: Option<String>,
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct SpotBatchOrderResponse {
468 #[serde(default)]
469 pub descr: Option<AddOrderDescription>,
470 #[serde(default)]
471 pub error: Option<String>,
472 #[serde(default)]
473 pub txid: Option<String>,
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize)]
477pub struct SpotAddOrderBatchResponse {
478 #[serde(default)]
479 pub orders: Vec<SpotBatchOrderResponse>,
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize)]
483pub struct SpotCancelOrderResponse {
484 pub count: i32,
485 #[serde(default)]
486 pub pending: Option<bool>,
487}
488
489#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct SpotCancelOrderBatchResponse {
491 pub count: i32,
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize)]
495pub struct SpotEditOrderResponse {
496 pub descr: Option<AddOrderDescription>,
497 pub txid: Option<String>,
498 #[serde(default)]
499 pub originaltxid: Option<String>,
500 #[serde(default)]
501 pub volume: Option<String>,
502 #[serde(default)]
503 pub price: Option<String>,
504 #[serde(default)]
505 pub price2: Option<String>,
506 #[serde(default)]
507 pub orders_cancelled: Option<i32>,
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct SpotAmendOrderResponse {
513 pub amend_id: String,
515}
516
517#[cfg(test)]
518mod tests {
519 use rstest::rstest;
520
521 use super::*;
522
523 fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
524
525 fn load_test_data(filename: &str) -> String {
526 let path = format!("test_data/{filename}");
527 std::fs::read_to_string(&path)
528 .unwrap_or_else(|e| panic!("Failed to load test data from {path}: {e}"))
529 }
530
531 #[rstest]
532 fn test_websocket_token_zeroizes_on_drop() {
533 assert_zeroize_on_drop::<WebSocketToken>();
534
535 let token = WebSocketToken {
536 token: SecretString::from("websocket-token-value"),
537 expires: 900,
538 };
539 let formatted = format!("{token:?}");
540
541 assert_eq!(
542 formatted,
543 "WebSocketToken { token: <redacted>, expires: 900 }"
544 );
545 assert!(!formatted.contains(token.token.expose_secret()));
546 }
547
548 #[rstest]
549 fn test_parse_server_time() {
550 let data = load_test_data("http_server_time.json");
551 let response: KrakenResponse<ServerTime> =
552 serde_json::from_str(&data).expect("Failed to parse server time");
553
554 assert!(response.error.is_empty());
555 let result = response.result.expect("Missing result");
556 assert!(result.unixtime > 0);
557 assert!(!result.rfc1123.is_empty());
558 }
559
560 #[rstest]
561 fn test_parse_system_status() {
562 let data = load_test_data("http_system_status.json");
563 let response: KrakenResponse<SystemStatus> =
564 serde_json::from_str(&data).expect("Failed to parse system status");
565
566 assert!(response.error.is_empty());
567 let result = response.result.expect("Missing result");
568 assert!(!result.timestamp.is_empty());
569 }
570
571 #[rstest]
572 fn test_parse_asset_pairs() {
573 let data = load_test_data("http_asset_pairs.json");
574 let response: KrakenResponse<AssetPairsResponse> =
575 serde_json::from_str(&data).expect("Failed to parse asset pairs");
576
577 assert!(response.error.is_empty());
578 let result = response.result.expect("Missing result");
579 assert!(!result.is_empty());
580
581 let pair = result.get("XBTUSDT").expect("XBTUSDT pair not found");
582 assert_eq!(pair.altname, "XBTUSDT");
583 assert_eq!(pair.base, "XXBT");
584 assert_eq!(pair.quote, "USDT");
585 assert!(pair.wsname.is_some());
586 }
587
588 #[rstest]
589 fn test_parse_ticker() {
590 let data = load_test_data("http_ticker.json");
591 let response: KrakenResponse<TickerResponse> =
592 serde_json::from_str(&data).expect("Failed to parse ticker");
593
594 assert!(response.error.is_empty());
595 let result = response.result.expect("Missing result");
596 assert!(!result.is_empty());
597
598 let ticker = result.get("XBTUSDT").expect("XBTUSDT ticker not found");
599 assert_eq!(ticker.ask.len(), 3);
600 assert_eq!(ticker.bid.len(), 3);
601 assert_eq!(ticker.last.len(), 2);
602 }
603
604 #[rstest]
605 fn test_parse_ohlc() {
606 let data = load_test_data("http_ohlc.json");
607 let response: KrakenResponse<serde_json::Value> =
608 serde_json::from_str(&data).expect("Failed to parse OHLC");
609
610 assert!(response.error.is_empty());
611 assert!(response.result.is_some());
612 }
613
614 #[rstest]
615 fn test_parse_order_book() {
616 let data = load_test_data("http_order_book.json");
617 let response: KrakenResponse<OrderBookResponse> =
618 serde_json::from_str(&data).expect("Failed to parse order book");
619
620 assert!(response.error.is_empty());
621 let result = response.result.expect("Missing result");
622 assert!(!result.is_empty());
623
624 let book = result.get("XBTUSDT").expect("XBTUSDT order book not found");
625 assert!(!book.asks.is_empty());
626 assert!(!book.bids.is_empty());
627 }
628
629 #[rstest]
630 fn test_parse_trades() {
631 let data = load_test_data("http_trades.json");
632 let response: KrakenResponse<TradesResponse> =
633 serde_json::from_str(&data).expect("Failed to parse trades");
634
635 assert!(response.error.is_empty());
636 let result = response.result.expect("Missing result");
637 assert!(!result.data.is_empty());
638 }
639
640 #[rstest]
641 fn test_open_positions_empty_array() {
642 let result: SpotOpenPositionsResponse = serde_json::from_str("[]").unwrap();
643 assert!(result.is_empty());
644 }
645
646 #[rstest]
647 fn test_open_positions_empty_object() {
648 let result: SpotOpenPositionsResponse = serde_json::from_str("{}").unwrap();
649 assert!(result.is_empty());
650 }
651
652 #[rstest]
653 fn test_open_positions_non_empty_array_errors() {
654 let err =
655 serde_json::from_str::<SpotOpenPositionsResponse>(r#"[{"posid": "123"}]"#).unwrap_err();
656 assert!(
657 err.to_string()
658 .contains("OpenPositions: expected empty array or object map"),
659 "unexpected error: {err}"
660 );
661 }
662}