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