1use indexmap::IndexMap;
19use serde::{Deserialize, Serialize};
20use ustr::Ustr;
21
22use crate::common::enums::{
23 KrakenAssetClass, KrakenOrderSide, KrakenOrderStatus, KrakenOrderType, KrakenPairStatus,
24 KrakenSpotTrigger, KrakenSystemStatus,
25};
26
27#[derive(Debug, Clone, serde::Deserialize)]
29pub struct KrakenResponse<T> {
30 pub error: Vec<String>,
31 pub result: Option<T>,
32}
33
34pub type BalanceResponse = IndexMap<String, String>;
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct AssetPairInfo {
44 pub altname: Ustr,
45 pub wsname: Option<Ustr>,
46 pub aclass_base: KrakenAssetClass,
47 pub base: Ustr,
48 pub aclass_quote: KrakenAssetClass,
49 pub quote: Ustr,
50 pub cost_decimals: u8,
51 pub pair_decimals: u8,
52 pub lot_decimals: u8,
53 pub lot_multiplier: i32,
54 #[serde(default)]
55 pub leverage_buy: Vec<i32>,
56 #[serde(default)]
57 pub leverage_sell: Vec<i32>,
58 #[serde(default)]
59 pub fees: Vec<(i32, f64)>,
60 #[serde(default)]
61 pub fees_maker: Vec<(i32, f64)>,
62 pub fee_volume_currency: Option<Ustr>,
63 pub margin_call: Option<i32>,
64 pub margin_stop: Option<i32>,
65 pub ordermin: Option<String>,
66 pub costmin: Option<String>,
67 pub tick_size: Option<String>,
68 pub status: Option<KrakenPairStatus>,
69 #[serde(default)]
70 pub long_position_limit: Option<i64>,
71 #[serde(default)]
72 pub short_position_limit: Option<i64>,
73}
74
75pub type AssetPairsResponse = IndexMap<String, AssetPairInfo>;
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct TickerInfo {
81 #[serde(rename = "a")]
82 pub ask: Vec<String>,
83 #[serde(rename = "b")]
84 pub bid: Vec<String>,
85 #[serde(rename = "c")]
86 pub last: Vec<String>,
87 #[serde(rename = "v")]
88 pub volume: Vec<String>,
89 #[serde(rename = "p")]
90 pub vwap: Vec<String>,
91 #[serde(rename = "t")]
92 pub trades: Vec<i64>,
93 #[serde(rename = "l")]
94 pub low: Vec<String>,
95 #[serde(rename = "h")]
96 pub high: Vec<String>,
97 #[serde(rename = "o")]
98 pub open: String,
99}
100
101pub type TickerResponse = IndexMap<String, TickerInfo>;
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct OhlcData {
107 pub time: i64,
108 pub open: String,
109 pub high: String,
110 pub low: String,
111 pub close: String,
112 pub vwap: String,
113 pub volume: String,
114 pub count: i64,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct OhlcResponse {
119 pub last: i64,
120 #[serde(flatten)]
121 pub data: IndexMap<String, Vec<Vec<serde_json::Value>>>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct TradeData {
128 pub price: String,
129 pub volume: String,
130 pub time: f64,
131 pub side: KrakenOrderSide,
132 pub order_type: KrakenOrderType,
133 pub misc: String,
134 #[serde(default)]
135 pub trade_id: Option<i64>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct TradesResponse {
140 pub last: String,
141 #[serde(flatten)]
142 pub data: IndexMap<String, Vec<Vec<serde_json::Value>>>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct OrderBookLevel {
149 pub price: String,
150 pub volume: String,
151 pub timestamp: i64,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct OrderBookData {
156 pub asks: Vec<Vec<serde_json::Value>>,
157 pub bids: Vec<Vec<serde_json::Value>>,
158}
159
160pub type OrderBookResponse = IndexMap<String, OrderBookData>;
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct SystemStatus {
166 pub status: KrakenSystemStatus,
167 pub timestamp: String,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct ServerTime {
174 pub unixtime: i64,
175 pub rfc1123: String,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct WebSocketToken {
182 pub token: String,
183 pub expires: i32,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct OrderDescription {
191 pub pair: String,
192 #[serde(rename = "type")]
193 pub order_side: KrakenOrderSide,
194 pub ordertype: KrakenOrderType,
195 pub price: String,
196 pub price2: String,
197 pub leverage: String,
198 pub order: String,
199 pub close: Option<String>,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct AddOrderDescription {
205 #[serde(default)]
206 pub order: Option<String>,
207 #[serde(default)]
208 pub close: Option<String>,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct SpotOrder {
213 pub refid: Option<String>,
214 pub userref: Option<i64>,
215 pub status: KrakenOrderStatus,
216 pub opentm: f64,
217 pub starttm: Option<f64>,
218 pub expiretm: Option<f64>,
219 pub descr: OrderDescription,
220 pub vol: String,
221 pub vol_exec: String,
222 pub cost: String,
223 pub fee: String,
224 pub price: String,
225 pub stopprice: Option<String>,
226 pub limitprice: Option<String>,
227 pub trigger: Option<KrakenSpotTrigger>,
228 pub misc: String,
229 pub oflags: String,
230 #[serde(default)]
231 pub trades: Option<Vec<String>>,
232 #[serde(default)]
233 pub closetm: Option<f64>,
234 #[serde(default)]
235 pub reason: Option<String>,
236 #[serde(default)]
237 pub ratecount: Option<i32>,
238 #[serde(default)]
239 pub cl_ord_id: Option<String>,
240 #[serde(default)]
241 pub amended: Option<bool>,
242 #[serde(default)]
244 pub avg_price: Option<String>,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct SpotOpenOrdersResult {
249 pub open: IndexMap<String, SpotOrder>,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct SpotClosedOrdersResult {
254 pub closed: IndexMap<String, SpotOrder>,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct SpotTrade {
259 pub ordertxid: String,
260 pub postxid: String,
261 pub pair: String,
262 pub time: f64,
263 #[serde(rename = "type")]
264 pub trade_type: KrakenOrderSide,
265 pub ordertype: KrakenOrderType,
266 pub price: String,
267 pub cost: String,
268 pub fee: String,
269 pub vol: String,
270 pub margin: String,
271 pub leverage: Option<String>,
272 pub misc: String,
273 #[serde(default)]
274 pub trade_id: Option<i64>,
275 #[serde(default)]
276 pub maker: Option<bool>,
277 #[serde(default)]
278 pub ledgers: Option<Vec<String>>,
279 #[serde(default)]
280 pub posstatus: Option<String>,
281 #[serde(default)]
282 pub cprice: Option<String>,
283 #[serde(default)]
284 pub ccost: Option<String>,
285 #[serde(default)]
286 pub cfee: Option<String>,
287 #[serde(default)]
288 pub cvol: Option<String>,
289 #[serde(default)]
290 pub cmargin: Option<String>,
291 #[serde(default)]
292 pub net: Option<String>,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct SpotTradesHistoryResult {
297 pub trades: IndexMap<String, SpotTrade>,
298 pub count: i32,
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct SpotAddOrderResponse {
305 pub descr: Option<AddOrderDescription>,
306 #[serde(default)]
307 pub txid: Vec<String>,
308 #[serde(default)]
309 pub cl_ord_id: Option<String>,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct SpotBatchOrderResponse {
314 #[serde(default)]
315 pub descr: Option<AddOrderDescription>,
316 #[serde(default)]
317 pub error: Option<String>,
318 #[serde(default)]
319 pub txid: Option<String>,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct SpotAddOrderBatchResponse {
324 #[serde(default)]
325 pub orders: Vec<SpotBatchOrderResponse>,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct SpotCancelOrderResponse {
330 pub count: i32,
331 #[serde(default)]
332 pub pending: Option<bool>,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct SpotCancelOrderBatchResponse {
337 pub count: i32,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct SpotEditOrderResponse {
342 pub descr: Option<AddOrderDescription>,
343 pub txid: Option<String>,
344 #[serde(default)]
345 pub originaltxid: Option<String>,
346 #[serde(default)]
347 pub volume: Option<String>,
348 #[serde(default)]
349 pub price: Option<String>,
350 #[serde(default)]
351 pub price2: Option<String>,
352 #[serde(default)]
353 pub orders_cancelled: Option<i32>,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct SpotAmendOrderResponse {
359 pub amend_id: String,
361}
362
363#[cfg(test)]
364mod tests {
365 use rstest::rstest;
366
367 use super::*;
368
369 fn load_test_data(filename: &str) -> String {
370 let path = format!("test_data/{filename}");
371 std::fs::read_to_string(&path)
372 .unwrap_or_else(|e| panic!("Failed to load test data from {path}: {e}"))
373 }
374
375 #[rstest]
376 fn test_parse_server_time() {
377 let data = load_test_data("http_server_time.json");
378 let response: KrakenResponse<ServerTime> =
379 serde_json::from_str(&data).expect("Failed to parse server time");
380
381 assert!(response.error.is_empty());
382 let result = response.result.expect("Missing result");
383 assert!(result.unixtime > 0);
384 assert!(!result.rfc1123.is_empty());
385 }
386
387 #[rstest]
388 fn test_parse_system_status() {
389 let data = load_test_data("http_system_status.json");
390 let response: KrakenResponse<SystemStatus> =
391 serde_json::from_str(&data).expect("Failed to parse system status");
392
393 assert!(response.error.is_empty());
394 let result = response.result.expect("Missing result");
395 assert!(!result.timestamp.is_empty());
396 }
397
398 #[rstest]
399 fn test_parse_asset_pairs() {
400 let data = load_test_data("http_asset_pairs.json");
401 let response: KrakenResponse<AssetPairsResponse> =
402 serde_json::from_str(&data).expect("Failed to parse asset pairs");
403
404 assert!(response.error.is_empty());
405 let result = response.result.expect("Missing result");
406 assert!(!result.is_empty());
407
408 let pair = result.get("XBTUSDT").expect("XBTUSDT pair not found");
409 assert_eq!(pair.altname.as_str(), "XBTUSDT");
410 assert_eq!(pair.base.as_str(), "XXBT");
411 assert_eq!(pair.quote.as_str(), "USDT");
412 assert!(pair.wsname.is_some());
413 }
414
415 #[rstest]
416 fn test_parse_ticker() {
417 let data = load_test_data("http_ticker.json");
418 let response: KrakenResponse<TickerResponse> =
419 serde_json::from_str(&data).expect("Failed to parse ticker");
420
421 assert!(response.error.is_empty());
422 let result = response.result.expect("Missing result");
423 assert!(!result.is_empty());
424
425 let ticker = result.get("XBTUSDT").expect("XBTUSDT ticker not found");
426 assert_eq!(ticker.ask.len(), 3);
427 assert_eq!(ticker.bid.len(), 3);
428 assert_eq!(ticker.last.len(), 2);
429 }
430
431 #[rstest]
432 fn test_parse_ohlc() {
433 let data = load_test_data("http_ohlc.json");
434 let response: KrakenResponse<serde_json::Value> =
435 serde_json::from_str(&data).expect("Failed to parse OHLC");
436
437 assert!(response.error.is_empty());
438 assert!(response.result.is_some());
439 }
440
441 #[rstest]
442 fn test_parse_order_book() {
443 let data = load_test_data("http_order_book.json");
444 let response: KrakenResponse<OrderBookResponse> =
445 serde_json::from_str(&data).expect("Failed to parse order book");
446
447 assert!(response.error.is_empty());
448 let result = response.result.expect("Missing result");
449 assert!(!result.is_empty());
450
451 let book = result.get("XBTUSDT").expect("XBTUSDT order book not found");
452 assert!(!book.asks.is_empty());
453 assert!(!book.bids.is_empty());
454 }
455
456 #[rstest]
457 fn test_parse_trades() {
458 let data = load_test_data("http_trades.json");
459 let response: KrakenResponse<TradesResponse> =
460 serde_json::from_str(&data).expect("Failed to parse trades");
461
462 assert!(response.error.is_empty());
463 let result = response.result.expect("Missing result");
464 assert!(!result.data.is_empty());
465 }
466}