nautilus_dydx/websocket/
messages.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//! WebSocket message types for dYdX public and private channels.
17
18use std::collections::HashMap;
19
20use chrono::{DateTime, Utc};
21use nautilus_model::enums::{OrderSide, PositionSide};
22use serde::{Deserialize, Serialize};
23use serde_json::Value;
24use ustr::Ustr;
25
26use super::enums::{DydxWsChannel, DydxWsMessageType, DydxWsOperation};
27use crate::common::enums::{
28    DydxCandleResolution, DydxFillType, DydxLiquidity, DydxOrderStatus, DydxOrderType,
29    DydxPositionStatus, DydxTickerType, DydxTimeInForce, DydxTradeType,
30};
31
32// ------------------------------------------------------------------------------------------------
33// Subscription messages
34// ------------------------------------------------------------------------------------------------
35
36/// dYdX WebSocket subscription message.
37///
38/// # References
39///
40/// <https://docs.dydx.trade/developers/indexer/websockets>
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct DydxSubscription {
43    /// The operation type (subscribe/unsubscribe).
44    #[serde(rename = "type")]
45    pub op: DydxWsOperation,
46    /// The channel to subscribe to.
47    pub channel: DydxWsChannel,
48    /// Optional channel-specific identifier (e.g., market symbol).
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub id: Option<String>,
51}
52
53/// Generic subscription/unsubscription confirmation message.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct DydxWsSubscriptionMsg {
56    /// The message type ("subscribed" or "unsubscribed").
57    #[serde(rename = "type")]
58    pub msg_type: DydxWsMessageType,
59    /// The connection ID.
60    pub connection_id: String,
61    /// The message sequence number.
62    pub message_id: u64,
63    /// The channel name.
64    pub channel: DydxWsChannel,
65    /// Optional channel-specific identifier.
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub id: Option<String>,
68}
69
70/// Connection established message.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct DydxWsConnectedMsg {
73    /// The message type ("connected").
74    #[serde(rename = "type")]
75    pub msg_type: DydxWsMessageType,
76    /// The connection ID assigned by the server.
77    pub connection_id: String,
78    /// The message sequence number.
79    pub message_id: u64,
80}
81
82// ------------------------------------------------------------------------------------------------
83// Channel data messages
84// ------------------------------------------------------------------------------------------------
85
86/// Single channel data update message.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct DydxWsChannelDataMsg {
89    /// The message type ("channel_data").
90    #[serde(rename = "type")]
91    pub msg_type: DydxWsMessageType,
92    /// The connection ID.
93    pub connection_id: String,
94    /// The message sequence number.
95    pub message_id: u64,
96    /// The channel name.
97    pub channel: DydxWsChannel,
98    /// Optional channel-specific identifier.
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub id: Option<String>,
101    /// The payload data (format depends on channel).
102    pub contents: Value,
103    /// API version.
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub version: Option<String>,
106}
107
108/// Batch channel data update message (multiple updates in one message).
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct DydxWsChannelBatchDataMsg {
111    /// The message type ("channel_batch_data").
112    #[serde(rename = "type")]
113    pub msg_type: DydxWsMessageType,
114    /// The connection ID.
115    pub connection_id: String,
116    /// The message sequence number.
117    pub message_id: u64,
118    /// The channel name.
119    pub channel: DydxWsChannel,
120    /// Optional channel-specific identifier.
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub id: Option<String>,
123    /// Array of payload data.
124    pub contents: Value,
125    /// API version.
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub version: Option<String>,
128}
129
130/// General WebSocket message structure for routing.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct DydxWsMessageGeneral {
133    #[serde(rename = "type")]
134    pub msg_type: Option<DydxWsMessageType>,
135    pub connection_id: Option<String>,
136    pub message_id: Option<u64>,
137    pub channel: Option<DydxWsChannel>,
138    pub id: Option<String>,
139    pub message: Option<String>,
140}
141
142/// Generic message structure for initial classification.
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct DydxWsGenericMsg {
145    /// The message type.
146    #[serde(rename = "type")]
147    pub msg_type: DydxWsMessageType,
148    /// Optional connection ID.
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub connection_id: Option<String>,
151    /// Optional message sequence number.
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    pub message_id: Option<u64>,
154    /// Optional channel name.
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub channel: Option<DydxWsChannel>,
157    /// Optional channel-specific identifier.
158    #[serde(default, skip_serializing_if = "Option::is_none")]
159    pub id: Option<String>,
160    /// Optional error message.
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub message: Option<String>,
163}
164
165impl DydxWsGenericMsg {
166    /// Returns `true` if this message is an error.
167    #[must_use]
168    pub fn is_error(&self) -> bool {
169        self.msg_type == DydxWsMessageType::Error
170    }
171
172    /// Returns `true` if this message is a subscription confirmation.
173    #[must_use]
174    pub fn is_subscribed(&self) -> bool {
175        self.msg_type == DydxWsMessageType::Subscribed
176    }
177
178    /// Returns `true` if this message is an unsubscription confirmation.
179    #[must_use]
180    pub fn is_unsubscribed(&self) -> bool {
181        self.msg_type == DydxWsMessageType::Unsubscribed
182    }
183
184    /// Returns `true` if this message is a connection notification.
185    #[must_use]
186    pub fn is_connected(&self) -> bool {
187        self.msg_type == DydxWsMessageType::Connected
188    }
189
190    /// Returns `true` if this message is channel data.
191    #[must_use]
192    pub fn is_channel_data(&self) -> bool {
193        self.msg_type == DydxWsMessageType::ChannelData
194    }
195
196    /// Returns `true` if this message is batch channel data.
197    #[must_use]
198    pub fn is_channel_batch_data(&self) -> bool {
199        self.msg_type == DydxWsMessageType::ChannelBatchData
200    }
201
202    /// Returns `true` if this message is an unknown/unrecognized type.
203    #[must_use]
204    pub fn is_unknown(&self) -> bool {
205        self.msg_type == DydxWsMessageType::Unknown
206    }
207}
208
209// ------------------------------------------------------------------------------------------------
210// Block height channel
211// ------------------------------------------------------------------------------------------------
212
213/// Block height subscription confirmed contents.
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct DydxBlockHeightSubscribedContents {
216    pub height: String,
217    pub time: DateTime<Utc>,
218}
219
220/// Block height subscription confirmed message.
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct DydxWsBlockHeightSubscribedData {
223    #[serde(rename = "type")]
224    pub msg_type: DydxWsMessageType,
225    pub connection_id: String,
226    pub message_id: u64,
227    pub channel: DydxWsChannel,
228    pub id: String,
229    pub contents: DydxBlockHeightSubscribedContents,
230}
231
232/// Block height channel data contents.
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct DydxBlockHeightChannelContents {
235    #[serde(rename = "blockHeight")]
236    pub block_height: String,
237    pub time: DateTime<Utc>,
238}
239
240/// Block height channel data message.
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct DydxWsBlockHeightChannelData {
243    #[serde(rename = "type")]
244    pub msg_type: DydxWsMessageType,
245    pub connection_id: String,
246    pub message_id: u64,
247    pub id: String,
248    pub channel: DydxWsChannel,
249    pub version: String,
250    pub contents: DydxBlockHeightChannelContents,
251}
252
253// ------------------------------------------------------------------------------------------------
254// Markets channel
255// ------------------------------------------------------------------------------------------------
256
257/// Oracle price data for a market (full format from subscribed message).
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct DydxOraclePriceMarketFull {
260    #[serde(rename = "oraclePrice")]
261    pub oracle_price: String,
262    #[serde(rename = "effectiveAt")]
263    pub effective_at: String,
264    #[serde(rename = "effectiveAtHeight")]
265    pub effective_at_height: String,
266    #[serde(rename = "marketId")]
267    pub market_id: u32,
268}
269
270/// Oracle price data for a market (simple format from channel_data).
271#[derive(Debug, Clone, Serialize, Deserialize)]
272#[serde(rename_all = "camelCase")]
273pub struct DydxOraclePriceMarket {
274    /// Oracle price.
275    pub oracle_price: String,
276}
277
278/// Market message contents.
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct DydxMarketMessageContents {
281    #[serde(rename = "oraclePrices")]
282    pub oracle_prices: Option<HashMap<String, DydxOraclePriceMarketFull>>,
283    pub trading: Option<Value>,
284}
285
286/// Markets channel data message.
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct DydxWsMarketChannelData {
289    #[serde(rename = "type")]
290    pub msg_type: DydxWsMessageType,
291    pub channel: DydxWsChannel,
292    pub contents: DydxMarketMessageContents,
293    pub version: String,
294    pub message_id: u64,
295    pub connection_id: Option<String>,
296    pub id: Option<String>,
297}
298
299/// Markets subscription confirmed message.
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct DydxWsMarketSubscribed {
302    #[serde(rename = "type")]
303    pub msg_type: DydxWsMessageType,
304    pub connection_id: String,
305    pub message_id: u64,
306    pub channel: DydxWsChannel,
307    pub contents: Value,
308}
309
310/// Contents of v4_markets channel_data message (simple format).
311#[derive(Debug, Clone, Serialize, Deserialize)]
312#[serde(rename_all = "camelCase")]
313pub struct DydxMarketsContents {
314    /// Oracle prices by market symbol.
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub oracle_prices: Option<HashMap<String, DydxOraclePriceMarket>>,
317}
318
319// ------------------------------------------------------------------------------------------------
320// Trades channel
321// ------------------------------------------------------------------------------------------------
322
323/// Trade message from v4_trades channel.
324#[derive(Debug, Clone, Serialize, Deserialize)]
325#[serde(rename_all = "camelCase")]
326pub struct DydxTrade {
327    /// Trade ID.
328    pub id: String,
329    /// Order side (BUY/SELL).
330    pub side: OrderSide,
331    /// Trade size.
332    pub size: String,
333    /// Trade price.
334    pub price: String,
335    /// Trade timestamp.
336    pub created_at: DateTime<Utc>,
337    /// Trade type.
338    #[serde(rename = "type")]
339    pub trade_type: DydxTradeType,
340    /// Block height (optional).
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub created_at_height: Option<String>,
343}
344
345/// Contents of v4_trades channel_data message.
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct DydxTradeContents {
348    /// Array of trades.
349    pub trades: Vec<DydxTrade>,
350}
351
352// ------------------------------------------------------------------------------------------------
353// Candles channel
354// ------------------------------------------------------------------------------------------------
355
356/// Candle/bar data from v4_candles channel.
357#[derive(Debug, Clone, Serialize, Deserialize)]
358#[serde(rename_all = "camelCase")]
359pub struct DydxCandle {
360    /// Base token volume.
361    pub base_token_volume: String,
362    /// Close price.
363    pub close: String,
364    /// High price.
365    pub high: String,
366    /// Low price.
367    pub low: String,
368    /// Open price.
369    pub open: String,
370    /// Resolution/timeframe.
371    pub resolution: DydxCandleResolution,
372    /// Start time.
373    pub started_at: DateTime<Utc>,
374    /// Starting open interest.
375    pub starting_open_interest: String,
376    /// Market ticker.
377    pub ticker: String,
378    /// Number of trades.
379    pub trades: i64,
380    /// USD volume.
381    pub usd_volume: String,
382    /// Orderbook mid price at close (optional).
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub orderbook_mid_price_close: Option<String>,
385    /// Orderbook mid price at open (optional).
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub orderbook_mid_price_open: Option<String>,
388}
389
390// ------------------------------------------------------------------------------------------------
391// Orderbook channel
392// ------------------------------------------------------------------------------------------------
393
394/// Order book price level (price, size tuple).
395pub type PriceLevel = (String, String);
396
397/// Contents of v4_orderbook channel_data/channel_batch_data messages.
398#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct DydxOrderbookContents {
400    /// Bid price levels.
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub bids: Option<Vec<PriceLevel>>,
403    /// Ask price levels.
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub asks: Option<Vec<PriceLevel>>,
406}
407
408/// Price level for orderbook snapshot (structured format).
409#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct DydxPriceLevel {
411    /// Price.
412    pub price: String,
413    /// Size.
414    pub size: String,
415}
416
417/// Contents of v4_orderbook subscribed (snapshot) message.
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct DydxOrderbookSnapshotContents {
420    /// Bid price levels.
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub bids: Option<Vec<DydxPriceLevel>>,
423    /// Ask price levels.
424    #[serde(skip_serializing_if = "Option::is_none")]
425    pub asks: Option<Vec<DydxPriceLevel>>,
426}
427
428// ------------------------------------------------------------------------------------------------
429// Subaccounts channel
430// ------------------------------------------------------------------------------------------------
431
432/// Subaccount balance update.
433#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct DydxAssetBalance {
435    pub symbol: Ustr,
436    pub side: OrderSide,
437    pub size: String,
438    #[serde(rename = "assetId")]
439    pub asset_id: String,
440}
441
442/// Subaccount perpetual position.
443#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct DydxPerpetualPosition {
445    pub market: Ustr,
446    pub status: DydxPositionStatus,
447    pub side: PositionSide,
448    pub size: String,
449    #[serde(rename = "maxSize")]
450    pub max_size: String,
451    #[serde(rename = "entryPrice")]
452    pub entry_price: String,
453    #[serde(rename = "exitPrice")]
454    pub exit_price: Option<String>,
455    #[serde(rename = "realizedPnl")]
456    pub realized_pnl: String,
457    #[serde(rename = "unrealizedPnl")]
458    pub unrealized_pnl: String,
459    #[serde(rename = "createdAt")]
460    pub created_at: String,
461    #[serde(rename = "closedAt")]
462    pub closed_at: Option<String>,
463    #[serde(rename = "sumOpen")]
464    pub sum_open: String,
465    #[serde(rename = "sumClose")]
466    pub sum_close: String,
467    #[serde(rename = "netFunding")]
468    pub net_funding: String,
469}
470
471/// Subaccount information.
472#[derive(Debug, Clone, Serialize, Deserialize)]
473pub struct DydxSubaccountInfo {
474    pub address: String,
475    #[serde(rename = "subaccountNumber")]
476    pub subaccount_number: u32,
477    pub equity: String,
478    #[serde(rename = "freeCollateral")]
479    pub free_collateral: String,
480    #[serde(rename = "openPerpetualPositions")]
481    pub open_perpetual_positions: Option<HashMap<String, DydxPerpetualPosition>>,
482    #[serde(rename = "assetPositions")]
483    pub asset_positions: Option<HashMap<String, DydxAssetBalance>>,
484    #[serde(rename = "marginEnabled")]
485    pub margin_enabled: bool,
486    #[serde(rename = "updatedAtHeight")]
487    pub updated_at_height: String,
488    #[serde(rename = "latestProcessedBlockHeight")]
489    pub latest_processed_block_height: String,
490}
491
492/// Order message from WebSocket.
493#[derive(Debug, Clone, Serialize, Deserialize)]
494pub struct DydxWsOrderSubaccountMessageContents {
495    pub id: String,
496    #[serde(rename = "subaccountId")]
497    pub subaccount_id: String,
498    #[serde(rename = "clientId")]
499    pub client_id: String,
500    #[serde(rename = "clobPairId")]
501    pub clob_pair_id: String,
502    pub side: OrderSide,
503    pub size: String,
504    pub price: String,
505    pub status: DydxOrderStatus,
506    #[serde(rename = "type")]
507    pub order_type: DydxOrderType,
508    #[serde(rename = "timeInForce")]
509    pub time_in_force: DydxTimeInForce,
510    #[serde(rename = "postOnly")]
511    pub post_only: bool,
512    #[serde(rename = "reduceOnly")]
513    pub reduce_only: bool,
514    #[serde(rename = "orderFlags")]
515    pub order_flags: String,
516    #[serde(rename = "goodTilBlock")]
517    pub good_til_block: Option<String>,
518    #[serde(rename = "goodTilBlockTime")]
519    pub good_til_block_time: Option<String>,
520    #[serde(rename = "createdAtHeight")]
521    pub created_at_height: String,
522    #[serde(rename = "clientMetadata")]
523    pub client_metadata: String,
524    #[serde(rename = "triggerPrice")]
525    pub trigger_price: Option<String>,
526    #[serde(rename = "totalFilled")]
527    pub total_filled: String,
528    #[serde(rename = "updatedAt")]
529    pub updated_at: Option<String>,
530    #[serde(rename = "updatedAtHeight")]
531    pub updated_at_height: Option<String>,
532}
533
534/// Fill message from WebSocket.
535#[derive(Debug, Clone, Serialize, Deserialize)]
536pub struct DydxWsFillSubaccountMessageContents {
537    pub id: String,
538    #[serde(rename = "subaccountId")]
539    pub subaccount_id: String,
540    pub side: OrderSide,
541    pub liquidity: DydxLiquidity,
542    #[serde(rename = "type")]
543    pub fill_type: DydxFillType,
544    pub market: Ustr,
545    #[serde(rename = "marketType")]
546    pub market_type: DydxTickerType,
547    pub price: String,
548    pub size: String,
549    pub fee: String,
550    #[serde(rename = "createdAt")]
551    pub created_at: String,
552    #[serde(rename = "createdAtHeight")]
553    pub created_at_height: String,
554    #[serde(rename = "orderId")]
555    pub order_id: String,
556    #[serde(rename = "clientMetadata")]
557    pub client_metadata: String,
558}
559
560/// Subaccount subscription contents.
561#[derive(Debug, Clone, Serialize, Deserialize)]
562pub struct DydxWsSubaccountsSubscribedContents {
563    pub subaccount: DydxSubaccountInfo,
564}
565
566/// Subaccounts subscription confirmed message.
567#[derive(Debug, Clone, Serialize, Deserialize)]
568pub struct DydxWsSubaccountsSubscribed {
569    #[serde(rename = "type")]
570    pub msg_type: DydxWsMessageType,
571    pub connection_id: String,
572    pub message_id: u64,
573    pub channel: DydxWsChannel,
574    pub id: String,
575    pub contents: DydxWsSubaccountsSubscribedContents,
576}
577
578/// Subaccounts channel data contents.
579#[derive(Debug, Clone, Serialize, Deserialize)]
580pub struct DydxWsSubaccountsChannelContents {
581    pub orders: Option<Vec<DydxWsOrderSubaccountMessageContents>>,
582    pub fills: Option<Vec<DydxWsFillSubaccountMessageContents>>,
583}
584
585/// Subaccounts channel data message.
586#[derive(Debug, Clone, Serialize, Deserialize)]
587pub struct DydxWsSubaccountsChannelData {
588    #[serde(rename = "type")]
589    pub msg_type: DydxWsMessageType,
590    pub connection_id: String,
591    pub message_id: u64,
592    pub id: String,
593    pub channel: DydxWsChannel,
594    pub version: String,
595    pub contents: DydxWsSubaccountsChannelContents,
596}