1use ahash::AHashMap;
17use derive_builder::Builder;
18use nautilus_core::serialization::{
19 deserialize_decimal, deserialize_decimal_from_str, deserialize_optional_decimal_from_str,
20 serialize_decimal_as_str,
21};
22use nautilus_model::{
23 data::{
24 Bar, Data, FundingRateUpdate, IndexPriceUpdate, MarkPriceUpdate, OrderBookDeltas,
25 OrderBookDepth10, QuoteTick, TradeTick,
26 },
27 reports::{FillReport, OrderStatusReport},
28};
29use rust_decimal::Decimal;
30use serde::{Deserialize, Serialize};
31use ustr::Ustr;
32
33use crate::{
34 common::enums::{
35 HyperliquidBarInterval, HyperliquidFillDirection, HyperliquidLiquidationMethod,
36 HyperliquidOrderStatus as HyperliquidOrderStatusEnum, HyperliquidSide,
37 HyperliquidTimeInForce, HyperliquidTpSl, HyperliquidTwapStatus,
38 },
39 http::models::{HyperliquidExchangeAction, HyperliquidExchangeRequest},
40};
41
42#[derive(Debug, Clone, Serialize)]
44#[serde(tag = "method")]
45#[serde(rename_all = "lowercase")]
46pub enum HyperliquidWsRequest {
47 Subscribe {
49 subscription: SubscriptionRequest,
51 },
52 Unsubscribe {
54 subscription: SubscriptionRequest,
56 },
57 Post {
59 id: u64,
61 request: PostRequest,
63 },
64 Ping,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
70#[serde(tag = "type")]
71#[serde(rename_all = "camelCase")]
72pub enum SubscriptionRequest {
73 AllMids {
75 #[serde(skip_serializing_if = "Option::is_none")]
76 dex: Option<String>,
77 },
78 AllDexsAssetCtxs,
80 Notification { user: String },
82 WebData2 { user: String },
84 Candle {
86 coin: Ustr,
87 interval: HyperliquidBarInterval,
88 },
89 L2Book {
91 coin: Ustr,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 #[serde(rename = "nSigFigs")]
94 n_sig_figs: Option<u32>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 mantissa: Option<u32>,
97 },
98 Trades { coin: Ustr },
100 OrderUpdates { user: String },
102 UserEvents { user: String },
104 UserFills {
106 user: String,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 #[serde(rename = "aggregateByTime")]
109 aggregate_by_time: Option<bool>,
110 },
111 UserFundings { user: String },
113 UserNonFundingLedgerUpdates { user: String },
115 ActiveAssetCtx { coin: Ustr },
117 ActiveSpotAssetCtx { coin: Ustr },
119 ActiveAssetData { user: String, coin: String },
121 UserTwapSliceFills { user: String },
123 UserTwapHistory { user: String },
125 Bbo { coin: Ustr },
127}
128
129#[derive(Debug, Clone, Serialize)]
131#[serde(tag = "type")]
132#[serde(rename_all = "lowercase")]
133pub enum PostRequest {
134 Info { payload: serde_json::Value },
136 Action {
138 payload: HyperliquidExchangeRequest<HyperliquidExchangeAction>,
139 },
140}
141
142#[derive(Debug, Clone, Serialize)]
144pub struct ActionPayload {
145 pub action: ActionRequest,
146 pub nonce: u64,
147 pub signature: SignatureData,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 #[serde(rename = "vaultAddress")]
150 pub vault_address: Option<String>,
151}
152
153#[derive(Debug, Clone, Serialize)]
155pub struct SignatureData {
156 pub r: String,
157 pub s: String,
158 pub v: String,
159}
160
161#[derive(Debug, Clone, Serialize)]
163#[serde(tag = "type")]
164#[serde(rename_all = "lowercase")]
165pub enum ActionRequest {
166 Order {
168 orders: Vec<OrderRequest>,
169 grouping: String,
170 },
171 Cancel {
173 cancels: Vec<CancelRequest>,
174 #[serde(rename = "f", skip_serializing_if = "Option::is_none")]
175 fast: Option<bool>,
176 },
177 CancelByCloid {
179 cancels: Vec<CancelByCloidRequest>,
180 #[serde(rename = "f", skip_serializing_if = "Option::is_none")]
181 fast: Option<bool>,
182 },
183 Modify { modifies: Vec<ModifyRequest> },
185}
186
187impl ActionRequest {
188 pub fn order(orders: Vec<OrderRequest>, grouping: impl Into<String>) -> Self {
195 Self::Order {
196 orders,
197 grouping: grouping.into(),
198 }
199 }
200
201 pub fn cancel(cancels: Vec<CancelRequest>) -> Self {
211 Self::Cancel {
212 cancels,
213 fast: None,
214 }
215 }
216
217 pub fn cancel_by_cloid(cancels: Vec<CancelByCloidRequest>) -> Self {
226 Self::CancelByCloid {
227 cancels,
228 fast: None,
229 }
230 }
231
232 pub fn modify(modifies: Vec<ModifyRequest>) -> Self {
241 Self::Modify { modifies }
242 }
243}
244
245#[derive(Debug, Clone, Serialize, Builder)]
247pub struct OrderRequest {
248 pub a: u32,
250 pub b: bool,
252 pub p: String,
254 pub s: String,
256 pub r: bool,
258 pub t: OrderTypeRequest,
260 #[serde(skip_serializing_if = "Option::is_none")]
262 pub c: Option<String>,
263}
264
265#[derive(Debug, Clone, Serialize)]
267#[serde(tag = "type")]
268#[serde(rename_all = "lowercase")]
269pub enum OrderTypeRequest {
270 Limit {
271 tif: TimeInForceRequest,
272 },
273 Trigger {
274 #[serde(rename = "isMarket")]
275 is_market: bool,
276 #[serde(rename = "triggerPx")]
277 trigger_px: String,
278 tpsl: TpSlRequest,
279 },
280}
281
282#[derive(Debug, Clone, Serialize)]
284#[serde(rename_all = "PascalCase")]
285pub enum TimeInForceRequest {
286 Alo,
287 Ioc,
288 Gtc,
289}
290
291#[derive(Debug, Clone, Serialize)]
293#[serde(rename_all = "lowercase")]
294pub enum TpSlRequest {
295 Tp,
296 Sl,
297}
298
299#[derive(Debug, Clone, Serialize)]
301pub struct CancelRequest {
302 pub a: u32,
304 pub o: u64,
306}
307
308#[derive(Debug, Clone, Serialize)]
310pub struct CancelByCloidRequest {
311 pub asset: u32,
313 pub cloid: String,
315}
316
317#[derive(Debug, Clone, Serialize)]
319pub struct ModifyRequest {
320 pub oid: u64,
322 pub order: OrderRequest,
324}
325
326#[derive(Debug, Clone, Deserialize)]
328pub struct SubscriptionResponseData {
329 pub method: String,
330 pub subscription: SubscriptionRequest,
331}
332
333#[derive(Debug, Clone, Deserialize)]
335#[serde(tag = "channel")]
336#[serde(rename_all = "camelCase")]
337pub enum HyperliquidWsMessage {
338 SubscriptionResponse { data: SubscriptionResponseData },
340 Post { data: PostResponse },
342 AllMids { data: AllMidsData },
344 AllDexsAssetCtxs { data: WsAllDexsAssetCtxsData },
346 Notification { data: NotificationData },
348 WebData2 { data: serde_json::Value },
350 Candle { data: CandleData },
352 L2Book { data: WsBookData },
354 Trades { data: Vec<WsTradeData> },
356 OrderUpdates { data: Vec<WsOrderData> },
358 UserEvents { data: WsUserEventData },
360 #[serde(rename = "user")]
362 User { data: WsUserEventData },
363 UserFills { data: WsUserFillsData },
365 UserFundings { data: WsUserFundingsData },
367 UserNonFundingLedgerUpdates { data: serde_json::Value },
369 ActiveAssetCtx { data: WsActiveAssetCtxData },
371 ActiveSpotAssetCtx { data: WsActiveAssetCtxData },
373 ActiveAssetData { data: WsActiveAssetData },
375 UserTwapSliceFills { data: WsUserTwapSliceFillsData },
377 UserTwapHistory { data: WsUserTwapHistoryData },
379 Bbo { data: WsBboData },
381 Error { data: String },
383 Pong,
385}
386
387#[derive(Debug, Clone, Deserialize)]
389pub struct PostResponse {
390 pub id: u64,
391 pub response: PostResponsePayload,
392}
393
394#[derive(Debug, Clone, Deserialize)]
396#[serde(tag = "type")]
397#[serde(rename_all = "lowercase")]
398pub enum PostResponsePayload {
399 Info { payload: serde_json::Value },
400 Action { payload: serde_json::Value },
401 Error { payload: String },
402}
403
404#[derive(Debug, Clone, Deserialize)]
406pub struct AllMidsData {
407 pub mids: AHashMap<Ustr, String>,
408}
409
410#[derive(Debug, Clone, Deserialize)]
412pub struct WsAllDexsAssetCtxsData {
413 pub ctxs: Vec<(String, Vec<PerpsAssetCtx>)>,
414}
415
416#[derive(Debug, Clone, Deserialize)]
418pub struct NotificationData {
419 pub notification: String,
420}
421
422#[derive(Debug, Clone, Deserialize)]
424pub struct CandleData {
425 pub t: u64,
427 #[serde(rename = "T")]
429 pub close_time: u64,
430 pub s: Ustr,
432 pub i: Ustr,
434 #[serde(deserialize_with = "deserialize_decimal_from_str")]
436 pub o: Decimal,
437 #[serde(deserialize_with = "deserialize_decimal_from_str")]
439 pub c: Decimal,
440 #[serde(deserialize_with = "deserialize_decimal_from_str")]
442 pub h: Decimal,
443 #[serde(deserialize_with = "deserialize_decimal_from_str")]
445 pub l: Decimal,
446 #[serde(deserialize_with = "deserialize_decimal_from_str")]
448 pub v: Decimal,
449 pub n: u32,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct WsBookData {
456 pub coin: Ustr,
457 pub levels: [Vec<WsLevelData>; 2], pub time: u64,
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize)]
463pub struct WsLevelData {
464 #[serde(
466 deserialize_with = "deserialize_decimal_from_str",
467 serialize_with = "serialize_decimal_as_str"
468 )]
469 pub px: Decimal,
470 #[serde(
472 deserialize_with = "deserialize_decimal_from_str",
473 serialize_with = "serialize_decimal_as_str"
474 )]
475 pub sz: Decimal,
476 pub n: u32,
478}
479
480#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct WsTradeData {
483 pub coin: Ustr,
484 pub side: HyperliquidSide,
485 #[serde(
486 deserialize_with = "deserialize_decimal_from_str",
487 serialize_with = "serialize_decimal_as_str"
488 )]
489 pub px: Decimal,
490 #[serde(
491 deserialize_with = "deserialize_decimal_from_str",
492 serialize_with = "serialize_decimal_as_str"
493 )]
494 pub sz: Decimal,
495 pub hash: String,
496 pub time: u64,
497 pub tid: u64,
498 pub users: [String; 2], }
500
501#[derive(Debug, Clone, Deserialize)]
503pub struct WsOrderData {
504 pub order: WsBasicOrderData,
505 pub status: HyperliquidOrderStatusEnum,
506 #[serde(rename = "statusTimestamp")]
507 pub status_timestamp: u64,
508}
509
510#[derive(Debug, Clone, Deserialize)]
512pub struct WsBasicOrderData {
513 pub coin: Ustr,
514 pub side: HyperliquidSide,
515 #[serde(rename = "limitPx", deserialize_with = "deserialize_decimal_from_str")]
516 pub limit_px: Decimal,
517 #[serde(deserialize_with = "deserialize_decimal_from_str")]
518 pub sz: Decimal,
519 pub oid: u64,
520 pub timestamp: u64,
521 #[serde(rename = "origSz", deserialize_with = "deserialize_decimal_from_str")]
522 pub orig_sz: Decimal,
523 pub cloid: Option<String>,
524 pub tif: Option<HyperliquidTimeInForce>,
525 #[serde(rename = "reduceOnly")]
526 pub reduce_only: Option<bool>,
527 #[serde(
529 rename = "triggerPx",
530 default,
531 deserialize_with = "deserialize_optional_decimal_from_str"
532 )]
533 pub trigger_px: Option<Decimal>,
534 #[serde(rename = "isMarket")]
536 pub is_market: Option<bool>,
537 pub tpsl: Option<HyperliquidTpSl>,
539 #[serde(rename = "triggerActivated")]
541 pub trigger_activated: Option<bool>,
542 #[serde(rename = "trailingStop")]
544 pub trailing_stop: Option<WsTrailingStopData>,
545}
546
547#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
549#[serde(rename_all = "camelCase")]
550pub enum TrailingOffsetType {
551 Price,
553 Percentage,
555 BasisPoints,
557}
558
559impl TrailingOffsetType {
560 pub fn format_offset(&self, offset: &str) -> String {
562 match self {
563 Self::Price => offset.to_string(),
564 Self::Percentage => format!("{offset}%"),
565 Self::BasisPoints => format!("{offset} bps"),
566 }
567 }
568}
569
570#[derive(Debug, Clone, Deserialize)]
572pub struct WsTrailingStopData {
573 #[serde(deserialize_with = "deserialize_decimal_from_str")]
575 pub offset: Decimal,
576 #[serde(rename = "offsetType")]
578 pub offset_type: TrailingOffsetType,
579 #[serde(
581 rename = "callbackPrice",
582 default,
583 deserialize_with = "deserialize_optional_decimal_from_str"
584 )]
585 pub callback_price: Option<Decimal>,
586}
587
588#[derive(Debug, Clone, Deserialize)]
590#[serde(untagged)]
591pub enum WsUserEventData {
592 Fills {
593 fills: Vec<WsFillData>,
594 },
595 Funding {
596 funding: WsUserFundingData,
597 },
598 Liquidation {
599 liquidation: WsLiquidationData,
600 },
601 NonUserCancel {
602 #[serde(rename = "nonUserCancel")]
603 non_user_cancel: Vec<WsNonUserCancelData>,
604 },
605 TriggerActivated {
607 #[serde(rename = "triggerActivated")]
608 trigger_activated: WsTriggerActivatedData,
609 },
610 TriggerTriggered {
612 #[serde(rename = "triggerTriggered")]
613 trigger_triggered: WsTriggerTriggeredData,
614 },
615}
616
617#[derive(Debug, Clone, Deserialize)]
619pub struct WsFillData {
620 pub coin: Ustr,
621 #[serde(deserialize_with = "deserialize_decimal_from_str")]
622 pub px: Decimal,
623 #[serde(deserialize_with = "deserialize_decimal_from_str")]
624 pub sz: Decimal,
625 pub side: HyperliquidSide,
626 pub time: u64,
627 #[serde(
628 rename = "startPosition",
629 deserialize_with = "deserialize_decimal_from_str"
630 )]
631 pub start_position: Decimal,
632 pub dir: HyperliquidFillDirection,
633 #[serde(
634 rename = "closedPnl",
635 deserialize_with = "deserialize_decimal_from_str"
636 )]
637 pub closed_pnl: Decimal,
638 pub hash: String,
639 pub oid: u64,
640 pub crossed: bool,
641 #[serde(deserialize_with = "deserialize_decimal_from_str")]
642 pub fee: Decimal,
643 pub tid: u64,
644 #[serde(default)]
645 pub liquidation: Option<FillLiquidationData>,
646 #[serde(rename = "feeToken")]
647 pub fee_token: Ustr,
648 #[serde(
649 rename = "builderFee",
650 default,
651 deserialize_with = "deserialize_optional_decimal_from_str"
652 )]
653 pub builder_fee: Option<Decimal>,
654 pub cloid: Option<String>,
656 #[serde(rename = "twapId")]
658 pub twap_id: Option<serde_json::Value>,
659}
660
661#[derive(Debug, Clone, Deserialize)]
663pub struct FillLiquidationData {
664 #[serde(rename = "liquidatedUser")]
665 pub liquidated_user: Option<String>,
666 #[serde(rename = "markPx", deserialize_with = "deserialize_decimal_from_str")]
667 pub mark_px: Decimal,
668 pub method: HyperliquidLiquidationMethod,
669}
670
671#[derive(Debug, Clone, Deserialize)]
673pub struct WsUserFundingData {
674 pub time: u64,
675 pub coin: Ustr,
676 #[serde(deserialize_with = "deserialize_decimal_from_str")]
677 pub usdc: Decimal,
678 #[serde(deserialize_with = "deserialize_decimal_from_str")]
679 pub szi: Decimal,
680 #[serde(
681 rename = "fundingRate",
682 deserialize_with = "deserialize_decimal_from_str"
683 )]
684 pub funding_rate: Decimal,
685}
686
687#[derive(Debug, Clone, Deserialize)]
689pub struct WsLiquidationData {
690 pub lid: u64,
691 pub liquidator: String,
692 pub liquidated_user: String,
693 #[serde(deserialize_with = "deserialize_decimal_from_str")]
694 pub liquidated_ntl_pos: Decimal,
695 #[serde(deserialize_with = "deserialize_decimal_from_str")]
696 pub liquidated_account_value: Decimal,
697}
698
699#[derive(Debug, Clone, Deserialize)]
701pub struct WsNonUserCancelData {
702 pub coin: Ustr,
703 pub oid: u64,
704}
705
706#[derive(Debug, Clone, Deserialize)]
708pub struct WsTriggerActivatedData {
709 pub coin: Ustr,
710 pub oid: u64,
711 pub time: u64,
712 #[serde(
713 rename = "triggerPx",
714 deserialize_with = "deserialize_decimal_from_str"
715 )]
716 pub trigger_px: Decimal,
717 pub tpsl: HyperliquidTpSl,
718}
719
720#[derive(Debug, Clone, Deserialize)]
722pub struct WsTriggerTriggeredData {
723 pub coin: Ustr,
724 pub oid: u64,
725 pub time: u64,
726 #[serde(
727 rename = "triggerPx",
728 deserialize_with = "deserialize_decimal_from_str"
729 )]
730 pub trigger_px: Decimal,
731 #[serde(rename = "marketPx", deserialize_with = "deserialize_decimal_from_str")]
732 pub market_px: Decimal,
733 pub tpsl: HyperliquidTpSl,
734 #[serde(rename = "resultingOid")]
736 pub resulting_oid: Option<u64>,
737}
738
739#[derive(Debug, Clone, Deserialize)]
741pub struct WsUserFillsData {
742 #[serde(rename = "isSnapshot")]
743 pub is_snapshot: Option<bool>,
744 pub user: String,
745 pub fills: Vec<WsFillData>,
746}
747
748#[derive(Debug, Clone, Deserialize)]
750pub struct WsUserFundingsData {
751 #[serde(rename = "isSnapshot")]
752 pub is_snapshot: Option<bool>,
753 pub user: String,
754 pub fundings: Vec<WsUserFundingData>,
755}
756
757#[derive(Debug, Clone, Deserialize)]
759#[serde(untagged)]
760pub enum WsActiveAssetCtxData {
761 Perp { coin: Ustr, ctx: PerpsAssetCtx },
762 Spot { coin: Ustr, ctx: SpotAssetCtx },
763}
764
765#[derive(Debug, Clone, Deserialize)]
767pub struct SharedAssetCtx {
768 #[serde(
769 rename = "dayNtlVlm",
770 deserialize_with = "deserialize_decimal_from_str"
771 )]
772 pub day_ntl_vlm: Decimal,
773 #[serde(
774 rename = "prevDayPx",
775 deserialize_with = "deserialize_decimal_from_str"
776 )]
777 pub prev_day_px: Decimal,
778 #[serde(rename = "markPx", deserialize_with = "deserialize_decimal_from_str")]
779 pub mark_px: Decimal,
780 #[serde(
781 rename = "midPx",
782 default,
783 deserialize_with = "deserialize_optional_decimal_from_str"
784 )]
785 pub mid_px: Option<Decimal>,
786 #[serde(rename = "impactPxs")]
787 pub impact_pxs: Option<Vec<String>>,
788 #[serde(
789 rename = "dayBaseVlm",
790 default,
791 deserialize_with = "deserialize_optional_decimal_from_str"
792 )]
793 pub day_base_vlm: Option<Decimal>,
794}
795
796#[derive(Debug, Clone, Deserialize)]
798pub struct PerpsAssetCtx {
799 #[serde(flatten)]
800 pub shared: SharedAssetCtx,
801 #[serde(deserialize_with = "deserialize_decimal_from_str")]
802 pub funding: Decimal,
803 #[serde(
804 rename = "openInterest",
805 deserialize_with = "deserialize_decimal_from_str"
806 )]
807 pub open_interest: Decimal,
808 #[serde(rename = "oraclePx", deserialize_with = "deserialize_decimal_from_str")]
809 pub oracle_px: Decimal,
810 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
811 pub premium: Option<Decimal>,
812}
813
814#[derive(Debug, Clone, Deserialize)]
816pub struct SpotAssetCtx {
817 #[serde(flatten)]
818 pub shared: SharedAssetCtx,
819 #[serde(
820 rename = "circulatingSupply",
821 deserialize_with = "deserialize_decimal_from_str"
822 )]
823 pub circulating_supply: Decimal,
824}
825
826#[derive(Debug, Clone, Deserialize)]
828pub struct WsActiveAssetData {
829 pub user: String,
830 pub coin: Ustr,
831 pub leverage: LeverageData,
832 #[serde(rename = "maxTradeSzs")]
833 pub max_trade_szs: [f64; 2],
834 #[serde(rename = "availableToTrade")]
835 pub available_to_trade: [f64; 2],
836}
837
838#[derive(Debug, Clone, Deserialize)]
840pub struct LeverageData {
841 pub value: f64,
842 pub type_: String,
843}
844
845#[derive(Debug, Clone, Deserialize)]
847pub struct WsUserTwapSliceFillsData {
848 #[serde(rename = "isSnapshot")]
849 pub is_snapshot: Option<bool>,
850 pub user: String,
851 #[serde(rename = "twapSliceFills")]
852 pub twap_slice_fills: Vec<WsTwapSliceFillData>,
853}
854
855#[derive(Debug, Clone, Deserialize)]
857pub struct WsTwapSliceFillData {
858 pub fill: WsFillData,
859 #[serde(rename = "twapId")]
860 pub twap_id: u64,
861}
862
863#[derive(Debug, Clone, Deserialize)]
865pub struct WsUserTwapHistoryData {
866 #[serde(rename = "isSnapshot")]
867 pub is_snapshot: Option<bool>,
868 pub user: String,
869 pub history: Vec<WsTwapHistoryData>,
870}
871
872#[derive(Debug, Clone, Deserialize)]
874pub struct WsTwapHistoryData {
875 pub state: TwapStateData,
876 pub status: TwapStatusData,
877 pub time: u64,
878 #[serde(default, rename = "twapId")]
879 pub twap_id: Option<u64>,
880}
881
882#[derive(Debug, Clone, Deserialize)]
884pub struct TwapStateData {
885 pub coin: Ustr,
886 pub user: String,
887 pub side: HyperliquidSide,
888 #[serde(deserialize_with = "deserialize_decimal")]
890 pub sz: Decimal,
891 #[serde(rename = "executedSz", deserialize_with = "deserialize_decimal")]
892 pub executed_sz: Decimal,
893 #[serde(rename = "executedNtl", deserialize_with = "deserialize_decimal")]
894 pub executed_ntl: Decimal,
895 pub minutes: u32,
896 #[serde(rename = "reduceOnly")]
897 pub reduce_only: bool,
898 pub randomize: bool,
899 pub timestamp: u64,
900}
901
902#[derive(Debug, Clone, Deserialize)]
904pub struct TwapStatusData {
905 pub status: HyperliquidTwapStatus,
906 #[serde(default)]
908 pub description: String,
909}
910
911#[derive(Debug, Clone, Deserialize)]
913pub struct WsBboData {
914 pub coin: Ustr,
915 pub time: u64,
916 pub bbo: [Option<WsLevelData>; 2], }
918
919#[cfg(test)]
920mod tests {
921 use rstest::rstest;
922 use rust_decimal_macros::dec;
923 use serde_json;
924
925 use super::*;
926
927 #[rstest]
928 fn test_subscription_request_serialization() {
929 let sub = SubscriptionRequest::L2Book {
930 coin: Ustr::from("BTC"),
931 n_sig_figs: Some(5),
932 mantissa: None,
933 };
934
935 let json = serde_json::to_string(&sub).unwrap();
936 assert!(json.contains(r#""type":"l2Book""#));
937 assert!(json.contains(r#""coin":"BTC""#));
938 }
939
940 #[rstest]
941 fn test_hyperliquid_ws_request_serialization() {
942 let req = HyperliquidWsRequest::Subscribe {
943 subscription: SubscriptionRequest::Trades {
944 coin: Ustr::from("ETH"),
945 },
946 };
947
948 let json = serde_json::to_string(&req).unwrap();
949 assert!(json.contains(r#""method":"subscribe""#));
950 assert!(json.contains(r#""type":"trades""#));
951 }
952
953 #[rstest]
954 fn test_order_request_serialization() {
955 let order = OrderRequest {
956 a: 0, b: true, p: "50000.0".to_string(),
959 s: "0.1".to_string(),
960 r: false,
961 t: OrderTypeRequest::Limit {
962 tif: TimeInForceRequest::Gtc,
963 },
964 c: Some("client-123".to_string()),
965 };
966
967 let json = serde_json::to_string(&order).unwrap();
968 assert!(json.contains(r#""a":0"#));
969 assert!(json.contains(r#""b":true"#));
970 assert!(json.contains(r#""p":"50000.0""#));
971 }
972
973 #[rstest]
974 fn test_ws_trade_data_deserialization() {
975 let json = r#"{
976 "coin": "BTC",
977 "side": "B",
978 "px": "50000.0",
979 "sz": "0.1",
980 "hash": "0x123",
981 "time": 1234567890,
982 "tid": 12345,
983 "users": ["0xabc", "0xdef"]
984 }"#;
985
986 let trade: WsTradeData = serde_json::from_str(json).unwrap();
987 assert_eq!(trade.coin, "BTC");
988 assert_eq!(trade.side, HyperliquidSide::Buy);
989 assert_eq!(trade.px, dec!(50000.0));
990 }
991
992 #[rstest]
993 fn test_ws_book_data_deserialization() {
994 let json = r#"{
995 "coin": "ETH",
996 "levels": [
997 [{"px": "3000.0", "sz": "1.0", "n": 1}],
998 [{"px": "3001.0", "sz": "2.0", "n": 2}]
999 ],
1000 "time": 1234567890
1001 }"#;
1002
1003 let book: WsBookData = serde_json::from_str(json).unwrap();
1004 assert_eq!(book.coin, "ETH");
1005 assert_eq!(book.levels[0].len(), 1);
1006 assert_eq!(book.levels[1].len(), 1);
1007 }
1008
1009 #[rstest]
1010 fn test_ws_trailing_stop_data_deserialization() {
1011 let json = r#"{
1012 "offset": "100.0",
1013 "offsetType": "price",
1014 "callbackPrice": "50000.0"
1015 }"#;
1016
1017 let data: WsTrailingStopData = serde_json::from_str(json).unwrap();
1018 assert_eq!(data.offset, dec!(100.0));
1019 assert_eq!(data.offset_type, TrailingOffsetType::Price);
1020 assert_eq!(data.callback_price.unwrap(), dec!(50000.0));
1021 }
1022
1023 #[rstest]
1024 fn test_ws_trigger_activated_data_deserialization() {
1025 let json = r#"{
1026 "coin": "BTC",
1027 "oid": 12345,
1028 "time": 1704470400000,
1029 "triggerPx": "50000.0",
1030 "tpsl": "sl"
1031 }"#;
1032
1033 let data: WsTriggerActivatedData = serde_json::from_str(json).unwrap();
1034 assert_eq!(data.coin, Ustr::from("BTC"));
1035 assert_eq!(data.oid, 12345);
1036 assert_eq!(data.trigger_px, dec!(50000.0));
1037 assert_eq!(data.tpsl, HyperliquidTpSl::Sl);
1038 assert_eq!(data.time, 1704470400000);
1039 }
1040
1041 #[rstest]
1042 fn test_ws_trigger_triggered_data_deserialization() {
1043 let json = r#"{
1044 "coin": "ETH",
1045 "oid": 67890,
1046 "time": 1704470500000,
1047 "triggerPx": "3000.0",
1048 "marketPx": "3001.0",
1049 "tpsl": "tp",
1050 "resultingOid": 99999
1051 }"#;
1052
1053 let data: WsTriggerTriggeredData = serde_json::from_str(json).unwrap();
1054 assert_eq!(data.coin, Ustr::from("ETH"));
1055 assert_eq!(data.oid, 67890);
1056 assert_eq!(data.trigger_px, dec!(3000.0));
1057 assert_eq!(data.market_px, dec!(3001.0));
1058 assert_eq!(data.tpsl, HyperliquidTpSl::Tp);
1059 assert_eq!(data.resulting_oid, Some(99999));
1060 }
1061
1062 #[rstest]
1063 fn test_ws_fill_data_deserialization_with_cloid_and_twap() {
1064 let json = r#"{
1065 "coin": "@107",
1066 "px": "31.737",
1067 "sz": "0.31",
1068 "side": "B",
1069 "time": 1769920606068,
1070 "startPosition": "0.0",
1071 "dir": "Buy",
1072 "closedPnl": "0.0",
1073 "hash": "0xc731e7561e5334a0c8ab043472ce7d01d400ff3bb95653726afa92a8dd570e8b",
1074 "oid": 308086083674,
1075 "crossed": true,
1076 "fee": "0.00021699",
1077 "tid": 812806034449156,
1078 "cloid": "0xd211f1c27288259290850338d22132a0",
1079 "feeToken": "HYPE",
1080 "twapId": null
1081 }"#;
1082
1083 let fill: WsFillData = serde_json::from_str(json).unwrap();
1084 assert_eq!(fill.coin, "@107");
1085 assert_eq!(fill.px, dec!(31.737));
1086 assert_eq!(fill.sz, dec!(0.31));
1087 assert_eq!(fill.side, HyperliquidSide::Buy);
1088 assert_eq!(fill.oid, 308086083674);
1089 assert!(fill.crossed);
1090 assert_eq!(fill.fee, dec!(0.00021699));
1091 assert_eq!(fill.fee_token, "HYPE");
1092 assert_eq!(
1093 fill.cloid,
1094 Some("0xd211f1c27288259290850338d22132a0".to_string())
1095 );
1096 assert!(fill.twap_id.is_none() || fill.twap_id == Some(serde_json::Value::Null));
1097 }
1098
1099 #[rstest]
1100 fn test_ws_user_fills_message_deserialization() {
1101 let json = r#"{"channel":"user","data":{"fills":[{"coin":"@107","px":"31.737","sz":"0.31","side":"B","time":1769920606068,"startPosition":"0.0","dir":"Buy","closedPnl":"0.0","hash":"0xc731e7561e5334a0c8ab043472ce7d01d400ff3bb95653726afa92a8dd570e8b","oid":308086083674,"crossed":true,"fee":"0.00021699","tid":812806034449156,"cloid":"0xd211f1c27288259290850338d22132a0","feeToken":"HYPE","twapId":null}]}}"#;
1102
1103 let msg: HyperliquidWsMessage = serde_json::from_str(json).unwrap();
1104
1105 match msg {
1106 HyperliquidWsMessage::User { data } => match data {
1107 WsUserEventData::Fills { fills } => {
1108 assert_eq!(fills.len(), 1);
1109 let fill = &fills[0];
1110 assert_eq!(fill.coin, "@107");
1111 assert_eq!(fill.px, dec!(31.737));
1112 assert_eq!(
1113 fill.cloid,
1114 Some("0xd211f1c27288259290850338d22132a0".to_string())
1115 );
1116 }
1117 _ => panic!("Expected Fills variant"),
1118 },
1119 _ => panic!("Expected User channel message"),
1120 }
1121 }
1122
1123 #[rstest]
1124 fn test_ws_user_fills_message_with_builder_fee() {
1125 let json = r#"{"channel":"user","data":{"fills":[{"coin":"BTC","px":"79146.0","sz":"0.001","side":"A","time":1769940855551,"startPosition":"0.00093","dir":"Long > Short","closedPnl":"0.046128","hash":"0x5f8b9c337a197c4061050434769793020e020019151c9b1203544786391d562b","oid":308254271324,"crossed":false,"fee":"0.019785","builderFee":"0.007914","tid":404237815023429,"cloid":"0x50663504b0f4fedea00080176229d94f","feeToken":"USDC","twapId":null}]}}"#;
1127
1128 let msg: HyperliquidWsMessage = serde_json::from_str(json).unwrap();
1129
1130 match msg {
1131 HyperliquidWsMessage::User { data } => match data {
1132 WsUserEventData::Fills { fills } => {
1133 assert_eq!(fills.len(), 1);
1134 let fill = &fills[0];
1135 assert_eq!(fill.coin, "BTC");
1136 assert_eq!(fill.px, dec!(79146.0));
1137 assert_eq!(fill.side, HyperliquidSide::Sell);
1138 assert_eq!(fill.builder_fee, Some(dec!(0.007914)));
1139 assert_eq!(fill.fee_token, "USDC");
1140 }
1141 _ => panic!("Expected Fills variant"),
1142 },
1143 _ => panic!("Expected User channel message"),
1144 }
1145 }
1146
1147 #[rstest]
1148 fn test_ws_user_fills_message_with_liquidation() {
1149 let json = include_str!("../../test_data/ws_user_fill_liquidation.json");
1152
1153 let msg: HyperliquidWsMessage = serde_json::from_str(json).unwrap();
1154
1155 match msg {
1156 HyperliquidWsMessage::User { data } => match data {
1157 WsUserEventData::Fills { fills } => {
1158 assert_eq!(fills.len(), 1);
1159 let fill = &fills[0];
1160 let liquidation = fill.liquidation.as_ref().expect("expected liquidation");
1161 assert_eq!(fill.coin, "BTC");
1162 assert_eq!(fill.side, HyperliquidSide::Sell);
1163 assert_eq!(liquidation.mark_px, dec!(66607.0));
1164 assert_eq!(liquidation.method, HyperliquidLiquidationMethod::Market);
1165 assert_eq!(
1166 liquidation.liquidated_user.as_deref(),
1167 Some("0x360878d351f05975e25f1807a27895e1e5e004fb"),
1168 );
1169 }
1170 _ => panic!("Expected Fills variant"),
1171 },
1172 _ => panic!("Expected User channel message"),
1173 }
1174 }
1175
1176 #[rstest]
1177 fn test_ws_trade_data_round_trips_decimals_as_strings() {
1178 let json = r#"{"coin":"BTC","side":"B","px":"66653.0","sz":"0.001","hash":"0xabc","time":1,"tid":2,"users":["0xa","0xb"]}"#;
1181
1182 let trade: WsTradeData = serde_json::from_str(json).unwrap();
1183 assert_eq!(trade.px, dec!(66653.0));
1184 assert_eq!(trade.sz, dec!(0.001));
1185
1186 let value = serde_json::to_value(&trade).unwrap();
1187 assert_eq!(value["px"], serde_json::Value::from("66653.0"));
1188 assert_eq!(value["sz"], serde_json::Value::from("0.001"));
1189 }
1190}
1191
1192#[derive(Debug, Clone)]
1199pub enum NautilusWsMessage {
1200 ExecutionReports(Vec<ExecutionReport>),
1202 Trades(Vec<TradeTick>),
1204 Quote(QuoteTick),
1206 Deltas(OrderBookDeltas),
1208 Depth10(Box<OrderBookDepth10>),
1210 Candle(Bar),
1212 MarkPrice(MarkPriceUpdate),
1214 IndexPrice(IndexPriceUpdate),
1216 FundingRate(FundingRateUpdate),
1218 CustomData(Data),
1220 Error(String),
1222 Reconnected,
1224}
1225
1226#[derive(Debug, Clone)]
1231#[allow(
1232 clippy::large_enum_variant,
1233 reason = "the variant size gap only crosses the threshold when high-precision widens the raw types"
1234)]
1235pub enum ExecutionReport {
1236 Order(OrderStatusReport),
1238 Fill(FillReport),
1240}