Skip to main content

nautilus_kraken/websocket/spot_v2/
messages.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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 WebSocket v2 API messages.
17
18use jiff::Timestamp;
19use rust_decimal::Decimal;
20use serde::{Deserialize, Serialize};
21use serde_json::{Value, value::RawValue};
22use ustr::Ustr;
23
24use super::enums::{
25    KrakenExecType, KrakenLiquidityInd, KrakenWsChannel, KrakenWsMessageType, KrakenWsMethod,
26    KrakenWsOrderStatus,
27};
28use crate::{
29    common::{
30        enums::{KrakenOrderSide, KrakenOrderType, KrakenSpotTrigger, KrakenTimeInForce},
31        serialization::{decimal, optional_decimal},
32    },
33    websocket::spot_v2::level_3::messages::{KrakenL3Snapshot, KrakenL3UpdateData},
34};
35
36/// Output message types from the Kraken Spot v2 WebSocket handler.
37#[derive(Clone, Debug)]
38pub enum KrakenSpotWsMessage {
39    Ticker(Vec<KrakenWsTickerData>),
40    Trade(Vec<KrakenWsTradeData>),
41    Book {
42        data: Vec<KrakenWsBookData>,
43        is_snapshot: bool,
44    },
45    Ohlc(Vec<KrakenWsOhlcData>),
46    Execution(Vec<KrakenWsExecutionData>),
47    OrderResponse(KrakenWsOrderResponse),
48    L3Snapshot(KrakenL3Snapshot),
49    L3Update(KrakenL3UpdateData),
50    Reconnected,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct KrakenWsRequest {
55    pub method: KrakenWsMethod,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub params: Option<KrakenWsParams>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub req_id: Option<u64>,
60}
61
62/// Parameters for a Kraken WebSocket request, covering both channel subscriptions and order methods.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(untagged)]
65pub enum KrakenWsParams {
66    /// Parameters for subscribe/unsubscribe channel requests.
67    Channel(KrakenWsChannelParams),
68    /// Parameters for the `add_order` method.
69    AddOrder(KrakenWsAddOrderParams),
70    /// Parameters for the `amend_order` method.
71    AmendOrder(KrakenWsAmendOrderParams),
72    /// Parameters for the `cancel_order` method.
73    CancelOrder(KrakenWsCancelOrderParams),
74    /// Parameters for the `batch_add` method.
75    BatchAdd(KrakenWsBatchAddParams),
76}
77
78/// Parameters for channel subscribe/unsubscribe requests.
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct KrakenWsChannelParams {
81    /// Channel to subscribe or unsubscribe.
82    pub channel: KrakenWsChannel,
83    /// Symbols to subscribe for (market data channels).
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub symbol: Option<Vec<Ustr>>,
86    /// Whether to receive a snapshot on subscribe.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub snapshot: Option<bool>,
89    /// Order book depth (book channel only).
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub depth: Option<u32>,
92    /// OHLC interval in minutes (ohlc channel only).
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub interval: Option<u32>,
95    /// Event trigger filter (ticker channel, e.g. `"bbo"`).
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub event_trigger: Option<String>,
98    /// Authentication token (private channels).
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub token: Option<String>,
101    /// Whether to include a snapshot of open orders (executions channel).
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub snap_orders: Option<bool>,
104    /// Whether to include a snapshot of recent trades (executions channel).
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub snap_trades: Option<bool>,
107}
108
109/// Parameters for the `add_order` WebSocket method.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct KrakenWsAddOrderParams {
112    /// Order type (limit, market, etc.).
113    pub order_type: KrakenOrderType,
114    /// Order side (buy or sell).
115    pub side: KrakenOrderSide,
116    /// Order quantity in base currency.
117    #[serde(with = "decimal")]
118    pub order_qty: Decimal,
119    /// Trading pair symbol (e.g. `"BTC/USD"`).
120    pub symbol: String,
121    /// Authentication token.
122    pub token: String,
123    /// Limit price (required for limit orders).
124    #[serde(
125        default,
126        skip_serializing_if = "Option::is_none",
127        with = "optional_decimal"
128    )]
129    pub limit_price: Option<Decimal>,
130    /// Time in force policy.
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub time_in_force: Option<KrakenTimeInForce>,
133    /// Expiration timestamp for `GoodTilDate` orders. Required by Kraken whenever
134    /// `time_in_force = GTD`. Accepts an RFC3339 timestamp (`"2026-12-31T23:59:59Z"`)
135    /// or a relative duration (`"+30s"`, `"+1h"`, `"+2D"`).
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub expire_time: Option<String>,
138    /// Client-assigned order ID.
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub cl_ord_id: Option<String>,
141    /// Whether the order must be a passive post-only order.
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub post_only: Option<bool>,
144    /// Whether the order may only reduce an existing position.
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub reduce_only: Option<bool>,
147    /// Trigger parameters for stop/take-profit orders.
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub trigger: Option<KrakenWsTriggerParams>,
150    /// Leverage multiplier for margin orders; omit for non-margin (cash) orders.
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub leverage: Option<u16>,
153    /// Conditional close order attached to the parent order.
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub conditional: Option<KrakenWsConditionalParams>,
156}
157
158/// Parameters for the `amend_order` WebSocket method.
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct KrakenWsAmendOrderParams {
161    /// Authentication token.
162    pub token: String,
163    /// Kraken order ID to amend (preferred over `cl_ord_id`).
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub order_id: Option<String>,
166    /// Client-assigned order ID to amend (used when `order_id` is unavailable).
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub cl_ord_id: Option<String>,
169    /// New order quantity (replaces the existing quantity).
170    #[serde(
171        default,
172        skip_serializing_if = "Option::is_none",
173        with = "optional_decimal"
174    )]
175    pub order_qty: Option<Decimal>,
176    /// New limit price.
177    #[serde(
178        default,
179        skip_serializing_if = "Option::is_none",
180        with = "optional_decimal"
181    )]
182    pub limit_price: Option<Decimal>,
183    /// New trigger price (for conditional orders).
184    #[serde(
185        default,
186        skip_serializing_if = "Option::is_none",
187        with = "optional_decimal"
188    )]
189    pub trigger_price: Option<Decimal>,
190}
191
192/// Parameters for the `cancel_order` WebSocket method.
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct KrakenWsCancelOrderParams {
195    /// Authentication token.
196    pub token: String,
197    /// One or more Kraken order IDs to cancel (preferred over `cl_ord_id`).
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub order_id: Option<Vec<String>>,
200    /// One or more client-assigned order IDs to cancel (used when `order_id` is unavailable).
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub cl_ord_id: Option<Vec<String>>,
203}
204
205/// Parameters for the `batch_add` WebSocket method.
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct KrakenWsBatchAddParams {
208    /// Trading pair symbol shared by all orders in the batch.
209    pub symbol: String,
210    /// List of orders to submit.
211    pub orders: Vec<KrakenWsBatchAddOrder>,
212    /// Authentication token.
213    pub token: String,
214}
215
216/// A single order entry within a `batch_add` request.
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct KrakenWsBatchAddOrder {
219    /// Order type.
220    pub order_type: KrakenOrderType,
221    /// Order side.
222    pub side: KrakenOrderSide,
223    /// Order quantity.
224    #[serde(with = "decimal")]
225    pub order_qty: Decimal,
226    /// Limit price (required for limit orders).
227    #[serde(
228        default,
229        skip_serializing_if = "Option::is_none",
230        with = "optional_decimal"
231    )]
232    pub limit_price: Option<Decimal>,
233    /// Client-assigned order ID.
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub cl_ord_id: Option<String>,
236    /// Time in force policy.
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub time_in_force: Option<KrakenTimeInForce>,
239    /// Expiration timestamp for `GoodTilDate` legs. Required by Kraken whenever
240    /// `time_in_force = GTD`. RFC3339 timestamp or relative duration string.
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub expire_time: Option<String>,
243    /// Whether the order must be a passive post-only order.
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub post_only: Option<bool>,
246    /// Whether the order may only reduce an existing position.
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub reduce_only: Option<bool>,
249    /// Leverage multiplier for margin orders; omit for non-margin (cash) orders.
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub leverage: Option<u16>,
252    /// Trigger parameters for stop-loss and take-profit order types.
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub trigger: Option<KrakenWsTriggerParams>,
255}
256
257/// Trigger parameters for stop/take-profit order types.
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct KrakenWsTriggerParams {
260    /// Reference price for the trigger.
261    pub reference: KrakenSpotTrigger,
262    /// Trigger price level.
263    #[serde(with = "decimal")]
264    pub price: Decimal,
265    /// Price direction for the trigger (above or below).
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub price_type: Option<String>,
268}
269
270/// Conditional close order attached to a parent order.
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct KrakenWsConditionalParams {
273    /// Order type for the conditional leg.
274    pub order_type: KrakenOrderType,
275    /// Limit price for the conditional leg.
276    #[serde(
277        default,
278        skip_serializing_if = "Option::is_none",
279        with = "optional_decimal"
280    )]
281    pub limit_price: Option<Decimal>,
282    /// Stop price for the conditional leg.
283    #[serde(
284        default,
285        skip_serializing_if = "Option::is_none",
286        with = "optional_decimal"
287    )]
288    pub trigger_price: Option<Decimal>,
289}
290
291/// Response envelope for order-method WebSocket responses.
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct KrakenWsOrderResponse {
294    /// The method that triggered this response.
295    pub method: KrakenWsMethod,
296    /// Echo of the request ID (only present when the client sent one).
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub req_id: Option<u64>,
299    /// Whether the request succeeded.
300    pub success: bool,
301    /// ISO 8601 timestamp when the request was received.
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub time_in: Option<String>,
304    /// ISO 8601 timestamp when the response was sent.
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub time_out: Option<String>,
307    /// Error message when `success` is `false`.
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub error: Option<String>,
310    /// Result payload when `success` is `true`.
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub result: Option<KrakenWsOrderResult>,
313}
314
315/// Result payload for single-order responses (`add_order`, `amend_order`, `cancel_order`).
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct KrakenWsOrderResult {
318    /// Kraken-assigned order ID.
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub order_id: Option<String>,
321    /// Client-assigned order ID echoed back.
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub cl_ord_id: Option<String>,
324    /// Integer user reference echoed back.
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub order_userref: Option<i64>,
327    /// Non-fatal warnings associated with the order.
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub warning: Option<Vec<String>>,
330    /// Per-order results for `batch_add` responses.
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub orders: Option<Vec<KrakenWsBatchOrderResult>>,
333}
334
335/// Per-order outcome within a `batch_add` response.
336#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct KrakenWsBatchOrderResult {
338    /// Whether this individual order succeeded.
339    pub success: bool,
340    /// Kraken-assigned order ID (present when `success` is `true`).
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub order_id: Option<String>,
343    /// Client-assigned order ID.
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub cl_ord_id: Option<String>,
346    /// Error message (present when `success` is `false`).
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub error: Option<String>,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
352#[serde(tag = "method")]
353pub enum KrakenWsResponse {
354    #[serde(rename = "pong")]
355    Pong(KrakenWsPong),
356    #[serde(rename = "subscribe")]
357    Subscribe(KrakenWsSubscribeResponse),
358    #[serde(rename = "unsubscribe")]
359    Unsubscribe(KrakenWsUnsubscribeResponse),
360    #[serde(other)]
361    Other,
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize)]
365pub struct KrakenWsPong {
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub req_id: Option<u64>,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct KrakenWsSubscribeResponse {
372    pub success: bool,
373    #[serde(skip_serializing_if = "Option::is_none")]
374    pub error: Option<String>,
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub req_id: Option<u64>,
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub result: Option<KrakenWsSubscriptionResult>,
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct KrakenWsUnsubscribeResponse {
383    pub success: bool,
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub error: Option<String>,
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub req_id: Option<u64>,
388}
389
390#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct KrakenWsSubscriptionResult {
392    pub channel: KrakenWsChannel,
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub snapshot: Option<bool>,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize)]
398pub struct KrakenWsMessage {
399    pub channel: KrakenWsChannel,
400    #[serde(rename = "type")]
401    pub event_type: KrakenWsMessageType,
402    pub data: Vec<Value>,
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub symbol: Option<Ustr>,
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub timestamp: Option<Timestamp>,
407}
408
409#[derive(Debug, Clone, Deserialize)]
410pub(crate) struct KrakenWsRawMessage {
411    pub channel: KrakenWsChannel,
412    #[serde(rename = "type")]
413    pub event_type: KrakenWsMessageType,
414    pub data: Vec<Box<RawValue>>,
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize)]
418pub struct KrakenWsTickerData {
419    pub symbol: Ustr,
420    #[serde(with = "decimal")]
421    pub bid: Decimal,
422    #[serde(with = "decimal")]
423    pub bid_qty: Decimal,
424    #[serde(with = "decimal")]
425    pub ask: Decimal,
426    #[serde(with = "decimal")]
427    pub ask_qty: Decimal,
428    #[serde(with = "decimal")]
429    pub last: Decimal,
430    #[serde(with = "decimal")]
431    pub volume: Decimal,
432    #[serde(with = "decimal")]
433    pub vwap: Decimal,
434    #[serde(with = "decimal")]
435    pub low: Decimal,
436    #[serde(with = "decimal")]
437    pub high: Decimal,
438    #[serde(with = "decimal")]
439    pub change: Decimal,
440    #[serde(with = "decimal")]
441    pub change_pct: Decimal,
442    pub timestamp: Timestamp,
443}
444
445#[derive(Debug, Clone, Serialize, Deserialize)]
446pub struct KrakenWsTradeData {
447    pub symbol: Ustr,
448    pub side: KrakenOrderSide,
449    #[serde(with = "decimal")]
450    pub price: Decimal,
451    #[serde(with = "decimal")]
452    pub qty: Decimal,
453    pub ord_type: KrakenOrderType,
454    pub trade_id: i64,
455    pub timestamp: Timestamp,
456}
457
458#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct KrakenWsBookData {
460    pub symbol: Ustr,
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub bids: Option<Vec<KrakenWsBookLevel>>,
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub asks: Option<Vec<KrakenWsBookLevel>>,
465    pub checksum: Option<u32>,
466    pub timestamp: Timestamp,
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct KrakenWsBookLevel {
471    #[serde(with = "decimal")]
472    pub price: Decimal,
473    #[serde(with = "decimal")]
474    pub qty: Decimal,
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize)]
478pub struct KrakenWsOhlcData {
479    pub symbol: Ustr,
480    pub interval: u32,
481    pub interval_begin: Timestamp,
482    #[serde(with = "decimal")]
483    pub open: Decimal,
484    #[serde(with = "decimal")]
485    pub high: Decimal,
486    #[serde(with = "decimal")]
487    pub low: Decimal,
488    #[serde(with = "decimal")]
489    pub close: Decimal,
490    #[serde(with = "decimal")]
491    pub volume: Decimal,
492    #[serde(with = "decimal")]
493    pub vwap: Decimal,
494    pub trades: i64,
495}
496
497/// Execution message from the Kraken executions channel.
498#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct KrakenWsExecutionData {
500    /// Execution type.
501    pub exec_type: KrakenExecType,
502    /// Kraken order ID.
503    pub order_id: String,
504    /// Client order ID (if provided when order was submitted).
505    #[serde(skip_serializing_if = "Option::is_none")]
506    pub cl_ord_id: Option<String>,
507    /// Trading pair symbol.
508    #[serde(skip_serializing_if = "Option::is_none")]
509    pub symbol: Option<String>,
510    /// Order side.
511    #[serde(skip_serializing_if = "Option::is_none")]
512    pub side: Option<KrakenOrderSide>,
513    /// Order type.
514    #[serde(skip_serializing_if = "Option::is_none")]
515    pub order_type: Option<KrakenOrderType>,
516    /// Order quantity.
517    #[serde(
518        default,
519        skip_serializing_if = "Option::is_none",
520        with = "optional_decimal"
521    )]
522    pub order_qty: Option<Decimal>,
523    /// Limit price.
524    #[serde(
525        default,
526        skip_serializing_if = "Option::is_none",
527        with = "optional_decimal"
528    )]
529    pub limit_price: Option<Decimal>,
530    /// Order status.
531    #[serde(skip_serializing_if = "Option::is_none")]
532    pub order_status: Option<KrakenWsOrderStatus>,
533    /// Cumulative filled quantity.
534    #[serde(
535        default,
536        skip_serializing_if = "Option::is_none",
537        with = "optional_decimal"
538    )]
539    pub cum_qty: Option<Decimal>,
540    /// Cumulative cost.
541    #[serde(
542        default,
543        skip_serializing_if = "Option::is_none",
544        with = "optional_decimal"
545    )]
546    pub cum_cost: Option<Decimal>,
547    /// Average fill price.
548    #[serde(
549        default,
550        skip_serializing_if = "Option::is_none",
551        with = "optional_decimal"
552    )]
553    pub avg_price: Option<Decimal>,
554    /// Time in force.
555    #[serde(skip_serializing_if = "Option::is_none")]
556    pub time_in_force: Option<KrakenTimeInForce>,
557    /// Post only flag.
558    #[serde(skip_serializing_if = "Option::is_none")]
559    pub post_only: Option<bool>,
560    /// Reduce only flag.
561    #[serde(skip_serializing_if = "Option::is_none")]
562    pub reduce_only: Option<bool>,
563    /// Event timestamp.
564    pub timestamp: Timestamp,
565    /// Execution/trade ID.
566    #[serde(skip_serializing_if = "Option::is_none")]
567    pub exec_id: Option<String>,
568    /// Last fill quantity.
569    #[serde(
570        default,
571        skip_serializing_if = "Option::is_none",
572        with = "optional_decimal"
573    )]
574    pub last_qty: Option<Decimal>,
575    /// Last fill price.
576    #[serde(
577        default,
578        skip_serializing_if = "Option::is_none",
579        with = "optional_decimal"
580    )]
581    pub last_price: Option<Decimal>,
582    /// Trade cost.
583    #[serde(
584        default,
585        skip_serializing_if = "Option::is_none",
586        with = "optional_decimal"
587    )]
588    pub cost: Option<Decimal>,
589    /// Liquidity indicator.
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub liquidity_ind: Option<KrakenLiquidityInd>,
592    /// Fees array.
593    #[serde(skip_serializing_if = "Option::is_none")]
594    pub fees: Option<Vec<KrakenWsFee>>,
595    /// Fee in USD equivalent.
596    #[serde(
597        default,
598        skip_serializing_if = "Option::is_none",
599        with = "optional_decimal"
600    )]
601    pub fee_usd_equiv: Option<Decimal>,
602    /// Cancel reason (when exec_type is Canceled/Expired).
603    #[serde(skip_serializing_if = "Option::is_none")]
604    pub reason: Option<String>,
605}
606
607/// Fee information from execution messages.
608#[derive(Debug, Clone, Serialize, Deserialize)]
609pub struct KrakenWsFee {
610    /// Fee asset.
611    pub asset: String,
612    /// Fee quantity.
613    #[serde(with = "decimal")]
614    pub qty: Decimal,
615}
616
617#[cfg(test)]
618mod tests {
619    use rstest::rstest;
620    use rust_decimal_macros::dec;
621
622    use super::*;
623
624    fn load_test_data(filename: &str) -> String {
625        let path = format!("test_data/{filename}");
626        std::fs::read_to_string(&path)
627            .unwrap_or_else(|e| panic!("Failed to load test data from {path}: {e}"))
628    }
629
630    #[rstest]
631    fn test_parse_subscribe_response() {
632        let data = load_test_data("ws_subscribe_response.json");
633        let response: KrakenWsResponse =
634            serde_json::from_str(&data).expect("Failed to parse subscribe response");
635
636        match response {
637            KrakenWsResponse::Subscribe(sub) => {
638                assert!(sub.success);
639                assert_eq!(sub.req_id, Some(1));
640                assert!(sub.result.is_some());
641                let result = sub.result.unwrap();
642                assert_eq!(result.channel, KrakenWsChannel::Ticker);
643            }
644            _ => panic!("Expected Subscribe response"),
645        }
646    }
647
648    #[rstest]
649    fn test_parse_pong() {
650        let data = load_test_data("ws_pong.json");
651        let response: KrakenWsResponse = serde_json::from_str(&data).expect("Failed to parse pong");
652
653        match response {
654            KrakenWsResponse::Pong(pong) => {
655                assert_eq!(pong.req_id, Some(42));
656            }
657            _ => panic!("Expected Pong response"),
658        }
659    }
660
661    #[rstest]
662    fn test_parse_ticker_snapshot() {
663        let data = load_test_data("ws_ticker_snapshot.json");
664        let message: KrakenWsRawMessage =
665            serde_json::from_str(&data).expect("Failed to parse ticker snapshot");
666
667        assert_eq!(message.channel, KrakenWsChannel::Ticker);
668        assert_eq!(message.event_type, KrakenWsMessageType::Snapshot);
669        assert!(!message.data.is_empty());
670
671        let ticker: KrakenWsTickerData =
672            serde_json::from_str(message.data[0].get()).expect("Failed to parse ticker data");
673        assert_eq!(ticker.symbol.as_str(), "BTC/USD");
674        assert_eq!(ticker.bid, dec!(105944.20));
675        assert_eq!(ticker.ask, dec!(105944.30));
676        assert_eq!(ticker.last, dec!(105899.40));
677        assert_eq!(ticker.timestamp.as_nanosecond(), 1_671_960_659_123_456_000);
678    }
679
680    #[rstest]
681    fn test_optional_decimal_fields_default_when_missing() {
682        let execution: KrakenWsExecutionData = serde_json::from_str(&load_test_data(
683            "ws_execution_missing_optional_decimals.json",
684        ))
685        .unwrap();
686        let amend: KrakenWsAmendOrderParams = serde_json::from_str(&load_test_data(
687            "ws_amend_order_missing_optional_decimals.json",
688        ))
689        .unwrap();
690
691        assert_eq!(
692            (
693                execution.order_qty,
694                execution.limit_price,
695                execution.cum_qty,
696                execution.cum_cost,
697                execution.avg_price,
698                execution.last_qty,
699                execution.last_price,
700                execution.cost,
701                execution.fee_usd_equiv,
702            ),
703            (None, None, None, None, None, None, None, None, None)
704        );
705        assert_eq!(
706            (amend.order_qty, amend.limit_price, amend.trigger_price),
707            (None, None, None)
708        );
709    }
710
711    #[rstest]
712    fn test_parse_trade_update() {
713        let data = load_test_data("ws_trade_update.json");
714        let message: KrakenWsRawMessage =
715            serde_json::from_str(&data).expect("Failed to parse trade update");
716
717        assert_eq!(message.channel, KrakenWsChannel::Trade);
718        assert_eq!(message.event_type, KrakenWsMessageType::Update);
719        assert_eq!(message.data.len(), 2);
720
721        let trade: KrakenWsTradeData =
722            serde_json::from_str(message.data[0].get()).expect("Failed to parse trade data");
723        assert_eq!(trade.symbol.as_str(), "BTC/USD");
724        assert_eq!(trade.price, dec!(105944.20));
725        assert_eq!(trade.qty, dec!(0.00027625));
726        assert!(trade.trade_id > 0);
727    }
728
729    #[rstest]
730    fn test_parse_book_snapshot() {
731        let data = load_test_data("ws_book_snapshot.json");
732        let message: KrakenWsRawMessage =
733            serde_json::from_str(&data).expect("Failed to parse book snapshot");
734
735        assert_eq!(message.channel, KrakenWsChannel::Book);
736        assert_eq!(message.event_type, KrakenWsMessageType::Snapshot);
737
738        let book: KrakenWsBookData =
739            serde_json::from_str(message.data[0].get()).expect("Failed to parse book data");
740        assert_eq!(book.symbol.as_str(), "BTC/USD");
741        assert!(book.bids.is_some());
742        assert!(book.asks.is_some());
743        assert!(book.checksum.is_some());
744        assert_eq!(book.timestamp.as_nanosecond(), 1_696_613_755_440_295_000);
745
746        let bids = book.bids.unwrap();
747        assert_eq!(bids.len(), 3);
748        assert_eq!(bids[0].price, dec!(105944.20));
749        assert_eq!(bids[0].qty, dec!(0.136));
750    }
751
752    #[rstest]
753    fn test_parse_book_update() {
754        let data = load_test_data("ws_book_update.json");
755        let message: KrakenWsRawMessage =
756            serde_json::from_str(&data).expect("Failed to parse book update");
757
758        assert_eq!(message.channel, KrakenWsChannel::Book);
759        assert_eq!(message.event_type, KrakenWsMessageType::Update);
760
761        let book: KrakenWsBookData =
762            serde_json::from_str(message.data[0].get()).expect("Failed to parse book data");
763        assert_eq!(book.timestamp.as_nanosecond(), 1_696_613_755_440_295_000);
764        assert!(book.checksum.is_some());
765    }
766
767    #[rstest]
768    fn test_parse_ohlc_update() {
769        let data = load_test_data("ws_ohlc_update.json");
770        let message: KrakenWsRawMessage =
771            serde_json::from_str(&data).expect("Failed to parse OHLC update");
772
773        assert_eq!(message.channel, KrakenWsChannel::Ohlc);
774        assert_eq!(message.event_type, KrakenWsMessageType::Update);
775
776        let ohlc: KrakenWsOhlcData =
777            serde_json::from_str(message.data[0].get()).expect("Failed to parse OHLC data");
778        assert_eq!(ohlc.symbol.as_str(), "BTC/USD");
779        assert_eq!(ohlc.open, dec!(106038.2));
780        assert_eq!(ohlc.high, dec!(106044.3));
781        assert_eq!(ohlc.low, dec!(106038.1));
782        assert_eq!(ohlc.close, dec!(106040.1));
783        assert_eq!(ohlc.interval, 1);
784        assert!(ohlc.trades > 0);
785    }
786
787    #[rstest]
788    fn test_serialize_add_order_request() {
789        let request = KrakenWsRequest {
790            method: KrakenWsMethod::AddOrder,
791            params: Some(KrakenWsParams::AddOrder(KrakenWsAddOrderParams {
792                order_type: KrakenOrderType::Limit,
793                side: KrakenOrderSide::Buy,
794                order_qty: dec!(0.01),
795                symbol: "BTC/USD".to_string(),
796                limit_price: Some(dec!(30000.0)),
797                time_in_force: Some(KrakenTimeInForce::GoodTilCancelled),
798                expire_time: None,
799                cl_ord_id: Some("O-20260505-000001".to_string()),
800                post_only: Some(true),
801                reduce_only: None,
802                leverage: None,
803                trigger: None,
804                conditional: None,
805                token: "TESTTOKEN".to_string(),
806            })),
807            req_id: Some(42),
808        };
809
810        let serialized = serde_json::to_string(&request).expect("Failed to serialize");
811        let expected: serde_json::Value =
812            serde_json::from_str(&load_test_data("ws_add_order_request.json"))
813                .expect("Failed to parse fixture");
814        let actual: serde_json::Value =
815            serde_json::from_str(&serialized).expect("Failed to parse serialized");
816        assert_eq!(actual, expected);
817    }
818
819    #[rstest]
820    fn test_serialize_add_order_request_preserves_decimal_precision() {
821        let request = KrakenWsAddOrderParams {
822            order_type: KrakenOrderType::Limit,
823            side: KrakenOrderSide::Buy,
824            order_qty: dec!(0.1234567890123456789012345678),
825            symbol: "BTC/USD".to_string(),
826            token: "TESTTOKEN".to_string(),
827            limit_price: Some(dec!(123456789.123456789)),
828            time_in_force: None,
829            expire_time: None,
830            cl_ord_id: None,
831            post_only: None,
832            reduce_only: None,
833            leverage: None,
834            trigger: None,
835            conditional: None,
836        };
837
838        let serialized = serde_json::to_string(&request).unwrap();
839
840        assert!(serialized.contains("\"order_qty\":0.1234567890123456789012345678"));
841        assert!(serialized.contains("\"limit_price\":123456789.123456789"));
842    }
843
844    #[rstest]
845    fn test_serialize_amend_order_request() {
846        let request = KrakenWsRequest {
847            method: KrakenWsMethod::AmendOrder,
848            params: Some(KrakenWsParams::AmendOrder(KrakenWsAmendOrderParams {
849                order_id: Some("OABCDE-12345-FGHIJ".to_string()),
850                cl_ord_id: None,
851                order_qty: Some(dec!(0.005)),
852                limit_price: None,
853                trigger_price: None,
854                token: "TESTTOKEN".to_string(),
855            })),
856            req_id: Some(43),
857        };
858
859        let serialized = serde_json::to_string(&request).expect("Failed to serialize");
860        let expected: serde_json::Value =
861            serde_json::from_str(&load_test_data("ws_amend_order_request.json"))
862                .expect("Failed to parse fixture");
863        let actual: serde_json::Value =
864            serde_json::from_str(&serialized).expect("Failed to parse serialized");
865        assert_eq!(actual, expected);
866    }
867
868    #[rstest]
869    fn test_serialize_cancel_order_request() {
870        let request = KrakenWsRequest {
871            method: KrakenWsMethod::CancelOrder,
872            params: Some(KrakenWsParams::CancelOrder(KrakenWsCancelOrderParams {
873                order_id: Some(vec!["OABCDE-12345-FGHIJ".to_string()]),
874                cl_ord_id: None,
875                token: "TESTTOKEN".to_string(),
876            })),
877            req_id: Some(44),
878        };
879
880        let serialized = serde_json::to_string(&request).expect("Failed to serialize");
881        let expected: serde_json::Value =
882            serde_json::from_str(&load_test_data("ws_cancel_order_request.json"))
883                .expect("Failed to parse fixture");
884        let actual: serde_json::Value =
885            serde_json::from_str(&serialized).expect("Failed to parse serialized");
886        assert_eq!(actual, expected);
887    }
888
889    #[rstest]
890    fn test_serialize_batch_add_request() {
891        let request = KrakenWsRequest {
892            method: KrakenWsMethod::BatchAdd,
893            params: Some(KrakenWsParams::BatchAdd(KrakenWsBatchAddParams {
894                symbol: "BTC/USD".to_string(),
895                orders: vec![
896                    KrakenWsBatchAddOrder {
897                        order_type: KrakenOrderType::Limit,
898                        side: KrakenOrderSide::Buy,
899                        order_qty: dec!(0.01),
900                        limit_price: Some(dec!(30000.0)),
901                        cl_ord_id: Some("O-A".to_string()),
902                        time_in_force: None,
903                        expire_time: None,
904                        post_only: None,
905                        reduce_only: None,
906                        leverage: None,
907                        trigger: None,
908                    },
909                    KrakenWsBatchAddOrder {
910                        order_type: KrakenOrderType::Limit,
911                        side: KrakenOrderSide::Sell,
912                        order_qty: dec!(0.01),
913                        limit_price: Some(dec!(31000.0)),
914                        cl_ord_id: Some("O-B".to_string()),
915                        time_in_force: None,
916                        expire_time: None,
917                        post_only: None,
918                        reduce_only: None,
919                        leverage: None,
920                        trigger: None,
921                    },
922                ],
923                token: "TESTTOKEN".to_string(),
924            })),
925            req_id: Some(45),
926        };
927
928        let serialized = serde_json::to_string(&request).expect("Failed to serialize");
929        let expected: serde_json::Value =
930            serde_json::from_str(&load_test_data("ws_batch_add_request.json"))
931                .expect("Failed to parse fixture");
932        let actual: serde_json::Value =
933            serde_json::from_str(&serialized).expect("Failed to parse serialized");
934        assert_eq!(actual, expected);
935    }
936
937    #[rstest]
938    fn test_add_order_params_serializes_expire_time_for_gtd() {
939        let params = KrakenWsAddOrderParams {
940            order_type: KrakenOrderType::Limit,
941            side: KrakenOrderSide::Buy,
942            order_qty: dec!(0.01),
943            symbol: "BTC/USD".to_string(),
944            token: "TKN".to_string(),
945            limit_price: Some(dec!(30000.0)),
946            time_in_force: Some(KrakenTimeInForce::GoodTilDate),
947            expire_time: Some("2026-12-31T23:59:59+00:00".to_string()),
948            cl_ord_id: None,
949            post_only: None,
950            reduce_only: None,
951            leverage: None,
952            trigger: None,
953            conditional: None,
954        };
955        let value: serde_json::Value =
956            serde_json::from_str(&serde_json::to_string(&params).expect("serialize"))
957                .expect("json");
958
959        assert_eq!(value["time_in_force"], "GTD");
960        assert_eq!(value["expire_time"], "2026-12-31T23:59:59+00:00");
961    }
962
963    #[rstest]
964    fn test_add_order_params_omits_expire_time_when_absent() {
965        let params = KrakenWsAddOrderParams {
966            order_type: KrakenOrderType::Limit,
967            side: KrakenOrderSide::Buy,
968            order_qty: dec!(0.01),
969            symbol: "BTC/USD".to_string(),
970            token: "TKN".to_string(),
971            limit_price: Some(dec!(30000.0)),
972            time_in_force: None,
973            expire_time: None,
974            cl_ord_id: None,
975            post_only: None,
976            reduce_only: None,
977            leverage: None,
978            trigger: None,
979            conditional: None,
980        };
981        let value: serde_json::Value =
982            serde_json::from_str(&serde_json::to_string(&params).expect("serialize"))
983                .expect("json");
984
985        assert!(value.get("expire_time").is_none());
986    }
987
988    #[rstest]
989    fn test_batch_add_order_serializes_leverage_and_trigger() {
990        let order = KrakenWsBatchAddOrder {
991            order_type: KrakenOrderType::StopLossLimit,
992            side: KrakenOrderSide::Buy,
993            order_qty: dec!(0.01),
994            limit_price: Some(dec!(31000.0)),
995            cl_ord_id: Some("O-CONDITIONAL".to_string()),
996            time_in_force: None,
997            expire_time: None,
998            post_only: None,
999            reduce_only: None,
1000            leverage: Some(2),
1001            trigger: Some(KrakenWsTriggerParams {
1002                reference: KrakenSpotTrigger::Last,
1003                price: dec!(30500.0),
1004                price_type: None,
1005            }),
1006        };
1007        let value: serde_json::Value =
1008            serde_json::from_str(&serde_json::to_string(&order).expect("serialize")).expect("json");
1009
1010        assert_eq!(
1011            value["leverage"], 2,
1012            "leverage must be serialized for margin batch legs",
1013        );
1014        assert!(
1015            value.get("trigger").is_some(),
1016            "trigger must be serialized for conditional batch legs",
1017        );
1018        assert_eq!(value["trigger"]["reference"], "last");
1019        assert_eq!(value["trigger"]["price"].to_string(), "30500.0");
1020    }
1021
1022    #[rstest]
1023    fn test_batch_add_order_omits_leverage_and_trigger_when_absent() {
1024        let order = KrakenWsBatchAddOrder {
1025            order_type: KrakenOrderType::Limit,
1026            side: KrakenOrderSide::Buy,
1027            order_qty: dec!(0.01),
1028            limit_price: Some(dec!(30000.0)),
1029            cl_ord_id: Some("O-PLAIN".to_string()),
1030            time_in_force: None,
1031            expire_time: None,
1032            post_only: None,
1033            reduce_only: None,
1034            leverage: None,
1035            trigger: None,
1036        };
1037        let value: serde_json::Value =
1038            serde_json::from_str(&serde_json::to_string(&order).expect("serialize")).expect("json");
1039
1040        assert!(
1041            value.get("leverage").is_none(),
1042            "leverage must be omitted when None"
1043        );
1044        assert!(
1045            value.get("trigger").is_none(),
1046            "trigger must be omitted when None"
1047        );
1048    }
1049
1050    #[rstest]
1051    fn test_deserialize_add_order_response_success() {
1052        let data = load_test_data("ws_add_order_response_success.json");
1053        let response: KrakenWsOrderResponse =
1054            serde_json::from_str(&data).expect("Failed to parse add_order success response");
1055
1056        assert_eq!(response.method, KrakenWsMethod::AddOrder);
1057        assert_eq!(response.req_id, Some(42));
1058        assert!(response.success);
1059        assert!(response.error.is_none());
1060
1061        let result = response.result.expect("Expected result");
1062        assert_eq!(result.order_id.as_deref(), Some("OABCDE-12345-FGHIJ"));
1063        assert_eq!(result.cl_ord_id.as_deref(), Some("O-20260505-000001"));
1064        assert_eq!(result.order_userref, Some(0));
1065    }
1066
1067    #[rstest]
1068    fn test_deserialize_add_order_response_failure() {
1069        let data = load_test_data("ws_add_order_response_failure.json");
1070        let response: KrakenWsOrderResponse =
1071            serde_json::from_str(&data).expect("Failed to parse add_order failure response");
1072
1073        assert_eq!(response.method, KrakenWsMethod::AddOrder);
1074        assert_eq!(response.req_id, Some(99));
1075        assert!(!response.success);
1076        assert_eq!(response.error.as_deref(), Some("EOrder:Insufficient funds"));
1077        assert!(response.result.is_none());
1078    }
1079
1080    #[rstest]
1081    fn test_deserialize_batch_add_response_partial() {
1082        let data = load_test_data("ws_batch_add_response_partial.json");
1083        let response: KrakenWsOrderResponse =
1084            serde_json::from_str(&data).expect("Failed to parse batch_add partial response");
1085
1086        assert_eq!(response.method, KrakenWsMethod::BatchAdd);
1087        assert_eq!(response.req_id, Some(45));
1088        assert!(response.success);
1089
1090        let result = response.result.expect("Expected result");
1091        let orders = result.orders.expect("Expected orders");
1092        assert_eq!(orders.len(), 2);
1093
1094        assert!(orders[0].success);
1095        assert_eq!(orders[0].order_id.as_deref(), Some("O1"));
1096        assert_eq!(orders[0].cl_ord_id.as_deref(), Some("O-A"));
1097        assert!(orders[0].error.is_none());
1098
1099        assert!(!orders[1].success);
1100        assert!(orders[1].order_id.is_none());
1101        assert_eq!(orders[1].cl_ord_id.as_deref(), Some("O-B"));
1102        assert_eq!(orders[1].error.as_deref(), Some("EOrder:Invalid price"));
1103    }
1104}