nautilus_hyperliquid/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
16use std::collections::HashMap;
17
18use derive_builder::Builder;
19use nautilus_model::{
20    data::{
21        Bar, FundingRateUpdate, IndexPriceUpdate, MarkPriceUpdate, OrderBookDeltas, QuoteTick,
22        TradeTick,
23    },
24    reports::{FillReport, OrderStatusReport},
25};
26use serde::{Deserialize, Serialize};
27use ustr::Ustr;
28
29use crate::common::enums::{
30    HyperliquidBarInterval, HyperliquidFillDirection, HyperliquidLiquidationMethod,
31    HyperliquidOrderStatus as HyperliquidOrderStatusEnum, HyperliquidSide, HyperliquidTpSl,
32    HyperliquidTwapStatus,
33};
34
35/// Represents an outbound WebSocket message from client to Hyperliquid.
36#[derive(Debug, Clone, Serialize)]
37#[serde(tag = "method")]
38#[serde(rename_all = "lowercase")]
39pub enum HyperliquidWsRequest {
40    /// Subscribe to a data feed.
41    Subscribe {
42        /// Subscription details.
43        subscription: SubscriptionRequest,
44    },
45    /// Unsubscribe from a data feed.
46    Unsubscribe {
47        /// Subscription details to remove.
48        subscription: SubscriptionRequest,
49    },
50    /// Post a request (info or action).
51    Post {
52        /// Request ID for tracking.
53        id: u64,
54        /// Request payload.
55        request: PostRequest,
56    },
57    /// Ping for keepalive.
58    Ping,
59}
60
61/// Represents subscription request types for WebSocket feeds.
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
63#[serde(tag = "type")]
64#[serde(rename_all = "camelCase")]
65pub enum SubscriptionRequest {
66    /// All mid prices across markets.
67    AllMids {
68        #[serde(skip_serializing_if = "Option::is_none")]
69        dex: Option<String>,
70    },
71    /// Notifications for a user.
72    Notification { user: String },
73    /// Web data for frontend.
74    WebData2 { user: String },
75    /// Candlestick data.
76    Candle {
77        coin: Ustr,
78        interval: HyperliquidBarInterval,
79    },
80    /// Level 2 order book.
81    L2Book {
82        coin: Ustr,
83        #[serde(skip_serializing_if = "Option::is_none")]
84        #[serde(rename = "nSigFigs")]
85        n_sig_figs: Option<u32>,
86        #[serde(skip_serializing_if = "Option::is_none")]
87        mantissa: Option<u32>,
88    },
89    /// Trade updates.
90    Trades { coin: Ustr },
91    /// Order updates for a user.
92    OrderUpdates { user: String },
93    /// User events (fills, funding, liquidations).
94    UserEvents { user: String },
95    /// User fill history.
96    UserFills {
97        user: String,
98        #[serde(skip_serializing_if = "Option::is_none")]
99        #[serde(rename = "aggregateByTime")]
100        aggregate_by_time: Option<bool>,
101    },
102    /// User funding payments.
103    UserFundings { user: String },
104    /// User ledger updates (non-funding).
105    UserNonFundingLedgerUpdates { user: String },
106    /// Active asset context (for perpetuals).
107    ActiveAssetCtx { coin: Ustr },
108    /// Active spot asset context.
109    ActiveSpotAssetCtx { coin: Ustr },
110    /// Active asset data for user.
111    ActiveAssetData { user: String, coin: String },
112    /// TWAP slice fills.
113    UserTwapSliceFills { user: String },
114    /// TWAP history.
115    UserTwapHistory { user: String },
116    /// Best bid/offer updates.
117    Bbo { coin: Ustr },
118}
119
120/// Post request wrapper for info and action requests.
121#[derive(Debug, Clone, Serialize)]
122#[serde(tag = "type")]
123#[serde(rename_all = "lowercase")]
124pub enum PostRequest {
125    /// Info request (no signature required).
126    Info { payload: serde_json::Value },
127    /// Action request (requires signature).
128    Action { payload: ActionPayload },
129}
130
131/// Action payload with signature.
132#[derive(Debug, Clone, Serialize)]
133pub struct ActionPayload {
134    pub action: ActionRequest,
135    pub nonce: u64,
136    pub signature: SignatureData,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    #[serde(rename = "vaultAddress")]
139    pub vault_address: Option<String>,
140}
141
142/// Signature data.
143#[derive(Debug, Clone, Serialize)]
144pub struct SignatureData {
145    pub r: String,
146    pub s: String,
147    pub v: String,
148}
149
150/// Action request types.
151#[derive(Debug, Clone, Serialize)]
152#[serde(tag = "type")]
153#[serde(rename_all = "lowercase")]
154pub enum ActionRequest {
155    /// Place orders.
156    Order {
157        orders: Vec<OrderRequest>,
158        grouping: String,
159    },
160    /// Cancel orders.
161    Cancel { cancels: Vec<CancelRequest> },
162    /// Cancel orders by client order ID.
163    CancelByCloid { cancels: Vec<CancelByCloidRequest> },
164    /// Modify orders.
165    Modify { modifies: Vec<ModifyRequest> },
166}
167
168impl ActionRequest {
169    /// Create a simple order action with default "na" grouping
170    ///
171    /// # Example
172    /// ```ignore
173    /// let action = ActionRequest::order(vec![order1, order2], "na");
174    /// ```
175    pub fn order(orders: Vec<OrderRequest>, grouping: impl Into<String>) -> Self {
176        Self::Order {
177            orders,
178            grouping: grouping.into(),
179        }
180    }
181
182    /// Create a cancel action for multiple orders
183    ///
184    /// # Example
185    /// ```ignore
186    /// let action = ActionRequest::cancel(vec![
187    ///     CancelRequest { a: 0, o: 12345 },
188    ///     CancelRequest { a: 1, o: 67890 },
189    /// ]);
190    /// ```
191    pub fn cancel(cancels: Vec<CancelRequest>) -> Self {
192        Self::Cancel { cancels }
193    }
194
195    /// Create a cancel-by-cloid action
196    ///
197    /// # Example
198    /// ```ignore
199    /// let action = ActionRequest::cancel_by_cloid(vec![
200    ///     CancelByCloidRequest { asset: 0, cloid: "order-1".to_string() },
201    /// ]);
202    /// ```
203    pub fn cancel_by_cloid(cancels: Vec<CancelByCloidRequest>) -> Self {
204        Self::CancelByCloid { cancels }
205    }
206
207    /// Create a modify action for multiple orders
208    ///
209    /// # Example
210    /// ```ignore
211    /// let action = ActionRequest::modify(vec![
212    ///     ModifyRequest { oid: 12345, order: new_order },
213    /// ]);
214    /// ```
215    pub fn modify(modifies: Vec<ModifyRequest>) -> Self {
216        Self::Modify { modifies }
217    }
218}
219
220/// Order placement request.
221#[derive(Debug, Clone, Serialize, Builder)]
222pub struct OrderRequest {
223    /// Asset ID.
224    pub a: u32,
225    /// Buy side (true = buy, false = sell).
226    pub b: bool,
227    /// Price.
228    pub p: String,
229    /// Size.
230    pub s: String,
231    /// Reduce only.
232    pub r: bool,
233    /// Order type.
234    pub t: OrderTypeRequest,
235    /// Client order ID (optional).
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub c: Option<String>,
238}
239
240/// Order type in request format.
241#[derive(Debug, Clone, Serialize)]
242#[serde(tag = "type")]
243#[serde(rename_all = "lowercase")]
244pub enum OrderTypeRequest {
245    Limit {
246        tif: TimeInForceRequest,
247    },
248    Trigger {
249        #[serde(rename = "isMarket")]
250        is_market: bool,
251        #[serde(rename = "triggerPx")]
252        trigger_px: String,
253        tpsl: TpSlRequest,
254    },
255}
256
257/// Time in force in request format.
258#[derive(Debug, Clone, Serialize)]
259#[serde(rename_all = "PascalCase")]
260pub enum TimeInForceRequest {
261    Alo,
262    Ioc,
263    Gtc,
264}
265
266/// TP/SL in request format.
267#[derive(Debug, Clone, Serialize)]
268#[serde(rename_all = "lowercase")]
269pub enum TpSlRequest {
270    Tp,
271    Sl,
272}
273
274/// Cancel order request.
275#[derive(Debug, Clone, Serialize)]
276pub struct CancelRequest {
277    /// Asset ID.
278    pub a: u32,
279    /// Order ID.
280    pub o: u64,
281}
282
283/// Cancel by client order ID request.
284#[derive(Debug, Clone, Serialize)]
285pub struct CancelByCloidRequest {
286    /// Asset ID.
287    pub asset: u32,
288    /// Client order ID.
289    pub cloid: String,
290}
291
292/// Modify order request.
293#[derive(Debug, Clone, Serialize)]
294pub struct ModifyRequest {
295    /// Order ID.
296    pub oid: u64,
297    /// New order details.
298    pub order: OrderRequest,
299}
300
301/// Subscription response data wrapper.
302#[derive(Debug, Clone, Deserialize)]
303pub struct SubscriptionResponseData {
304    pub method: String,
305    pub subscription: SubscriptionRequest,
306}
307
308/// Inbound WebSocket message from Hyperliquid server.
309#[derive(Debug, Clone, Deserialize)]
310#[serde(tag = "channel")]
311#[serde(rename_all = "camelCase")]
312pub enum HyperliquidWsMessage {
313    /// Subscription confirmation.
314    SubscriptionResponse { data: SubscriptionResponseData },
315    /// Post request response.
316    Post { data: PostResponse },
317    /// All mid prices.
318    AllMids { data: AllMidsData },
319    /// Notifications.
320    Notification { data: NotificationData },
321    /// Web data.
322    WebData2 { data: serde_json::Value },
323    /// Candlestick data.
324    Candle { data: CandleData },
325    /// Level 2 order book.
326    L2Book { data: WsBookData },
327    /// Trade updates.
328    Trades { data: Vec<WsTradeData> },
329    /// Order updates.
330    OrderUpdates { data: Vec<WsOrderData> },
331    /// User events.
332    UserEvents { data: WsUserEventData },
333    /// User fills.
334    UserFills { data: WsUserFillsData },
335    /// User funding payments.
336    UserFundings { data: WsUserFundingsData },
337    /// User ledger updates.
338    UserNonFundingLedgerUpdates { data: serde_json::Value },
339    /// Active asset context.
340    ActiveAssetCtx { data: WsActiveAssetCtxData },
341    /// Active spot asset context (same data as ActiveAssetCtx, different channel name).
342    ActiveSpotAssetCtx { data: WsActiveAssetCtxData },
343    /// Active asset data.
344    ActiveAssetData { data: WsActiveAssetData },
345    /// TWAP slice fills.
346    UserTwapSliceFills { data: WsUserTwapSliceFillsData },
347    /// TWAP history.
348    UserTwapHistory { data: WsUserTwapHistoryData },
349    /// Best bid/offer.
350    Bbo { data: WsBboData },
351    /// Error response.
352    Error { data: String },
353    /// Pong response.
354    Pong,
355}
356
357/// Post response data.
358#[derive(Debug, Clone, Deserialize)]
359pub struct PostResponse {
360    pub id: u64,
361    pub response: PostResponsePayload,
362}
363
364/// Post response payload.
365#[derive(Debug, Clone, Deserialize)]
366#[serde(tag = "type")]
367#[serde(rename_all = "lowercase")]
368pub enum PostResponsePayload {
369    Info { payload: serde_json::Value },
370    Action { payload: serde_json::Value },
371    Error { payload: String },
372}
373
374/// All mid prices data.
375#[derive(Debug, Clone, Deserialize)]
376pub struct AllMidsData {
377    pub mids: HashMap<String, String>,
378}
379
380/// Notification data.
381#[derive(Debug, Clone, Deserialize)]
382pub struct NotificationData {
383    pub notification: String,
384}
385
386/// Candlestick data.
387#[derive(Debug, Clone, Deserialize)]
388pub struct CandleData {
389    /// Open time (millis).
390    pub t: u64,
391    /// Close time (millis).
392    #[serde(rename = "T")]
393    pub close_time: u64,
394    /// Symbol.
395    pub s: Ustr,
396    /// Interval.
397    pub i: String,
398    /// Open price.
399    pub o: String,
400    /// Close price.
401    pub c: String,
402    /// High price.
403    pub h: String,
404    /// Low price.
405    pub l: String,
406    /// Volume.
407    pub v: String,
408    /// Number of trades.
409    pub n: u32,
410}
411
412/// WebSocket book data.
413#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct WsBookData {
415    pub coin: Ustr,
416    pub levels: [Vec<WsLevelData>; 2], // [bids, asks]
417    pub time: u64,
418}
419
420/// WebSocket level data.
421#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct WsLevelData {
423    /// Price.
424    pub px: String,
425    /// Size.
426    pub sz: String,
427    /// Number of orders.
428    pub n: u32,
429}
430
431/// WebSocket trade data.
432#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct WsTradeData {
434    pub coin: Ustr,
435    pub side: HyperliquidSide,
436    pub px: String,
437    pub sz: String,
438    pub hash: String,
439    pub time: u64,
440    pub tid: u64,
441    pub users: [String; 2], // [buyer, seller]
442}
443
444/// WebSocket order data.
445#[derive(Debug, Clone, Deserialize)]
446pub struct WsOrderData {
447    pub order: WsBasicOrderData,
448    pub status: HyperliquidOrderStatusEnum,
449    #[serde(rename = "statusTimestamp")]
450    pub status_timestamp: u64,
451}
452
453/// Basic order data.
454#[derive(Debug, Clone, Deserialize)]
455pub struct WsBasicOrderData {
456    pub coin: Ustr,
457    pub side: HyperliquidSide,
458    #[serde(rename = "limitPx")]
459    pub limit_px: String,
460    pub sz: String,
461    pub oid: u64,
462    pub timestamp: u64,
463    #[serde(rename = "origSz")]
464    pub orig_sz: String,
465    pub cloid: Option<String>,
466    /// Trigger price for conditional orders (stop/take-profit).
467    #[serde(rename = "triggerPx")]
468    pub trigger_px: Option<String>,
469    /// Whether this is a market or limit trigger order.
470    #[serde(rename = "isMarket")]
471    pub is_market: Option<bool>,
472    /// Take-profit or stop-loss indicator.
473    pub tpsl: Option<HyperliquidTpSl>,
474    /// Whether the trigger has been activated.
475    #[serde(rename = "triggerActivated")]
476    pub trigger_activated: Option<bool>,
477    /// Trailing stop parameters if applicable.
478    #[serde(rename = "trailingStop")]
479    pub trailing_stop: Option<WsTrailingStopData>,
480}
481
482/// Trailing stop offset type.
483#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
484#[serde(rename_all = "camelCase")]
485pub enum TrailingOffsetType {
486    /// Price offset.
487    Price,
488    /// Percentage offset.
489    Percentage,
490    /// Basis points offset.
491    BasisPoints,
492}
493
494impl TrailingOffsetType {
495    /// Format the offset value with the appropriate unit.
496    pub fn format_offset(&self, offset: &str) -> String {
497        match self {
498            Self::Price => offset.to_string(),
499            Self::Percentage => format!("{offset}%"),
500            Self::BasisPoints => format!("{offset} bps"),
501        }
502    }
503}
504
505/// Trailing stop data from WebSocket.
506#[derive(Debug, Clone, Deserialize)]
507pub struct WsTrailingStopData {
508    /// Trailing offset value.
509    pub offset: String,
510    /// Offset type.
511    #[serde(rename = "offsetType")]
512    pub offset_type: TrailingOffsetType,
513    /// Current callback price (highest/lowest price reached).
514    #[serde(rename = "callbackPrice")]
515    pub callback_price: Option<String>,
516}
517
518/// WebSocket user event data.
519#[derive(Debug, Clone, Deserialize)]
520#[serde(untagged)]
521pub enum WsUserEventData {
522    Fills {
523        fills: Vec<WsFillData>,
524    },
525    Funding {
526        funding: WsUserFundingData,
527    },
528    Liquidation {
529        liquidation: WsLiquidationData,
530    },
531    NonUserCancel {
532        #[serde(rename = "nonUserCancel")]
533        non_user_cancel: Vec<WsNonUserCancelData>,
534    },
535    /// Trigger order activated (moved from pending to active).
536    TriggerActivated {
537        #[serde(rename = "triggerActivated")]
538        trigger_activated: WsTriggerActivatedData,
539    },
540    /// Trigger order executed (trigger price reached, order placed).
541    TriggerTriggered {
542        #[serde(rename = "triggerTriggered")]
543        trigger_triggered: WsTriggerTriggeredData,
544    },
545}
546
547/// WebSocket fill data.
548#[derive(Debug, Clone, Deserialize)]
549pub struct WsFillData {
550    pub coin: Ustr,
551    pub px: String,
552    pub sz: String,
553    pub side: HyperliquidSide,
554    pub time: u64,
555    #[serde(rename = "startPosition")]
556    pub start_position: String,
557    pub dir: HyperliquidFillDirection,
558    #[serde(rename = "closedPnl")]
559    pub closed_pnl: String,
560    pub hash: String,
561    pub oid: u64,
562    pub crossed: bool,
563    pub fee: String,
564    pub tid: u64,
565    pub liquidation: Option<FillLiquidationData>,
566    #[serde(rename = "feeToken")]
567    pub fee_token: String,
568    #[serde(rename = "builderFee")]
569    pub builder_fee: Option<String>,
570}
571
572/// Fill liquidation data.
573#[derive(Debug, Clone, Deserialize)]
574pub struct FillLiquidationData {
575    #[serde(rename = "liquidatedUser")]
576    pub liquidated_user: Option<String>,
577    #[serde(rename = "markPx")]
578    pub mark_px: f64,
579    pub method: HyperliquidLiquidationMethod,
580}
581
582/// WebSocket user funding data.
583#[derive(Debug, Clone, Deserialize)]
584pub struct WsUserFundingData {
585    pub time: u64,
586    pub coin: Ustr,
587    pub usdc: String,
588    pub szi: String,
589    #[serde(rename = "fundingRate")]
590    pub funding_rate: String,
591}
592
593/// WebSocket liquidation data.
594#[derive(Debug, Clone, Deserialize)]
595pub struct WsLiquidationData {
596    pub lid: u64,
597    pub liquidator: String,
598    pub liquidated_user: String,
599    pub liquidated_ntl_pos: String,
600    pub liquidated_account_value: String,
601}
602
603/// WebSocket non-user cancel data.
604#[derive(Debug, Clone, Deserialize)]
605pub struct WsNonUserCancelData {
606    pub coin: Ustr,
607    pub oid: u64,
608}
609
610/// Trigger order activated event data.
611#[derive(Debug, Clone, Deserialize)]
612pub struct WsTriggerActivatedData {
613    pub coin: Ustr,
614    pub oid: u64,
615    pub time: u64,
616    #[serde(rename = "triggerPx")]
617    pub trigger_px: String,
618    pub tpsl: HyperliquidTpSl,
619}
620
621/// Trigger order triggered event data.
622#[derive(Debug, Clone, Deserialize)]
623pub struct WsTriggerTriggeredData {
624    pub coin: Ustr,
625    pub oid: u64,
626    pub time: u64,
627    #[serde(rename = "triggerPx")]
628    pub trigger_px: String,
629    #[serde(rename = "marketPx")]
630    pub market_px: String,
631    pub tpsl: HyperliquidTpSl,
632    /// Order ID of the resulting market/limit order after trigger.
633    #[serde(rename = "resultingOid")]
634    pub resulting_oid: Option<u64>,
635}
636
637/// WebSocket user fills data.
638#[derive(Debug, Clone, Deserialize)]
639pub struct WsUserFillsData {
640    #[serde(rename = "isSnapshot")]
641    pub is_snapshot: Option<bool>,
642    pub user: String,
643    pub fills: Vec<WsFillData>,
644}
645
646/// WebSocket user fundings data.
647#[derive(Debug, Clone, Deserialize)]
648pub struct WsUserFundingsData {
649    #[serde(rename = "isSnapshot")]
650    pub is_snapshot: Option<bool>,
651    pub user: String,
652    pub fundings: Vec<WsUserFundingData>,
653}
654
655/// WebSocket active asset context data.
656#[derive(Debug, Clone, Deserialize)]
657#[serde(untagged)]
658pub enum WsActiveAssetCtxData {
659    Perp { coin: Ustr, ctx: PerpsAssetCtx },
660    Spot { coin: Ustr, ctx: SpotAssetCtx },
661}
662
663/// Shared asset context fields.
664#[derive(Debug, Clone, Deserialize)]
665pub struct SharedAssetCtx {
666    #[serde(rename = "dayNtlVlm")]
667    pub day_ntl_vlm: String,
668    #[serde(rename = "prevDayPx")]
669    pub prev_day_px: String,
670    #[serde(rename = "markPx")]
671    pub mark_px: String,
672    #[serde(rename = "midPx")]
673    pub mid_px: Option<String>,
674    #[serde(rename = "impactPxs")]
675    pub impact_pxs: Option<Vec<String>>,
676    #[serde(rename = "dayBaseVlm")]
677    pub day_base_vlm: Option<String>,
678}
679
680/// Perps asset context.
681#[derive(Debug, Clone, Deserialize)]
682pub struct PerpsAssetCtx {
683    #[serde(flatten)]
684    pub shared: SharedAssetCtx,
685    pub funding: String,
686    #[serde(rename = "openInterest")]
687    pub open_interest: String,
688    #[serde(rename = "oraclePx")]
689    pub oracle_px: String,
690    pub premium: Option<String>,
691}
692
693/// Spot asset context.
694#[derive(Debug, Clone, Deserialize)]
695pub struct SpotAssetCtx {
696    #[serde(flatten)]
697    pub shared: SharedAssetCtx,
698    #[serde(rename = "circulatingSupply")]
699    pub circulating_supply: String,
700}
701
702/// WebSocket active asset data.
703#[derive(Debug, Clone, Deserialize)]
704pub struct WsActiveAssetData {
705    pub user: String,
706    pub coin: Ustr,
707    pub leverage: LeverageData,
708    #[serde(rename = "maxTradeSzs")]
709    pub max_trade_szs: [f64; 2],
710    #[serde(rename = "availableToTrade")]
711    pub available_to_trade: [f64; 2],
712}
713
714/// Leverage data.
715#[derive(Debug, Clone, Deserialize)]
716pub struct LeverageData {
717    pub value: f64,
718    pub type_: String,
719}
720
721/// WebSocket TWAP slice fills data.
722#[derive(Debug, Clone, Deserialize)]
723pub struct WsUserTwapSliceFillsData {
724    #[serde(rename = "isSnapshot")]
725    pub is_snapshot: Option<bool>,
726    pub user: String,
727    #[serde(rename = "twapSliceFills")]
728    pub twap_slice_fills: Vec<WsTwapSliceFillData>,
729}
730
731/// TWAP slice fill data.
732#[derive(Debug, Clone, Deserialize)]
733pub struct WsTwapSliceFillData {
734    pub fill: WsFillData,
735    #[serde(rename = "twapId")]
736    pub twap_id: u64,
737}
738
739/// WebSocket TWAP history data.
740#[derive(Debug, Clone, Deserialize)]
741pub struct WsUserTwapHistoryData {
742    #[serde(rename = "isSnapshot")]
743    pub is_snapshot: Option<bool>,
744    pub user: String,
745    pub history: Vec<WsTwapHistoryData>,
746}
747
748/// TWAP history data.
749#[derive(Debug, Clone, Deserialize)]
750pub struct WsTwapHistoryData {
751    pub state: TwapStateData,
752    pub status: TwapStatusData,
753    pub time: u64,
754}
755
756/// TWAP state data.
757#[derive(Debug, Clone, Deserialize)]
758pub struct TwapStateData {
759    pub coin: Ustr,
760    pub user: String,
761    pub side: HyperliquidSide,
762    pub sz: f64,
763    #[serde(rename = "executedSz")]
764    pub executed_sz: f64,
765    #[serde(rename = "executedNtl")]
766    pub executed_ntl: f64,
767    pub minutes: u32,
768    #[serde(rename = "reduceOnly")]
769    pub reduce_only: bool,
770    pub randomize: bool,
771    pub timestamp: u64,
772}
773
774/// TWAP status data.
775#[derive(Debug, Clone, Deserialize)]
776pub struct TwapStatusData {
777    pub status: HyperliquidTwapStatus,
778    pub description: String,
779}
780
781/// WebSocket BBO data.
782#[derive(Debug, Clone, Deserialize)]
783pub struct WsBboData {
784    pub coin: Ustr,
785    pub time: u64,
786    pub bbo: [Option<WsLevelData>; 2], // [bid, ask]
787}
788
789////////////////////////////////////////////////////////////////////////////////
790// Tests
791////////////////////////////////////////////////////////////////////////////////
792
793#[cfg(test)]
794mod tests {
795    use rstest::rstest;
796    use serde_json;
797
798    use super::*;
799
800    #[rstest]
801    fn test_subscription_request_serialization() {
802        let sub = SubscriptionRequest::L2Book {
803            coin: Ustr::from("BTC"),
804            n_sig_figs: Some(5),
805            mantissa: None,
806        };
807
808        let json = serde_json::to_string(&sub).unwrap();
809        assert!(json.contains(r#""type":"l2Book""#));
810        assert!(json.contains(r#""coin":"BTC""#));
811    }
812
813    #[rstest]
814    fn test_hyperliquid_ws_request_serialization() {
815        let req = HyperliquidWsRequest::Subscribe {
816            subscription: SubscriptionRequest::Trades {
817                coin: Ustr::from("ETH"),
818            },
819        };
820
821        let json = serde_json::to_string(&req).unwrap();
822        assert!(json.contains(r#""method":"subscribe""#));
823        assert!(json.contains(r#""type":"trades""#));
824    }
825
826    #[rstest]
827    fn test_order_request_serialization() {
828        let order = OrderRequest {
829            a: 0,    // BTC asset ID
830            b: true, // buy
831            p: "50000.0".to_string(),
832            s: "0.1".to_string(),
833            r: false,
834            t: OrderTypeRequest::Limit {
835                tif: TimeInForceRequest::Gtc,
836            },
837            c: Some("client-123".to_string()),
838        };
839
840        let json = serde_json::to_string(&order).unwrap();
841        assert!(json.contains(r#""a":0"#));
842        assert!(json.contains(r#""b":true"#));
843        assert!(json.contains(r#""p":"50000.0""#));
844    }
845
846    #[rstest]
847    fn test_ws_trade_data_deserialization() {
848        let json = r#"{
849            "coin": "BTC",
850            "side": "B",
851            "px": "50000.0",
852            "sz": "0.1",
853            "hash": "0x123",
854            "time": 1234567890,
855            "tid": 12345,
856            "users": ["0xabc", "0xdef"]
857        }"#;
858
859        let trade: WsTradeData = serde_json::from_str(json).unwrap();
860        assert_eq!(trade.coin, "BTC");
861        assert_eq!(trade.side, HyperliquidSide::Buy);
862        assert_eq!(trade.px, "50000.0");
863    }
864
865    #[rstest]
866    fn test_ws_book_data_deserialization() {
867        let json = r#"{
868            "coin": "ETH",
869            "levels": [
870                [{"px": "3000.0", "sz": "1.0", "n": 1}],
871                [{"px": "3001.0", "sz": "2.0", "n": 2}]
872            ],
873            "time": 1234567890
874        }"#;
875
876        let book: WsBookData = serde_json::from_str(json).unwrap();
877        assert_eq!(book.coin, "ETH");
878        assert_eq!(book.levels[0].len(), 1);
879        assert_eq!(book.levels[1].len(), 1);
880    }
881
882    // ========================================================================
883    // Conditional Order WebSocket Message Tests
884    // ========================================================================
885
886    #[rstest]
887    fn test_ws_trailing_stop_data_deserialization() {
888        let json = r#"{
889            "offset": "100.0",
890            "offsetType": "price",
891            "callbackPrice": "50000.0"
892        }"#;
893
894        let data: WsTrailingStopData = serde_json::from_str(json).unwrap();
895        assert_eq!(data.offset, "100.0");
896        assert_eq!(data.offset_type, TrailingOffsetType::Price);
897        assert_eq!(data.callback_price.unwrap(), "50000.0");
898    }
899
900    #[rstest]
901    fn test_ws_trigger_activated_data_deserialization() {
902        let json = r#"{
903            "coin": "BTC",
904            "oid": 12345,
905            "time": 1704470400000,
906            "triggerPx": "50000.0",
907            "tpsl": "sl"
908        }"#;
909
910        let data: WsTriggerActivatedData = serde_json::from_str(json).unwrap();
911        assert_eq!(data.coin, Ustr::from("BTC"));
912        assert_eq!(data.oid, 12345);
913        assert_eq!(data.trigger_px, "50000.0");
914        assert_eq!(data.tpsl, HyperliquidTpSl::Sl);
915        assert_eq!(data.time, 1704470400000);
916    }
917
918    #[rstest]
919    fn test_ws_trigger_triggered_data_deserialization() {
920        let json = r#"{
921            "coin": "ETH",
922            "oid": 67890,
923            "time": 1704470500000,
924            "triggerPx": "3000.0",
925            "marketPx": "3001.0",
926            "tpsl": "tp",
927            "resultingOid": 99999
928        }"#;
929
930        let data: WsTriggerTriggeredData = serde_json::from_str(json).unwrap();
931        assert_eq!(data.coin, Ustr::from("ETH"));
932        assert_eq!(data.oid, 67890);
933        assert_eq!(data.trigger_px, "3000.0");
934        assert_eq!(data.market_px, "3001.0");
935        assert_eq!(data.tpsl, HyperliquidTpSl::Tp);
936        assert_eq!(data.resulting_oid, Some(99999));
937    }
938}
939
940/// Nautilus WebSocket message wrapper for routing to execution engine.
941///
942/// Wraps parsed messages from the handler.
943///
944/// All parsing happens in the handler layer, with parsed Nautilus domain objects.
945/// passed through to the Python layer.
946#[derive(Debug, Clone)]
947pub enum NautilusWsMessage {
948    /// Execution reports (order status and fills).
949    ExecutionReports(Vec<ExecutionReport>),
950    /// Parsed trade ticks.
951    Trades(Vec<TradeTick>),
952    /// Parsed quote tick (from BBO).
953    Quote(QuoteTick),
954    /// Parsed order book deltas.
955    Deltas(OrderBookDeltas),
956    /// Parsed candle/bar.
957    Candle(Bar),
958    /// Mark price update.
959    MarkPrice(MarkPriceUpdate),
960    /// Index price update.
961    IndexPrice(IndexPriceUpdate),
962    /// Funding rate update.
963    FundingRate(FundingRateUpdate),
964    /// Error occurred.
965    Error(String),
966    /// WebSocket reconnected.
967    Reconnected,
968}
969
970/// Execution report wrapper for order status and fill reports.
971///
972/// This enum allows both order status updates and fill reports.
973/// to be sent through the execution engine.
974#[derive(Debug, Clone)]
975#[allow(clippy::large_enum_variant)]
976pub enum ExecutionReport {
977    /// Order status report.
978    Order(OrderStatusReport),
979    /// Fill report.
980    Fill(FillReport),
981}