nautilus_kraken/http/spot/
models.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Data models for Kraken Spot HTTP API responses.
17
18use 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/// Wrapper for Kraken API responses.
28#[derive(Debug, Clone, serde::Deserialize)]
29pub struct KrakenResponse<T> {
30    pub error: Vec<String>,
31    pub result: Option<T>,
32}
33
34// Balance Models
35
36/// Response from Kraken Balance endpoint.
37/// Maps currency codes (e.g., "USDT", "ETH") to their balance amounts as strings.
38pub type BalanceResponse = IndexMap<String, String>;
39
40// Asset Pairs (Instruments) Models
41
42#[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// Ticker Models
78
79#[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// OHLC (Candlestick) Models
104
105#[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// Trades Models
125
126#[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// Order Book Models
146
147#[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// System Status Models
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct SystemStatus {
166    pub status: KrakenSystemStatus,
167    pub timestamp: String,
168}
169
170// Server Time Models
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct ServerTime {
174    pub unixtime: i64,
175    pub rfc1123: String,
176}
177
178// WebSocket Token Models
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct WebSocketToken {
182    pub token: String,
183    pub expires: i32,
184}
185
186// Spot Private Trading Models
187
188/// Order description from QueryOrders response (full details, required fields).
189#[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/// Order description from AddOrder response (simpler, with optional fields).
203#[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    /// Average fill price (if returned by the API)
243    #[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// Spot Order Execution Models
302
303#[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 SpotCancelOrderResponse {
314    pub count: i32,
315    #[serde(default)]
316    pub pending: Option<bool>,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct SpotCancelOrderBatchResponse {
321    pub count: i32,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct SpotEditOrderResponse {
326    pub descr: Option<AddOrderDescription>,
327    pub txid: Option<String>,
328    #[serde(default)]
329    pub originaltxid: Option<String>,
330    #[serde(default)]
331    pub volume: Option<String>,
332    #[serde(default)]
333    pub price: Option<String>,
334    #[serde(default)]
335    pub price2: Option<String>,
336    #[serde(default)]
337    pub orders_cancelled: Option<i32>,
338}
339
340/// Response from `POST /0/private/AmendOrder`.
341#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct SpotAmendOrderResponse {
343    /// The amend transaction ID.
344    pub amend_id: String,
345}
346
347#[cfg(test)]
348mod tests {
349    use rstest::rstest;
350
351    use super::*;
352
353    fn load_test_data(filename: &str) -> String {
354        let path = format!("test_data/{filename}");
355        std::fs::read_to_string(&path)
356            .unwrap_or_else(|e| panic!("Failed to load test data from {path}: {e}"))
357    }
358
359    #[rstest]
360    fn test_parse_server_time() {
361        let data = load_test_data("http_server_time.json");
362        let response: KrakenResponse<ServerTime> =
363            serde_json::from_str(&data).expect("Failed to parse server time");
364
365        assert!(response.error.is_empty());
366        let result = response.result.expect("Missing result");
367        assert!(result.unixtime > 0);
368        assert!(!result.rfc1123.is_empty());
369    }
370
371    #[rstest]
372    fn test_parse_system_status() {
373        let data = load_test_data("http_system_status.json");
374        let response: KrakenResponse<SystemStatus> =
375            serde_json::from_str(&data).expect("Failed to parse system status");
376
377        assert!(response.error.is_empty());
378        let result = response.result.expect("Missing result");
379        assert!(!result.timestamp.is_empty());
380    }
381
382    #[rstest]
383    fn test_parse_asset_pairs() {
384        let data = load_test_data("http_asset_pairs.json");
385        let response: KrakenResponse<AssetPairsResponse> =
386            serde_json::from_str(&data).expect("Failed to parse asset pairs");
387
388        assert!(response.error.is_empty());
389        let result = response.result.expect("Missing result");
390        assert!(!result.is_empty());
391
392        let pair = result.get("XBTUSDT").expect("XBTUSDT pair not found");
393        assert_eq!(pair.altname.as_str(), "XBTUSDT");
394        assert_eq!(pair.base.as_str(), "XXBT");
395        assert_eq!(pair.quote.as_str(), "USDT");
396        assert!(pair.wsname.is_some());
397    }
398
399    #[rstest]
400    fn test_parse_ticker() {
401        let data = load_test_data("http_ticker.json");
402        let response: KrakenResponse<TickerResponse> =
403            serde_json::from_str(&data).expect("Failed to parse ticker");
404
405        assert!(response.error.is_empty());
406        let result = response.result.expect("Missing result");
407        assert!(!result.is_empty());
408
409        let ticker = result.get("XBTUSDT").expect("XBTUSDT ticker not found");
410        assert_eq!(ticker.ask.len(), 3);
411        assert_eq!(ticker.bid.len(), 3);
412        assert_eq!(ticker.last.len(), 2);
413    }
414
415    #[rstest]
416    fn test_parse_ohlc() {
417        let data = load_test_data("http_ohlc.json");
418        let response: KrakenResponse<serde_json::Value> =
419            serde_json::from_str(&data).expect("Failed to parse OHLC");
420
421        assert!(response.error.is_empty());
422        assert!(response.result.is_some());
423    }
424
425    #[rstest]
426    fn test_parse_order_book() {
427        let data = load_test_data("http_order_book.json");
428        let response: KrakenResponse<OrderBookResponse> =
429            serde_json::from_str(&data).expect("Failed to parse order book");
430
431        assert!(response.error.is_empty());
432        let result = response.result.expect("Missing result");
433        assert!(!result.is_empty());
434
435        let book = result.get("XBTUSDT").expect("XBTUSDT order book not found");
436        assert!(!book.asks.is_empty());
437        assert!(!book.bids.is_empty());
438    }
439
440    #[rstest]
441    fn test_parse_trades() {
442        let data = load_test_data("http_trades.json");
443        let response: KrakenResponse<TradesResponse> =
444            serde_json::from_str(&data).expect("Failed to parse trades");
445
446        assert!(response.error.is_empty());
447        let result = response.result.expect("Missing result");
448        assert!(!result.data.is_empty());
449    }
450}