Skip to main content

kraken_api_client/futures/ws/
messages.rs

1//! Futures WebSocket message types.
2//!
3//! The Futures WebSocket API uses a different message format than Spot:
4//! - Subscriptions use `event` field instead of `method`
5//! - Feeds use `feed` field instead of `channel`
6//! - Authentication uses challenge/response instead of tokens
7
8use rust_decimal::Decimal;
9use serde::{Deserialize, Serialize};
10
11
12// Request Messages
13
14
15/// Challenge request for authentication.
16#[derive(Debug, Clone, Serialize)]
17pub struct ChallengeRequest {
18    /// Event type (always "challenge").
19    pub event: &'static str,
20    /// API key.
21    pub api_key: String,
22}
23
24impl ChallengeRequest {
25    /// Create a new challenge request.
26    pub fn new(api_key: impl Into<String>) -> Self {
27        Self {
28            event: "challenge",
29            api_key: api_key.into(),
30        }
31    }
32}
33
34/// Subscribe request for a public feed.
35#[derive(Debug, Clone, Serialize)]
36pub struct SubscribeRequest {
37    /// Event type (always "subscribe").
38    pub event: &'static str,
39    /// Feed name.
40    pub feed: String,
41    /// Product IDs to subscribe to.
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub product_ids: Option<Vec<String>>,
44}
45
46impl SubscribeRequest {
47    /// Create a new public subscription request.
48    pub fn public(feed: impl Into<String>, product_ids: Vec<String>) -> Self {
49        Self {
50            event: "subscribe",
51            feed: feed.into(),
52            product_ids: if product_ids.is_empty() {
53                None
54            } else {
55                Some(product_ids)
56            },
57        }
58    }
59
60    /// Create a new subscription for all products.
61    pub fn all(feed: impl Into<String>) -> Self {
62        Self {
63            event: "subscribe",
64            feed: feed.into(),
65            product_ids: None,
66        }
67    }
68}
69
70/// Subscribe request for a private feed (authenticated).
71#[derive(Debug, Clone, Serialize)]
72pub struct PrivateSubscribeRequest {
73    /// Event type (always "subscribe").
74    pub event: &'static str,
75    /// Feed name.
76    pub feed: String,
77    /// Original challenge (from server).
78    pub original_challenge: String,
79    /// Signed challenge (HMAC-SHA512 of SHA256 hash).
80    pub signed_challenge: String,
81    /// Product IDs to subscribe to (optional for private feeds).
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub product_ids: Option<Vec<String>>,
84}
85
86impl PrivateSubscribeRequest {
87    /// Create a new private subscription request.
88    pub fn new(
89        feed: impl Into<String>,
90        original_challenge: String,
91        signed_challenge: String,
92    ) -> Self {
93        Self {
94            event: "subscribe",
95            feed: feed.into(),
96            original_challenge,
97            signed_challenge,
98            product_ids: None,
99        }
100    }
101
102    /// Add product IDs filter.
103    pub fn with_product_ids(mut self, product_ids: Vec<String>) -> Self {
104        self.product_ids = Some(product_ids);
105        self
106    }
107}
108
109/// Unsubscribe request.
110#[derive(Debug, Clone, Serialize)]
111pub struct UnsubscribeRequest {
112    /// Event type (always "unsubscribe").
113    pub event: &'static str,
114    /// Feed name.
115    pub feed: String,
116    /// Product IDs to unsubscribe from.
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub product_ids: Option<Vec<String>>,
119}
120
121impl UnsubscribeRequest {
122    /// Create a new unsubscription request.
123    pub fn new(feed: impl Into<String>, product_ids: Vec<String>) -> Self {
124        Self {
125            event: "unsubscribe",
126            feed: feed.into(),
127            product_ids: if product_ids.is_empty() {
128                None
129            } else {
130                Some(product_ids)
131            },
132        }
133    }
134}
135
136
137// Response Messages
138
139
140/// Challenge response from the server.
141#[derive(Debug, Clone, Deserialize)]
142pub struct ChallengeResponse {
143    /// Event type (should be "challenge").
144    pub event: String,
145    /// The challenge message (UUID to sign).
146    pub message: String,
147}
148
149/// Subscription confirmation.
150#[derive(Debug, Clone, Deserialize)]
151pub struct SubscribedResponse {
152    /// Event type (should be "subscribed").
153    pub event: String,
154    /// Feed name.
155    pub feed: String,
156    /// Product IDs subscribed to.
157    #[serde(default)]
158    pub product_ids: Option<Vec<String>>,
159}
160
161/// Unsubscription confirmation.
162#[derive(Debug, Clone, Deserialize)]
163pub struct UnsubscribedResponse {
164    /// Event type (should be "unsubscribed").
165    pub event: String,
166    /// Feed name.
167    pub feed: String,
168    /// Product IDs unsubscribed from.
169    #[serde(default)]
170    pub product_ids: Option<Vec<String>>,
171}
172
173/// Error response from the server.
174#[derive(Debug, Clone, Deserialize)]
175pub struct ErrorResponse {
176    /// Event type (should be "error").
177    pub event: String,
178    /// Error message.
179    pub message: String,
180}
181
182/// Info/alert response from the server.
183#[derive(Debug, Clone, Deserialize)]
184pub struct InfoResponse {
185    /// Event type (should be "info" or "alert").
186    pub event: String,
187    /// Info/alert message.
188    pub message: String,
189    /// Version info (optional).
190    #[serde(default)]
191    pub version: Option<String>,
192}
193
194
195// Feed Data Messages
196
197
198/// Order book update message.
199#[derive(Debug, Clone, Deserialize)]
200pub struct BookMessage {
201    /// Feed name.
202    pub feed: String,
203    /// Product ID.
204    pub product_id: String,
205    /// Sequence number.
206    #[serde(default)]
207    pub seq: Option<u64>,
208    /// Timestamp in milliseconds.
209    #[serde(default)]
210    pub timestamp: Option<u64>,
211    /// Bids (price levels).
212    #[serde(default)]
213    pub bids: Vec<BookLevel>,
214    /// Asks (price levels).
215    #[serde(default)]
216    pub asks: Vec<BookLevel>,
217}
218
219/// Order book snapshot message.
220#[derive(Debug, Clone, Deserialize)]
221pub struct BookSnapshotMessage {
222    /// Feed name.
223    pub feed: String,
224    /// Product ID.
225    pub product_id: String,
226    /// Sequence number.
227    #[serde(default)]
228    pub seq: Option<u64>,
229    /// Timestamp in milliseconds.
230    #[serde(default)]
231    pub timestamp: Option<u64>,
232    /// Bids (price levels).
233    #[serde(default)]
234    pub bids: Vec<BookLevel>,
235    /// Asks (price levels).
236    #[serde(default)]
237    pub asks: Vec<BookLevel>,
238}
239
240/// A price level in the order book.
241#[derive(Debug, Clone, Deserialize)]
242pub struct BookLevel {
243    /// Price.
244    pub price: Decimal,
245    /// Quantity.
246    pub qty: Decimal,
247}
248
249/// Ticker message.
250#[derive(Debug, Clone, Deserialize)]
251pub struct TickerMessage {
252    /// Feed name.
253    pub feed: String,
254    /// Product ID.
255    pub product_id: String,
256    /// Timestamp in milliseconds.
257    #[serde(default)]
258    pub time: Option<u64>,
259    /// Best bid price.
260    #[serde(default)]
261    pub bid: Option<Decimal>,
262    /// Best bid size.
263    #[serde(default)]
264    pub bid_size: Option<Decimal>,
265    /// Best ask price.
266    #[serde(default)]
267    pub ask: Option<Decimal>,
268    /// Best ask size.
269    #[serde(default)]
270    pub ask_size: Option<Decimal>,
271    /// Last trade price.
272    #[serde(default)]
273    pub last: Option<Decimal>,
274    /// Last trade size.
275    #[serde(default)]
276    pub last_size: Option<Decimal>,
277    /// 24h volume.
278    #[serde(default)]
279    pub volume: Option<Decimal>,
280    /// Mark price.
281    #[serde(default, rename = "markPrice")]
282    pub mark_price: Option<Decimal>,
283    /// Open interest.
284    #[serde(default, rename = "openInterest")]
285    pub open_interest: Option<Decimal>,
286    /// Funding rate.
287    #[serde(default)]
288    pub funding_rate: Option<Decimal>,
289    /// Funding rate prediction.
290    #[serde(default)]
291    pub funding_rate_prediction: Option<Decimal>,
292    /// Change in last 24h (percentage).
293    #[serde(default)]
294    pub change: Option<Decimal>,
295    /// Premium.
296    #[serde(default)]
297    pub premium: Option<Decimal>,
298    /// Index price.
299    #[serde(default)]
300    pub index: Option<Decimal>,
301    /// Post only flag.
302    #[serde(default)]
303    pub post_only: Option<bool>,
304    /// Suspended flag.
305    #[serde(default)]
306    pub suspended: Option<bool>,
307}
308
309/// Trade message.
310#[derive(Debug, Clone, Deserialize)]
311pub struct TradeMessage {
312    /// Feed name.
313    pub feed: String,
314    /// Product ID.
315    pub product_id: String,
316    /// Trade ID.
317    #[serde(default)]
318    pub uid: Option<String>,
319    /// Trade side ("buy" or "sell").
320    #[serde(default)]
321    pub side: Option<String>,
322    /// Trade type.
323    #[serde(rename = "type", default)]
324    pub trade_type: Option<String>,
325    /// Trade price.
326    #[serde(default)]
327    pub price: Option<Decimal>,
328    /// Trade quantity.
329    #[serde(default)]
330    pub qty: Option<Decimal>,
331    /// Trade time in milliseconds.
332    #[serde(default)]
333    pub time: Option<u64>,
334    /// Sequence number.
335    #[serde(default)]
336    pub seq: Option<u64>,
337}
338
339/// Trades snapshot message.
340#[derive(Debug, Clone, Deserialize)]
341pub struct TradesSnapshotMessage {
342    /// Feed name.
343    pub feed: String,
344    /// Product ID.
345    pub product_id: String,
346    /// List of recent trades.
347    pub trades: Vec<TradeItem>,
348}
349
350/// A single trade item in the snapshot.
351#[derive(Debug, Clone, Deserialize)]
352pub struct TradeItem {
353    /// Trade ID.
354    #[serde(default)]
355    pub uid: Option<String>,
356    /// Trade side.
357    pub side: String,
358    /// Trade type.
359    #[serde(rename = "type", default)]
360    pub trade_type: Option<String>,
361    /// Trade price.
362    pub price: Decimal,
363    /// Trade quantity.
364    pub qty: Decimal,
365    /// Trade time in milliseconds.
366    pub time: u64,
367    /// Sequence number.
368    #[serde(default)]
369    pub seq: Option<u64>,
370}
371
372
373// Private Feed Messages
374
375
376/// Open orders message.
377#[derive(Debug, Clone, Deserialize)]
378pub struct OpenOrdersMessage {
379    /// Feed name.
380    pub feed: String,
381    /// List of open orders (for snapshot).
382    #[serde(default)]
383    pub orders: Option<Vec<WsOrder>>,
384    /// Single order update.
385    #[serde(flatten)]
386    pub order: Option<WsOrder>,
387}
388
389/// Order data from WebSocket.
390#[derive(Debug, Clone, Deserialize)]
391pub struct WsOrder {
392    /// Order ID.
393    #[serde(default)]
394    pub order_id: Option<String>,
395    /// Client order ID.
396    #[serde(default)]
397    pub cli_ord_id: Option<String>,
398    /// Instrument/symbol.
399    #[serde(default)]
400    pub instrument: Option<String>,
401    /// Order side ("buy" or "sell").
402    #[serde(default)]
403    pub side: Option<String>,
404    /// Order type ("lmt", "mkt", "stp", "take_profit").
405    #[serde(default)]
406    pub order_type: Option<String>,
407    /// Limit price.
408    #[serde(default)]
409    pub limit_price: Option<Decimal>,
410    /// Stop price.
411    #[serde(default)]
412    pub stop_price: Option<Decimal>,
413    /// Original quantity.
414    #[serde(default)]
415    pub qty: Option<Decimal>,
416    /// Filled quantity.
417    #[serde(default)]
418    pub filled: Option<Decimal>,
419    /// Reduce only flag.
420    #[serde(default)]
421    pub reduce_only: Option<bool>,
422    /// Timestamp.
423    #[serde(default)]
424    pub time: Option<u64>,
425    /// Last update timestamp.
426    #[serde(default)]
427    pub last_update_time: Option<u64>,
428    /// Order status.
429    #[serde(default)]
430    pub status: Option<String>,
431    /// Reason (for cancellation).
432    #[serde(default)]
433    pub reason: Option<String>,
434}
435
436/// Fills message.
437#[derive(Debug, Clone, Deserialize)]
438pub struct FillsMessage {
439    /// Feed name.
440    pub feed: String,
441    /// List of fills (for snapshot).
442    #[serde(default)]
443    pub fills: Option<Vec<WsFill>>,
444    /// Single fill update (for realtime).
445    #[serde(flatten)]
446    pub fill: Option<WsFill>,
447}
448
449/// Fill data from WebSocket.
450#[derive(Debug, Clone, Deserialize)]
451pub struct WsFill {
452    /// Fill ID.
453    #[serde(default)]
454    pub fill_id: Option<String>,
455    /// Order ID.
456    #[serde(default)]
457    pub order_id: Option<String>,
458    /// Client order ID.
459    #[serde(default)]
460    pub cli_ord_id: Option<String>,
461    /// Instrument/symbol.
462    #[serde(default)]
463    pub instrument: Option<String>,
464    /// Fill side ("buy" or "sell").
465    #[serde(default)]
466    pub side: Option<String>,
467    /// Fill price.
468    #[serde(default)]
469    pub price: Option<Decimal>,
470    /// Fill quantity.
471    #[serde(default)]
472    pub qty: Option<Decimal>,
473    /// Fill type ("maker", "taker", "liquidation").
474    #[serde(default)]
475    pub fill_type: Option<String>,
476    /// Fee paid.
477    #[serde(default)]
478    pub fee_paid: Option<Decimal>,
479    /// Fee currency.
480    #[serde(default)]
481    pub fee_currency: Option<String>,
482    /// Timestamp.
483    #[serde(default)]
484    pub time: Option<u64>,
485}
486
487/// Open positions message.
488#[derive(Debug, Clone, Deserialize)]
489pub struct OpenPositionsMessage {
490    /// Feed name.
491    pub feed: String,
492    /// Account type.
493    #[serde(default)]
494    pub account: Option<String>,
495    /// List of positions (for snapshot).
496    #[serde(default)]
497    pub positions: Option<Vec<WsPosition>>,
498    /// Single position update.
499    #[serde(flatten)]
500    pub position: Option<WsPosition>,
501}
502
503/// Position data from WebSocket.
504#[derive(Debug, Clone, Deserialize)]
505pub struct WsPosition {
506    /// Instrument/symbol.
507    #[serde(default)]
508    pub instrument: Option<String>,
509    /// Position balance (positive = long, negative = short).
510    #[serde(default)]
511    pub balance: Option<Decimal>,
512    /// Entry price.
513    #[serde(default)]
514    pub entry_price: Option<Decimal>,
515    /// Mark price.
516    #[serde(default)]
517    pub mark_price: Option<Decimal>,
518    /// Index price.
519    #[serde(default)]
520    pub index_price: Option<Decimal>,
521    /// PnL (unrealized).
522    #[serde(default)]
523    pub pnl: Option<Decimal>,
524    /// Effective leverage.
525    #[serde(default)]
526    pub effective_leverage: Option<Decimal>,
527    /// Initial margin.
528    #[serde(default)]
529    pub initial_margin: Option<Decimal>,
530    /// Maintenance margin.
531    #[serde(default)]
532    pub maintenance_margin: Option<Decimal>,
533    /// Return on equity.
534    #[serde(default)]
535    pub return_on_equity: Option<Decimal>,
536}
537
538/// Balances message.
539#[derive(Debug, Clone, Deserialize)]
540pub struct BalancesMessage {
541    /// Feed name.
542    pub feed: String,
543    /// Account type.
544    #[serde(default)]
545    pub account: Option<String>,
546    /// Sequence number.
547    #[serde(default)]
548    pub seq: Option<u64>,
549    /// Total balance.
550    #[serde(default)]
551    pub balance: Option<Decimal>,
552    /// Available balance.
553    #[serde(default)]
554    pub available: Option<Decimal>,
555    /// Margin used.
556    #[serde(default)]
557    pub margin: Option<Decimal>,
558    /// PnL.
559    #[serde(default)]
560    pub pnl: Option<Decimal>,
561    /// Collateral balances (for flex/multi-collateral accounts).
562    #[serde(default)]
563    pub flex_futures: Option<FlexFuturesBalance>,
564}
565
566/// Flex/Multi-collateral account balance.
567#[derive(Debug, Clone, Deserialize)]
568pub struct FlexFuturesBalance {
569    /// Currencies and their balances.
570    #[serde(default)]
571    pub currencies: Option<serde_json::Value>,
572    /// Portfolio value.
573    #[serde(default)]
574    pub portfolio_value: Option<Decimal>,
575    /// Available margin.
576    #[serde(default)]
577    pub available_margin: Option<Decimal>,
578    /// Initial margin.
579    #[serde(default)]
580    pub initial_margin: Option<Decimal>,
581    /// Maintenance margin.
582    #[serde(default)]
583    pub maintenance_margin: Option<Decimal>,
584    /// Unrealized PnL.
585    #[serde(default)]
586    pub unrealized_pnl: Option<Decimal>,
587}
588
589
590// Tests
591
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596
597    #[test]
598    fn test_challenge_request_serialization() {
599        let req = ChallengeRequest::new("my_api_key");
600        let json = serde_json::to_string(&req).unwrap();
601        assert!(json.contains("\"event\":\"challenge\""));
602        assert!(json.contains("\"api_key\":\"my_api_key\""));
603    }
604
605    #[test]
606    fn test_subscribe_request_serialization() {
607        let req = SubscribeRequest::public("book", vec!["PI_XBTUSD".into()]);
608        let json = serde_json::to_string(&req).unwrap();
609        assert!(json.contains("\"event\":\"subscribe\""));
610        assert!(json.contains("\"feed\":\"book\""));
611        assert!(json.contains("\"product_ids\":[\"PI_XBTUSD\"]"));
612    }
613
614    #[test]
615    fn test_challenge_response_deserialization() {
616        let json = r#"{"event":"challenge","message":"123e4567-e89b-12d3-a456-426614174000"}"#;
617        let resp: ChallengeResponse = serde_json::from_str(json).unwrap();
618        assert_eq!(resp.event, "challenge");
619        assert_eq!(resp.message, "123e4567-e89b-12d3-a456-426614174000");
620    }
621
622    #[test]
623    fn test_book_message_deserialization() {
624        let json = r#"{
625            "feed": "book",
626            "product_id": "PI_XBTUSD",
627            "seq": 1234,
628            "timestamp": 1640000000000,
629            "bids": [{"price": "50000.0", "qty": "1.5"}],
630            "asks": [{"price": "50001.0", "qty": "2.0"}]
631        }"#;
632        let msg: BookMessage = serde_json::from_str(json).unwrap();
633        assert_eq!(msg.feed, "book");
634        assert_eq!(msg.product_id, "PI_XBTUSD");
635        assert_eq!(msg.seq, Some(1234));
636        assert_eq!(msg.bids.len(), 1);
637        assert_eq!(msg.asks.len(), 1);
638    }
639
640    #[test]
641    fn test_ticker_message_deserialization() {
642        let json = r#"{
643            "feed": "ticker",
644            "product_id": "PI_XBTUSD",
645            "bid": "50000.0",
646            "ask": "50001.0",
647            "last": "50000.5",
648            "volume": "1000.0",
649            "funding_rate": "0.0001"
650        }"#;
651        let msg: TickerMessage = serde_json::from_str(json).unwrap();
652        assert_eq!(msg.feed, "ticker");
653        assert_eq!(msg.product_id, "PI_XBTUSD");
654        assert!(msg.bid.is_some());
655        assert!(msg.ask.is_some());
656    }
657
658    #[test]
659    fn test_private_subscribe_request() {
660        let req = PrivateSubscribeRequest::new(
661            "open_orders",
662            "challenge-uuid".to_string(),
663            "signed-challenge".to_string(),
664        );
665        let json = serde_json::to_string(&req).unwrap();
666        assert!(json.contains("\"event\":\"subscribe\""));
667        assert!(json.contains("\"feed\":\"open_orders\""));
668        assert!(json.contains("\"original_challenge\":\"challenge-uuid\""));
669        assert!(json.contains("\"signed_challenge\":\"signed-challenge\""));
670    }
671}