1use indexmap::IndexMap;
19use serde::{
20 Deserialize, Deserializer, Serialize,
21 de::{MapAccess, SeqAccess, Visitor},
22};
23use ustr::Ustr;
24
25use crate::common::enums::{
26 KrakenAssetClass, KrakenOrderSide, KrakenOrderStatus, KrakenOrderType, KrakenPairStatus,
27 KrakenSpotTrigger, KrakenSystemStatus,
28};
29
30#[derive(Debug, Clone, serde::Deserialize)]
32pub struct KrakenResponse<T> {
33 pub error: Vec<String>,
34 pub result: Option<T>,
35}
36
37pub type BalanceResponse = IndexMap<String, String>;
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct TradeBalanceResponse {
49 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)]
59 pub ml: Option<String>, }
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct SpotOpenPosition {
65 pub ordertxid: String,
66 pub pair: String,
67 pub time: f64,
68 #[serde(rename = "type")]
69 pub side: KrakenOrderSide,
70 pub ordertype: KrakenOrderType,
71 pub cost: String,
72 pub fee: String,
73 pub vol: String,
74 pub vol_closed: String,
75 pub margin: String,
76 #[serde(default)]
77 pub posstatus: Option<String>,
78 #[serde(default)]
79 pub value: Option<String>, #[serde(default)]
81 pub net: Option<String>, #[serde(default)]
83 pub terms: Option<String>,
84 #[serde(default)]
85 pub rollovertm: Option<String>,
86 #[serde(default)]
87 pub misc: Option<String>,
88 #[serde(default)]
89 pub oflags: Option<String>,
90}
91
92#[derive(Debug, Clone, Default)]
97pub struct SpotOpenPositionsResponse(IndexMap<String, SpotOpenPosition>);
98
99impl std::ops::Deref for SpotOpenPositionsResponse {
100 type Target = IndexMap<String, SpotOpenPosition>;
101 fn deref(&self) -> &Self::Target {
102 &self.0
103 }
104}
105
106impl<'de> Deserialize<'de> for SpotOpenPositionsResponse {
107 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
108 struct V;
109 impl<'de> Visitor<'de> for V {
110 type Value = SpotOpenPositionsResponse;
111 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
112 write!(f, "a map of open positions or an empty array")
113 }
114 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
115 let mut out = IndexMap::new();
116 while let Some((k, v)) = map.next_entry::<String, SpotOpenPosition>()? {
117 out.insert(k, v);
118 }
119 Ok(SpotOpenPositionsResponse(out))
120 }
121 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
122 if seq.next_element::<serde::de::IgnoredAny>()?.is_some() {
123 return Err(serde::de::Error::custom(
124 "OpenPositions: expected empty array or object map, received non-empty array",
125 ));
126 }
127 Ok(SpotOpenPositionsResponse(IndexMap::new()))
128 }
129 }
130 deserializer.deserialize_any(V)
131 }
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct AssetPairInfo {
138 pub altname: Ustr,
139 pub wsname: Option<Ustr>,
140 pub aclass_base: KrakenAssetClass,
141 pub base: Ustr,
142 pub aclass_quote: KrakenAssetClass,
143 pub quote: Ustr,
144 pub cost_decimals: u8,
145 pub pair_decimals: u8,
146 pub lot_decimals: u8,
147 pub lot_multiplier: i32,
148 #[serde(default)]
149 pub leverage_buy: Vec<i32>,
150 #[serde(default)]
151 pub leverage_sell: Vec<i32>,
152 #[serde(default)]
153 pub fees: Vec<(i32, f64)>,
154 #[serde(default)]
155 pub fees_maker: Vec<(i32, f64)>,
156 pub fee_volume_currency: Option<Ustr>,
157 pub margin_call: Option<i32>,
158 pub margin_stop: Option<i32>,
159 pub ordermin: Option<String>,
160 pub costmin: Option<String>,
161 pub tick_size: Option<String>,
162 pub status: Option<KrakenPairStatus>,
163 #[serde(default)]
164 pub long_position_limit: Option<i64>,
165 #[serde(default)]
166 pub short_position_limit: Option<i64>,
167}
168
169pub type AssetPairsResponse = IndexMap<String, AssetPairInfo>;
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct TickerInfo {
175 #[serde(rename = "a")]
176 pub ask: Vec<String>,
177 #[serde(rename = "b")]
178 pub bid: Vec<String>,
179 #[serde(rename = "c")]
180 pub last: Vec<String>,
181 #[serde(rename = "v")]
182 pub volume: Vec<String>,
183 #[serde(rename = "p")]
184 pub vwap: Vec<String>,
185 #[serde(rename = "t")]
186 pub trades: Vec<i64>,
187 #[serde(rename = "l")]
188 pub low: Vec<String>,
189 #[serde(rename = "h")]
190 pub high: Vec<String>,
191 #[serde(rename = "o")]
192 pub open: String,
193}
194
195pub type TickerResponse = IndexMap<String, TickerInfo>;
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct OhlcData {
201 pub time: i64,
202 pub open: String,
203 pub high: String,
204 pub low: String,
205 pub close: String,
206 pub vwap: String,
207 pub volume: String,
208 pub count: i64,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct OhlcResponse {
213 pub last: i64,
214 #[serde(flatten)]
215 pub data: IndexMap<String, Vec<Vec<serde_json::Value>>>,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct TradeData {
222 pub price: String,
223 pub volume: String,
224 pub time: f64,
225 pub side: KrakenOrderSide,
226 pub order_type: KrakenOrderType,
227 pub misc: String,
228 #[serde(default)]
229 pub trade_id: Option<i64>,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct TradesResponse {
234 pub last: String,
235 #[serde(flatten)]
236 pub data: IndexMap<String, Vec<Vec<serde_json::Value>>>,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct OrderBookLevel {
243 pub price: String,
244 pub volume: String,
245 pub timestamp: i64,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct OrderBookData {
250 pub asks: Vec<Vec<serde_json::Value>>,
251 pub bids: Vec<Vec<serde_json::Value>>,
252}
253
254pub type OrderBookResponse = IndexMap<String, OrderBookData>;
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct SystemStatus {
260 pub status: KrakenSystemStatus,
261 pub timestamp: String,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct ServerTime {
268 pub unixtime: i64,
269 pub rfc1123: String,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct WebSocketToken {
276 pub token: String,
277 pub expires: i32,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct OrderDescription {
285 pub pair: String,
286 #[serde(rename = "type")]
287 pub order_side: KrakenOrderSide,
288 pub ordertype: KrakenOrderType,
289 pub price: String,
290 pub price2: String,
291 pub leverage: String,
292 pub order: String,
293 pub close: Option<String>,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct AddOrderDescription {
299 #[serde(default)]
300 pub order: Option<String>,
301 #[serde(default)]
302 pub close: Option<String>,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct SpotOrder {
307 pub refid: Option<String>,
308 pub userref: Option<i64>,
309 pub status: KrakenOrderStatus,
310 pub opentm: f64,
311 pub starttm: Option<f64>,
312 pub expiretm: Option<f64>,
313 pub descr: OrderDescription,
314 pub vol: String,
315 pub vol_exec: String,
316 pub cost: String,
317 pub fee: String,
318 pub price: String,
319 pub stopprice: Option<String>,
320 pub limitprice: Option<String>,
321 pub trigger: Option<KrakenSpotTrigger>,
322 pub misc: String,
323 pub oflags: String,
324 #[serde(default)]
325 pub trades: Option<Vec<String>>,
326 #[serde(default)]
327 pub closetm: Option<f64>,
328 #[serde(default)]
329 pub reason: Option<String>,
330 #[serde(default)]
331 pub ratecount: Option<i32>,
332 #[serde(default)]
333 pub cl_ord_id: Option<String>,
334 #[serde(default)]
335 pub amended: Option<bool>,
336 #[serde(default)]
338 pub avg_price: Option<String>,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct SpotOpenOrdersResult {
343 pub open: IndexMap<String, SpotOrder>,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct SpotClosedOrdersResult {
348 pub closed: IndexMap<String, SpotOrder>,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct SpotTrade {
353 pub ordertxid: String,
354 pub postxid: String,
355 pub pair: String,
356 pub time: f64,
357 #[serde(rename = "type")]
358 pub trade_type: KrakenOrderSide,
359 pub ordertype: KrakenOrderType,
360 pub price: String,
361 pub cost: String,
362 pub fee: String,
363 pub vol: String,
364 pub margin: String,
365 pub leverage: Option<String>,
366 pub misc: String,
367 #[serde(default)]
368 pub trade_id: Option<i64>,
369 #[serde(default)]
370 pub maker: Option<bool>,
371 #[serde(default)]
372 pub ledgers: Option<Vec<String>>,
373 #[serde(default)]
374 pub posstatus: Option<String>,
375 #[serde(default)]
376 pub cprice: Option<String>,
377 #[serde(default)]
378 pub ccost: Option<String>,
379 #[serde(default)]
380 pub cfee: Option<String>,
381 #[serde(default)]
382 pub cvol: Option<String>,
383 #[serde(default)]
384 pub cmargin: Option<String>,
385 #[serde(default)]
386 pub net: Option<String>,
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct SpotTradesHistoryResult {
391 pub trades: IndexMap<String, SpotTrade>,
392 pub count: i32,
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
398pub struct SpotAddOrderResponse {
399 pub descr: Option<AddOrderDescription>,
400 #[serde(default)]
401 pub txid: Vec<String>,
402 #[serde(default)]
403 pub cl_ord_id: Option<String>,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct SpotBatchOrderResponse {
408 #[serde(default)]
409 pub descr: Option<AddOrderDescription>,
410 #[serde(default)]
411 pub error: Option<String>,
412 #[serde(default)]
413 pub txid: Option<String>,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct SpotAddOrderBatchResponse {
418 #[serde(default)]
419 pub orders: Vec<SpotBatchOrderResponse>,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct SpotCancelOrderResponse {
424 pub count: i32,
425 #[serde(default)]
426 pub pending: Option<bool>,
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct SpotCancelOrderBatchResponse {
431 pub count: i32,
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
435pub struct SpotEditOrderResponse {
436 pub descr: Option<AddOrderDescription>,
437 pub txid: Option<String>,
438 #[serde(default)]
439 pub originaltxid: Option<String>,
440 #[serde(default)]
441 pub volume: Option<String>,
442 #[serde(default)]
443 pub price: Option<String>,
444 #[serde(default)]
445 pub price2: Option<String>,
446 #[serde(default)]
447 pub orders_cancelled: Option<i32>,
448}
449
450#[derive(Debug, Clone, Serialize, Deserialize)]
452pub struct SpotAmendOrderResponse {
453 pub amend_id: String,
455}
456
457#[cfg(test)]
458mod tests {
459 use rstest::rstest;
460
461 use super::*;
462
463 fn load_test_data(filename: &str) -> String {
464 let path = format!("test_data/{filename}");
465 std::fs::read_to_string(&path)
466 .unwrap_or_else(|e| panic!("Failed to load test data from {path}: {e}"))
467 }
468
469 #[rstest]
470 fn test_parse_server_time() {
471 let data = load_test_data("http_server_time.json");
472 let response: KrakenResponse<ServerTime> =
473 serde_json::from_str(&data).expect("Failed to parse server time");
474
475 assert!(response.error.is_empty());
476 let result = response.result.expect("Missing result");
477 assert!(result.unixtime > 0);
478 assert!(!result.rfc1123.is_empty());
479 }
480
481 #[rstest]
482 fn test_parse_system_status() {
483 let data = load_test_data("http_system_status.json");
484 let response: KrakenResponse<SystemStatus> =
485 serde_json::from_str(&data).expect("Failed to parse system status");
486
487 assert!(response.error.is_empty());
488 let result = response.result.expect("Missing result");
489 assert!(!result.timestamp.is_empty());
490 }
491
492 #[rstest]
493 fn test_parse_asset_pairs() {
494 let data = load_test_data("http_asset_pairs.json");
495 let response: KrakenResponse<AssetPairsResponse> =
496 serde_json::from_str(&data).expect("Failed to parse asset pairs");
497
498 assert!(response.error.is_empty());
499 let result = response.result.expect("Missing result");
500 assert!(!result.is_empty());
501
502 let pair = result.get("XBTUSDT").expect("XBTUSDT pair not found");
503 assert_eq!(pair.altname.as_str(), "XBTUSDT");
504 assert_eq!(pair.base.as_str(), "XXBT");
505 assert_eq!(pair.quote.as_str(), "USDT");
506 assert!(pair.wsname.is_some());
507 }
508
509 #[rstest]
510 fn test_parse_ticker() {
511 let data = load_test_data("http_ticker.json");
512 let response: KrakenResponse<TickerResponse> =
513 serde_json::from_str(&data).expect("Failed to parse ticker");
514
515 assert!(response.error.is_empty());
516 let result = response.result.expect("Missing result");
517 assert!(!result.is_empty());
518
519 let ticker = result.get("XBTUSDT").expect("XBTUSDT ticker not found");
520 assert_eq!(ticker.ask.len(), 3);
521 assert_eq!(ticker.bid.len(), 3);
522 assert_eq!(ticker.last.len(), 2);
523 }
524
525 #[rstest]
526 fn test_parse_ohlc() {
527 let data = load_test_data("http_ohlc.json");
528 let response: KrakenResponse<serde_json::Value> =
529 serde_json::from_str(&data).expect("Failed to parse OHLC");
530
531 assert!(response.error.is_empty());
532 assert!(response.result.is_some());
533 }
534
535 #[rstest]
536 fn test_parse_order_book() {
537 let data = load_test_data("http_order_book.json");
538 let response: KrakenResponse<OrderBookResponse> =
539 serde_json::from_str(&data).expect("Failed to parse order book");
540
541 assert!(response.error.is_empty());
542 let result = response.result.expect("Missing result");
543 assert!(!result.is_empty());
544
545 let book = result.get("XBTUSDT").expect("XBTUSDT order book not found");
546 assert!(!book.asks.is_empty());
547 assert!(!book.bids.is_empty());
548 }
549
550 #[rstest]
551 fn test_parse_trades() {
552 let data = load_test_data("http_trades.json");
553 let response: KrakenResponse<TradesResponse> =
554 serde_json::from_str(&data).expect("Failed to parse trades");
555
556 assert!(response.error.is_empty());
557 let result = response.result.expect("Missing result");
558 assert!(!result.data.is_empty());
559 }
560
561 #[rstest]
562 fn test_open_positions_empty_array() {
563 let result: SpotOpenPositionsResponse = serde_json::from_str("[]").unwrap();
564 assert!(result.is_empty());
565 }
566
567 #[rstest]
568 fn test_open_positions_empty_object() {
569 let result: SpotOpenPositionsResponse = serde_json::from_str("{}").unwrap();
570 assert!(result.is_empty());
571 }
572
573 #[rstest]
574 fn test_open_positions_non_empty_array_errors() {
575 let err =
576 serde_json::from_str::<SpotOpenPositionsResponse>(r#"[{"posid": "123"}]"#).unwrap_err();
577 assert!(
578 err.to_string()
579 .contains("OpenPositions: expected empty array or object map"),
580 "unexpected error: {err}"
581 );
582 }
583}