nautilus_kraken/http/
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 HTTP API responses.
17
18use indexmap::IndexMap;
19use serde::{Deserialize, Serialize};
20use ustr::Ustr;
21
22use crate::common::enums::{
23    KrakenAssetClass, KrakenOrderSide, KrakenOrderType, KrakenPairStatus, KrakenSystemStatus,
24};
25
26// Asset Pairs (Instruments) Models
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct AssetPairInfo {
30    pub altname: Ustr,
31    pub wsname: Option<Ustr>,
32    pub aclass_base: KrakenAssetClass,
33    pub base: Ustr,
34    pub aclass_quote: KrakenAssetClass,
35    pub quote: Ustr,
36    pub cost_decimals: u8,
37    pub pair_decimals: u8,
38    pub lot_decimals: u8,
39    pub lot_multiplier: i32,
40    #[serde(default)]
41    pub leverage_buy: Vec<i32>,
42    #[serde(default)]
43    pub leverage_sell: Vec<i32>,
44    #[serde(default)]
45    pub fees: Vec<(i32, f64)>,
46    #[serde(default)]
47    pub fees_maker: Vec<(i32, f64)>,
48    pub fee_volume_currency: Option<Ustr>,
49    pub margin_call: Option<i32>,
50    pub margin_stop: Option<i32>,
51    pub ordermin: Option<String>,
52    pub costmin: Option<String>,
53    pub tick_size: Option<String>,
54    pub status: Option<KrakenPairStatus>,
55    #[serde(default)]
56    pub long_position_limit: Option<i64>,
57    #[serde(default)]
58    pub short_position_limit: Option<i64>,
59}
60
61pub type AssetPairsResponse = IndexMap<String, AssetPairInfo>;
62
63// Ticker Models
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct TickerInfo {
67    #[serde(rename = "a")]
68    pub ask: Vec<String>,
69    #[serde(rename = "b")]
70    pub bid: Vec<String>,
71    #[serde(rename = "c")]
72    pub last: Vec<String>,
73    #[serde(rename = "v")]
74    pub volume: Vec<String>,
75    #[serde(rename = "p")]
76    pub vwap: Vec<String>,
77    #[serde(rename = "t")]
78    pub trades: Vec<i64>,
79    #[serde(rename = "l")]
80    pub low: Vec<String>,
81    #[serde(rename = "h")]
82    pub high: Vec<String>,
83    #[serde(rename = "o")]
84    pub open: String,
85}
86
87pub type TickerResponse = IndexMap<String, TickerInfo>;
88
89// OHLC (Candlestick) Models
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct OhlcData {
93    pub time: i64,
94    pub open: String,
95    pub high: String,
96    pub low: String,
97    pub close: String,
98    pub vwap: String,
99    pub volume: String,
100    pub count: i64,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct OhlcResponse {
105    pub last: i64,
106    #[serde(flatten)]
107    pub data: IndexMap<String, Vec<Vec<serde_json::Value>>>,
108}
109
110// Trades Models
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct TradeData {
114    pub price: String,
115    pub volume: String,
116    pub time: f64,
117    pub side: KrakenOrderSide,
118    pub order_type: KrakenOrderType,
119    pub misc: String,
120    #[serde(default)]
121    pub trade_id: Option<i64>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct TradesResponse {
126    pub last: String,
127    #[serde(flatten)]
128    pub data: IndexMap<String, Vec<Vec<serde_json::Value>>>,
129}
130
131// Order Book Models
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct OrderBookLevel {
135    pub price: String,
136    pub volume: String,
137    pub timestamp: i64,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct OrderBookData {
142    pub asks: Vec<Vec<serde_json::Value>>,
143    pub bids: Vec<Vec<serde_json::Value>>,
144}
145
146pub type OrderBookResponse = IndexMap<String, OrderBookData>;
147
148// System Status Models
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct SystemStatus {
152    pub status: KrakenSystemStatus,
153    pub timestamp: String,
154}
155
156// Server Time Models
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct ServerTime {
160    pub unixtime: i64,
161    pub rfc1123: String,
162}
163
164// WebSocket Token Models
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct WebSocketToken {
168    pub token: String,
169    pub expires: i32,
170}
171
172// Futures Models
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct FuturesMarginLevel {
176    pub contracts: i64,
177    #[serde(rename = "initialMargin")]
178    pub initial_margin: f64,
179    #[serde(rename = "maintenanceMargin")]
180    pub maintenance_margin: f64,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct FuturesInstrument {
185    pub symbol: String,
186    #[serde(rename = "type")]
187    pub instrument_type: String,
188    pub underlying: String,
189    #[serde(rename = "tickSize")]
190    pub tick_size: f64,
191    #[serde(rename = "contractSize")]
192    pub contract_size: f64,
193    pub tradeable: bool,
194    #[serde(rename = "impactMidSize")]
195    pub impact_mid_size: f64,
196    #[serde(rename = "maxPositionSize")]
197    pub max_position_size: f64,
198    #[serde(rename = "openingDate")]
199    pub opening_date: String,
200    #[serde(rename = "marginLevels")]
201    pub margin_levels: Vec<FuturesMarginLevel>,
202    #[serde(rename = "fundingRateCoefficient", default)]
203    pub funding_rate_coefficient: Option<i32>,
204    #[serde(rename = "maxRelativeFundingRate", default)]
205    pub max_relative_funding_rate: Option<f64>,
206    #[serde(default)]
207    pub isin: Option<String>,
208    #[serde(rename = "contractValueTradePrecision")]
209    pub contract_value_trade_precision: i32,
210    #[serde(rename = "postOnly")]
211    pub post_only: bool,
212    #[serde(rename = "feeScheduleUid")]
213    pub fee_schedule_uid: String,
214    pub mtf: bool,
215    pub base: String,
216    pub quote: String,
217    pub pair: String,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct FuturesInstrumentsResponse {
222    pub result: String,
223    pub instruments: Vec<FuturesInstrument>,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct FuturesTicker {
228    pub symbol: String,
229    pub last: f64,
230    #[serde(rename = "lastTime")]
231    pub last_time: String,
232    pub tag: String,
233    pub pair: String,
234    #[serde(rename = "markPrice")]
235    pub mark_price: f64,
236    pub bid: f64,
237    #[serde(rename = "bidSize")]
238    pub bid_size: f64,
239    pub ask: f64,
240    #[serde(rename = "askSize")]
241    pub ask_size: f64,
242    #[serde(rename = "vol24h")]
243    pub vol_24h: f64,
244    #[serde(rename = "volumeQuote")]
245    pub volume_quote: f64,
246    #[serde(rename = "openInterest")]
247    pub open_interest: f64,
248    #[serde(rename = "open24h")]
249    pub open_24h: f64,
250    #[serde(rename = "high24h")]
251    pub high_24h: f64,
252    #[serde(rename = "low24h")]
253    pub low_24h: f64,
254    #[serde(rename = "lastSize")]
255    pub last_size: f64,
256    #[serde(rename = "fundingRate", default)]
257    pub funding_rate: Option<f64>,
258    #[serde(rename = "fundingRatePrediction", default)]
259    pub funding_rate_prediction: Option<f64>,
260    pub suspended: bool,
261    #[serde(rename = "indexPrice")]
262    pub index_price: f64,
263    #[serde(rename = "postOnly")]
264    pub post_only: bool,
265    #[serde(rename = "change24h")]
266    pub change_24h: f64,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct FuturesTickersResponse {
271    pub result: String,
272    #[serde(rename = "serverTime")]
273    pub server_time: String,
274    pub tickers: Vec<FuturesTicker>,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct FuturesCandle {
279    pub time: i64,
280    pub open: String,
281    pub high: String,
282    pub low: String,
283    pub close: String,
284    pub volume: String,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct FuturesCandlesResponse {
289    pub candles: Vec<FuturesCandle>,
290}
291
292#[cfg(test)]
293mod tests {
294    use rstest::rstest;
295
296    use super::*;
297    use crate::http::client::KrakenResponse;
298
299    fn load_test_data(filename: &str) -> String {
300        let path = format!("test_data/{filename}");
301        std::fs::read_to_string(&path)
302            .unwrap_or_else(|e| panic!("Failed to load test data from {path}: {e}"))
303    }
304
305    #[rstest]
306    fn test_parse_server_time() {
307        let data = load_test_data("http_server_time.json");
308        let response: KrakenResponse<ServerTime> =
309            serde_json::from_str(&data).expect("Failed to parse server time");
310
311        assert!(response.error.is_empty());
312        let result = response.result.expect("Missing result");
313        assert!(result.unixtime > 0);
314        assert!(!result.rfc1123.is_empty());
315    }
316
317    #[rstest]
318    fn test_parse_system_status() {
319        let data = load_test_data("http_system_status.json");
320        let response: KrakenResponse<SystemStatus> =
321            serde_json::from_str(&data).expect("Failed to parse system status");
322
323        assert!(response.error.is_empty());
324        let result = response.result.expect("Missing result");
325        assert!(!result.timestamp.is_empty());
326    }
327
328    #[rstest]
329    fn test_parse_asset_pairs() {
330        let data = load_test_data("http_asset_pairs.json");
331        let response: KrakenResponse<AssetPairsResponse> =
332            serde_json::from_str(&data).expect("Failed to parse asset pairs");
333
334        assert!(response.error.is_empty());
335        let result = response.result.expect("Missing result");
336        assert!(!result.is_empty());
337
338        let pair = result.get("XBTUSDT").expect("XBTUSDT pair not found");
339        assert_eq!(pair.altname.as_str(), "XBTUSDT");
340        assert_eq!(pair.base.as_str(), "XXBT");
341        assert_eq!(pair.quote.as_str(), "USDT");
342        assert!(pair.wsname.is_some());
343    }
344
345    #[rstest]
346    fn test_parse_ticker() {
347        let data = load_test_data("http_ticker.json");
348        let response: KrakenResponse<TickerResponse> =
349            serde_json::from_str(&data).expect("Failed to parse ticker");
350
351        assert!(response.error.is_empty());
352        let result = response.result.expect("Missing result");
353        assert!(!result.is_empty());
354
355        let ticker = result.get("XBTUSDT").expect("XBTUSDT ticker not found");
356        assert_eq!(ticker.ask.len(), 3);
357        assert_eq!(ticker.bid.len(), 3);
358        assert_eq!(ticker.last.len(), 2);
359    }
360
361    #[rstest]
362    fn test_parse_ohlc() {
363        let data = load_test_data("http_ohlc.json");
364        let response: KrakenResponse<serde_json::Value> =
365            serde_json::from_str(&data).expect("Failed to parse OHLC");
366
367        assert!(response.error.is_empty());
368        assert!(response.result.is_some());
369    }
370
371    #[rstest]
372    fn test_parse_order_book() {
373        let data = load_test_data("http_order_book.json");
374        let response: KrakenResponse<OrderBookResponse> =
375            serde_json::from_str(&data).expect("Failed to parse order book");
376
377        assert!(response.error.is_empty());
378        let result = response.result.expect("Missing result");
379        assert!(!result.is_empty());
380
381        let book = result.get("XBTUSDT").expect("XBTUSDT order book not found");
382        assert!(!book.asks.is_empty());
383        assert!(!book.bids.is_empty());
384    }
385
386    #[rstest]
387    fn test_parse_trades() {
388        let data = load_test_data("http_trades.json");
389        let response: KrakenResponse<TradesResponse> =
390            serde_json::from_str(&data).expect("Failed to parse trades");
391
392        assert!(response.error.is_empty());
393        let result = response.result.expect("Missing result");
394        assert!(!result.data.is_empty());
395    }
396}