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 nautilus_model::{
21    data::{Data, OrderBookDeltas},
22    events::AccountState,
23    reports::{FillReport, OrderStatusReport, PositionStatusReport},
24};
25use serde::{Deserialize, Serialize};
26use serde_json::Value;
27
28use crate::{
29    schemas::ws::DydxWsMessageType,
30    websocket::{
31        enums::{DydxWsChannel, DydxWsOperation},
32        error::DydxWebSocketError,
33    },
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/// High level message emitted by the dYdX WebSocket client.
54#[derive(Debug, Clone)]
55pub enum DydxWsMessage {
56    /// Subscription acknowledgement.
57    Subscribed(DydxWsSubscriptionMsg),
58    /// Unsubscription acknowledgement.
59    Unsubscribed(DydxWsSubscriptionMsg),
60    /// Subaccounts subscription with initial account state.
61    SubaccountsSubscribed(crate::schemas::ws::DydxWsSubaccountsSubscribed),
62    /// Connected acknowledgement with connection_id.
63    Connected(DydxWsConnectedMsg),
64    /// Channel data update.
65    ChannelData(DydxWsChannelDataMsg),
66    /// Batch of channel data updates.
67    ChannelBatchData(DydxWsChannelBatchDataMsg),
68    /// Error received from the venue or client lifecycle.
69    Error(DydxWebSocketError),
70    /// Raw message payload that does not yet have a typed representation.
71    Raw(Value),
72    /// Notification that the underlying connection reconnected.
73    Reconnected,
74    /// Explicit pong event (text-based heartbeat acknowledgement).
75    Pong,
76}
77
78/// Nautilus domain message emitted after parsing dYdX WebSocket events.
79///
80/// This enum contains fully-parsed Nautilus domain objects ready for consumption
81/// by the Python layer without additional processing.
82#[derive(Debug, Clone)]
83pub enum NautilusWsMessage {
84    /// Market data (trades, quotes, bars).
85    Data(Vec<Data>),
86    /// Order book deltas.
87    Deltas(Box<OrderBookDeltas>),
88    /// Order status reports from subaccount stream.
89    Order(Box<OrderStatusReport>),
90    /// Fill reports from subaccount stream.
91    Fill(Box<FillReport>),
92    /// Position status reports from subaccount stream.
93    Position(Box<PositionStatusReport>),
94    /// Account state updates from subaccount stream.
95    AccountState(Box<AccountState>),
96    /// Raw subaccount subscription with full state (for execution client parsing).
97    SubaccountSubscribed(Box<crate::schemas::ws::DydxWsSubaccountsSubscribed>),
98    /// Raw subaccounts channel data (orders/fills) for execution client parsing.
99    SubaccountsChannelData(Box<crate::schemas::ws::DydxWsSubaccountsChannelData>),
100    /// Oracle price updates from markets channel (for execution client).
101    OraclePrices(HashMap<String, crate::websocket::types::DydxOraclePriceMarket>),
102    /// Error message.
103    Error(DydxWebSocketError),
104    /// Reconnection notification.
105    Reconnected,
106}
107
108/// Generic subscription/unsubscription confirmation message.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct DydxWsSubscriptionMsg {
111    /// The message type ("subscribed" or "unsubscribed").
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}
124
125/// Connection established message.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct DydxWsConnectedMsg {
128    /// The message type ("connected").
129    #[serde(rename = "type")]
130    pub msg_type: DydxWsMessageType,
131    /// The connection ID assigned by the server.
132    pub connection_id: String,
133    /// The message sequence number.
134    pub message_id: u64,
135}
136
137/// Single channel data update message.
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct DydxWsChannelDataMsg {
140    /// The message type ("channel_data").
141    #[serde(rename = "type")]
142    pub msg_type: DydxWsMessageType,
143    /// The connection ID.
144    pub connection_id: String,
145    /// The message sequence number.
146    pub message_id: u64,
147    /// The channel name.
148    pub channel: DydxWsChannel,
149    /// Optional channel-specific identifier.
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub id: Option<String>,
152    /// The payload data (format depends on channel).
153    pub contents: Value,
154    /// API version.
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub version: Option<String>,
157}
158
159/// Batch channel data update message (multiple updates in one message).
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct DydxWsChannelBatchDataMsg {
162    /// The message type ("channel_batch_data").
163    #[serde(rename = "type")]
164    pub msg_type: DydxWsMessageType,
165    /// The connection ID.
166    pub connection_id: String,
167    /// The message sequence number.
168    pub message_id: u64,
169    /// The channel name.
170    pub channel: DydxWsChannel,
171    /// Optional channel-specific identifier.
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub id: Option<String>,
174    /// Array of payload data.
175    pub contents: Value,
176    /// API version.
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub version: Option<String>,
179}
180
181/// Generic message structure for initial classification.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct DydxWsGenericMsg {
184    /// The message type.
185    #[serde(rename = "type")]
186    pub msg_type: DydxWsMessageType,
187    /// Optional connection ID.
188    #[serde(default, skip_serializing_if = "Option::is_none")]
189    pub connection_id: Option<String>,
190    /// Optional message sequence number.
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub message_id: Option<u64>,
193    /// Optional channel name.
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub channel: Option<DydxWsChannel>,
196    /// Optional channel-specific identifier.
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub id: Option<String>,
199    /// Optional error message.
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub message: Option<String>,
202}
203
204impl DydxWsGenericMsg {
205    /// Returns `true` if this message is an error.
206    #[must_use]
207    pub fn is_error(&self) -> bool {
208        self.msg_type == DydxWsMessageType::Error
209    }
210
211    /// Returns `true` if this message is a subscription confirmation.
212    #[must_use]
213    pub fn is_subscribed(&self) -> bool {
214        self.msg_type == DydxWsMessageType::Subscribed
215    }
216
217    /// Returns `true` if this message is an unsubscription confirmation.
218    #[must_use]
219    pub fn is_unsubscribed(&self) -> bool {
220        self.msg_type == DydxWsMessageType::Unsubscribed
221    }
222
223    /// Returns `true` if this message is a connection notification.
224    #[must_use]
225    pub fn is_connected(&self) -> bool {
226        self.msg_type == DydxWsMessageType::Connected
227    }
228
229    /// Returns `true` if this message is channel data.
230    #[must_use]
231    pub fn is_channel_data(&self) -> bool {
232        self.msg_type == DydxWsMessageType::ChannelData
233    }
234
235    /// Returns `true` if this message is batch channel data.
236    #[must_use]
237    pub fn is_channel_batch_data(&self) -> bool {
238        self.msg_type == DydxWsMessageType::ChannelBatchData
239    }
240}
241
242// ================================================================================================
243// Channel content types
244// ================================================================================================
245
246use chrono::{DateTime, Utc};
247use nautilus_model::enums::OrderSide;
248
249/// Trade message from v4_trades channel.
250#[derive(Debug, Clone, Serialize, Deserialize)]
251#[serde(rename_all = "camelCase")]
252pub struct DydxTrade {
253    /// Trade ID.
254    pub id: String,
255    /// Order side (BUY/SELL).
256    pub side: OrderSide,
257    /// Trade size.
258    pub size: String,
259    /// Trade price.
260    pub price: String,
261    /// Trade timestamp.
262    pub created_at: DateTime<Utc>,
263    /// Order type.
264    #[serde(rename = "type")]
265    pub order_type: String,
266    /// Block height (optional).
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub created_at_height: Option<String>,
269}
270
271/// Contents of v4_trades channel_data message.
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct DydxTradeContents {
274    /// Array of trades.
275    pub trades: Vec<DydxTrade>,
276}
277
278/// Candle/bar data from v4_candles channel.
279#[derive(Debug, Clone, Serialize, Deserialize)]
280#[serde(rename_all = "camelCase")]
281pub struct DydxCandle {
282    /// Base token volume.
283    pub base_token_volume: String,
284    /// Close price.
285    pub close: String,
286    /// High price.
287    pub high: String,
288    /// Low price.
289    pub low: String,
290    /// Open price.
291    pub open: String,
292    /// Resolution/timeframe.
293    pub resolution: String,
294    /// Start time.
295    pub started_at: DateTime<Utc>,
296    /// Starting open interest.
297    pub starting_open_interest: String,
298    /// Market ticker.
299    pub ticker: String,
300    /// Number of trades.
301    pub trades: i64,
302    /// USD volume.
303    pub usd_volume: String,
304    /// Orderbook mid price at close (optional).
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub orderbook_mid_price_close: Option<String>,
307    /// Orderbook mid price at open (optional).
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub orderbook_mid_price_open: Option<String>,
310}
311
312/// Order book price level (price, size tuple).
313pub type PriceLevel = (String, String);
314
315/// Contents of v4_orderbook channel_data/channel_batch_data messages.
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct DydxOrderbookContents {
318    /// Bid price levels.
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub bids: Option<Vec<PriceLevel>>,
321    /// Ask price levels.
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub asks: Option<Vec<PriceLevel>>,
324}
325
326/// Price level for orderbook snapshot (structured format).
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct DydxPriceLevel {
329    /// Price.
330    pub price: String,
331    /// Size.
332    pub size: String,
333}
334
335/// Contents of v4_orderbook subscribed (snapshot) message.
336#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct DydxOrderbookSnapshotContents {
338    /// Bid price levels.
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub bids: Option<Vec<DydxPriceLevel>>,
341    /// Ask price levels.
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub asks: Option<Vec<DydxPriceLevel>>,
344}
345
346/// Oracle price data for a market.
347#[derive(Debug, Clone, Serialize, Deserialize)]
348#[serde(rename_all = "camelCase")]
349pub struct DydxOraclePriceMarket {
350    /// Oracle price.
351    pub oracle_price: String,
352}
353
354/// Contents of v4_markets channel_data message.
355#[derive(Debug, Clone, Serialize, Deserialize)]
356#[serde(rename_all = "camelCase")]
357pub struct DydxMarketsContents {
358    /// Oracle prices by market symbol.
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub oracle_prices: Option<HashMap<String, DydxOraclePriceMarket>>,
361}