1use ahash::AHashMap;
17use derive_builder::Builder;
18use nautilus_model::{
19 data::{
20 Bar, Data, FundingRateUpdate, IndexPriceUpdate, MarkPriceUpdate, OrderBookDeltas,
21 OrderBookDepth10, QuoteTick, TradeTick,
22 },
23 reports::{FillReport, OrderStatusReport},
24};
25use serde::{Deserialize, Serialize};
26use ustr::Ustr;
27
28use crate::{
29 common::enums::{
30 HyperliquidBarInterval, HyperliquidFillDirection, HyperliquidLiquidationMethod,
31 HyperliquidOrderStatus as HyperliquidOrderStatusEnum, HyperliquidSide,
32 HyperliquidTimeInForce, HyperliquidTpSl, HyperliquidTwapStatus,
33 },
34 http::models::{HyperliquidExchangeRequest, HyperliquidExecAction},
35};
36
37#[derive(Debug, Clone, Serialize)]
39#[serde(tag = "method")]
40#[serde(rename_all = "lowercase")]
41pub enum HyperliquidWsRequest {
42 Subscribe {
44 subscription: SubscriptionRequest,
46 },
47 Unsubscribe {
49 subscription: SubscriptionRequest,
51 },
52 Post {
54 id: u64,
56 request: PostRequest,
58 },
59 Ping,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
65#[serde(tag = "type")]
66#[serde(rename_all = "camelCase")]
67pub enum SubscriptionRequest {
68 AllMids {
70 #[serde(skip_serializing_if = "Option::is_none")]
71 dex: Option<String>,
72 },
73 AllDexsAssetCtxs,
75 Notification { user: String },
77 WebData2 { user: String },
79 Candle {
81 coin: Ustr,
82 interval: HyperliquidBarInterval,
83 },
84 L2Book {
86 coin: Ustr,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 #[serde(rename = "nSigFigs")]
89 n_sig_figs: Option<u32>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 mantissa: Option<u32>,
92 },
93 Trades { coin: Ustr },
95 OrderUpdates { user: String },
97 UserEvents { user: String },
99 UserFills {
101 user: String,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 #[serde(rename = "aggregateByTime")]
104 aggregate_by_time: Option<bool>,
105 },
106 UserFundings { user: String },
108 UserNonFundingLedgerUpdates { user: String },
110 ActiveAssetCtx { coin: Ustr },
112 ActiveSpotAssetCtx { coin: Ustr },
114 ActiveAssetData { user: String, coin: String },
116 UserTwapSliceFills { user: String },
118 UserTwapHistory { user: String },
120 Bbo { coin: Ustr },
122}
123
124#[derive(Debug, Clone, Serialize)]
126#[serde(tag = "type")]
127#[serde(rename_all = "lowercase")]
128pub enum PostRequest {
129 Info { payload: serde_json::Value },
131 Action {
133 payload: HyperliquidExchangeRequest<HyperliquidExecAction>,
134 },
135}
136
137#[derive(Debug, Clone, Serialize)]
139pub struct ActionPayload {
140 pub action: ActionRequest,
141 pub nonce: u64,
142 pub signature: SignatureData,
143 #[serde(skip_serializing_if = "Option::is_none")]
144 #[serde(rename = "vaultAddress")]
145 pub vault_address: Option<String>,
146}
147
148#[derive(Debug, Clone, Serialize)]
150pub struct SignatureData {
151 pub r: String,
152 pub s: String,
153 pub v: String,
154}
155
156#[derive(Debug, Clone, Serialize)]
158#[serde(tag = "type")]
159#[serde(rename_all = "lowercase")]
160pub enum ActionRequest {
161 Order {
163 orders: Vec<OrderRequest>,
164 grouping: String,
165 },
166 Cancel { cancels: Vec<CancelRequest> },
168 CancelByCloid { cancels: Vec<CancelByCloidRequest> },
170 Modify { modifies: Vec<ModifyRequest> },
172}
173
174impl ActionRequest {
175 pub fn order(orders: Vec<OrderRequest>, grouping: impl Into<String>) -> Self {
182 Self::Order {
183 orders,
184 grouping: grouping.into(),
185 }
186 }
187
188 pub fn cancel(cancels: Vec<CancelRequest>) -> Self {
198 Self::Cancel { cancels }
199 }
200
201 pub fn cancel_by_cloid(cancels: Vec<CancelByCloidRequest>) -> Self {
210 Self::CancelByCloid { cancels }
211 }
212
213 pub fn modify(modifies: Vec<ModifyRequest>) -> Self {
222 Self::Modify { modifies }
223 }
224}
225
226#[derive(Debug, Clone, Serialize, Builder)]
228pub struct OrderRequest {
229 pub a: u32,
231 pub b: bool,
233 pub p: String,
235 pub s: String,
237 pub r: bool,
239 pub t: OrderTypeRequest,
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub c: Option<String>,
244}
245
246#[derive(Debug, Clone, Serialize)]
248#[serde(tag = "type")]
249#[serde(rename_all = "lowercase")]
250pub enum OrderTypeRequest {
251 Limit {
252 tif: TimeInForceRequest,
253 },
254 Trigger {
255 #[serde(rename = "isMarket")]
256 is_market: bool,
257 #[serde(rename = "triggerPx")]
258 trigger_px: String,
259 tpsl: TpSlRequest,
260 },
261}
262
263#[derive(Debug, Clone, Serialize)]
265#[serde(rename_all = "PascalCase")]
266pub enum TimeInForceRequest {
267 Alo,
268 Ioc,
269 Gtc,
270}
271
272#[derive(Debug, Clone, Serialize)]
274#[serde(rename_all = "lowercase")]
275pub enum TpSlRequest {
276 Tp,
277 Sl,
278}
279
280#[derive(Debug, Clone, Serialize)]
282pub struct CancelRequest {
283 pub a: u32,
285 pub o: u64,
287}
288
289#[derive(Debug, Clone, Serialize)]
291pub struct CancelByCloidRequest {
292 pub asset: u32,
294 pub cloid: String,
296}
297
298#[derive(Debug, Clone, Serialize)]
300pub struct ModifyRequest {
301 pub oid: u64,
303 pub order: OrderRequest,
305}
306
307#[derive(Debug, Clone, Deserialize)]
309pub struct SubscriptionResponseData {
310 pub method: String,
311 pub subscription: SubscriptionRequest,
312}
313
314#[derive(Debug, Clone, Deserialize)]
316#[serde(tag = "channel")]
317#[serde(rename_all = "camelCase")]
318pub enum HyperliquidWsMessage {
319 SubscriptionResponse { data: SubscriptionResponseData },
321 Post { data: PostResponse },
323 AllMids { data: AllMidsData },
325 AllDexsAssetCtxs { data: WsAllDexsAssetCtxsData },
327 Notification { data: NotificationData },
329 WebData2 { data: serde_json::Value },
331 Candle { data: CandleData },
333 L2Book { data: WsBookData },
335 Trades { data: Vec<WsTradeData> },
337 OrderUpdates { data: Vec<WsOrderData> },
339 UserEvents { data: WsUserEventData },
341 #[serde(rename = "user")]
343 User { data: WsUserEventData },
344 UserFills { data: WsUserFillsData },
346 UserFundings { data: WsUserFundingsData },
348 UserNonFundingLedgerUpdates { data: serde_json::Value },
350 ActiveAssetCtx { data: WsActiveAssetCtxData },
352 ActiveSpotAssetCtx { data: WsActiveAssetCtxData },
354 ActiveAssetData { data: WsActiveAssetData },
356 UserTwapSliceFills { data: WsUserTwapSliceFillsData },
358 UserTwapHistory { data: WsUserTwapHistoryData },
360 Bbo { data: WsBboData },
362 Error { data: String },
364 Pong,
366}
367
368#[derive(Debug, Clone, Deserialize)]
370pub struct PostResponse {
371 pub id: u64,
372 pub response: PostResponsePayload,
373}
374
375#[derive(Debug, Clone, Deserialize)]
377#[serde(tag = "type")]
378#[serde(rename_all = "lowercase")]
379pub enum PostResponsePayload {
380 Info { payload: serde_json::Value },
381 Action { payload: serde_json::Value },
382 Error { payload: String },
383}
384
385#[derive(Debug, Clone, Deserialize)]
387pub struct AllMidsData {
388 pub mids: AHashMap<Ustr, String>,
389}
390
391#[derive(Debug, Clone, Deserialize)]
393pub struct WsAllDexsAssetCtxsData {
394 pub ctxs: Vec<(String, Vec<PerpsAssetCtx>)>,
395}
396
397#[derive(Debug, Clone, Deserialize)]
399pub struct NotificationData {
400 pub notification: String,
401}
402
403#[derive(Debug, Clone, Deserialize)]
405pub struct CandleData {
406 pub t: u64,
408 #[serde(rename = "T")]
410 pub close_time: u64,
411 pub s: Ustr,
413 pub i: Ustr,
415 pub o: String,
417 pub c: String,
419 pub h: String,
421 pub l: String,
423 pub v: String,
425 pub n: u32,
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize)]
431pub struct WsBookData {
432 pub coin: Ustr,
433 pub levels: [Vec<WsLevelData>; 2], pub time: u64,
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct WsLevelData {
440 pub px: String,
442 pub sz: String,
444 pub n: u32,
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct WsTradeData {
451 pub coin: Ustr,
452 pub side: HyperliquidSide,
453 pub px: String,
454 pub sz: String,
455 pub hash: String,
456 pub time: u64,
457 pub tid: u64,
458 pub users: [String; 2], }
460
461#[derive(Debug, Clone, Deserialize)]
463pub struct WsOrderData {
464 pub order: WsBasicOrderData,
465 pub status: HyperliquidOrderStatusEnum,
466 #[serde(rename = "statusTimestamp")]
467 pub status_timestamp: u64,
468}
469
470#[derive(Debug, Clone, Deserialize)]
472pub struct WsBasicOrderData {
473 pub coin: Ustr,
474 pub side: HyperliquidSide,
475 #[serde(rename = "limitPx")]
476 pub limit_px: String,
477 pub sz: String,
478 pub oid: u64,
479 pub timestamp: u64,
480 #[serde(rename = "origSz")]
481 pub orig_sz: String,
482 pub cloid: Option<String>,
483 pub tif: Option<HyperliquidTimeInForce>,
484 #[serde(rename = "reduceOnly")]
485 pub reduce_only: Option<bool>,
486 #[serde(rename = "triggerPx")]
488 pub trigger_px: Option<String>,
489 #[serde(rename = "isMarket")]
491 pub is_market: Option<bool>,
492 pub tpsl: Option<HyperliquidTpSl>,
494 #[serde(rename = "triggerActivated")]
496 pub trigger_activated: Option<bool>,
497 #[serde(rename = "trailingStop")]
499 pub trailing_stop: Option<WsTrailingStopData>,
500}
501
502#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
504#[serde(rename_all = "camelCase")]
505pub enum TrailingOffsetType {
506 Price,
508 Percentage,
510 BasisPoints,
512}
513
514impl TrailingOffsetType {
515 pub fn format_offset(&self, offset: &str) -> String {
517 match self {
518 Self::Price => offset.to_string(),
519 Self::Percentage => format!("{offset}%"),
520 Self::BasisPoints => format!("{offset} bps"),
521 }
522 }
523}
524
525#[derive(Debug, Clone, Deserialize)]
527pub struct WsTrailingStopData {
528 pub offset: String,
530 #[serde(rename = "offsetType")]
532 pub offset_type: TrailingOffsetType,
533 #[serde(rename = "callbackPrice")]
535 pub callback_price: Option<String>,
536}
537
538#[derive(Debug, Clone, Deserialize)]
540#[serde(untagged)]
541pub enum WsUserEventData {
542 Fills {
543 fills: Vec<WsFillData>,
544 },
545 Funding {
546 funding: WsUserFundingData,
547 },
548 Liquidation {
549 liquidation: WsLiquidationData,
550 },
551 NonUserCancel {
552 #[serde(rename = "nonUserCancel")]
553 non_user_cancel: Vec<WsNonUserCancelData>,
554 },
555 TriggerActivated {
557 #[serde(rename = "triggerActivated")]
558 trigger_activated: WsTriggerActivatedData,
559 },
560 TriggerTriggered {
562 #[serde(rename = "triggerTriggered")]
563 trigger_triggered: WsTriggerTriggeredData,
564 },
565}
566
567#[derive(Debug, Clone, Deserialize)]
569pub struct WsFillData {
570 pub coin: Ustr,
571 pub px: String,
572 pub sz: String,
573 pub side: HyperliquidSide,
574 pub time: u64,
575 #[serde(rename = "startPosition")]
576 pub start_position: String,
577 pub dir: HyperliquidFillDirection,
578 #[serde(rename = "closedPnl")]
579 pub closed_pnl: String,
580 pub hash: String,
581 pub oid: u64,
582 pub crossed: bool,
583 pub fee: String,
584 pub tid: u64,
585 #[serde(default)]
586 pub liquidation: Option<FillLiquidationData>,
587 #[serde(rename = "feeToken")]
588 pub fee_token: Ustr,
589 #[serde(rename = "builderFee")]
590 pub builder_fee: Option<String>,
591 pub cloid: Option<String>,
593 #[serde(rename = "twapId")]
595 pub twap_id: Option<serde_json::Value>,
596}
597
598#[derive(Debug, Clone, Deserialize)]
600pub struct FillLiquidationData {
601 #[serde(rename = "liquidatedUser")]
602 pub liquidated_user: Option<String>,
603 #[serde(rename = "markPx")]
604 pub mark_px: f64,
605 pub method: HyperliquidLiquidationMethod,
606}
607
608#[derive(Debug, Clone, Deserialize)]
610pub struct WsUserFundingData {
611 pub time: u64,
612 pub coin: Ustr,
613 pub usdc: String,
614 pub szi: String,
615 #[serde(rename = "fundingRate")]
616 pub funding_rate: String,
617}
618
619#[derive(Debug, Clone, Deserialize)]
621pub struct WsLiquidationData {
622 pub lid: u64,
623 pub liquidator: String,
624 pub liquidated_user: String,
625 pub liquidated_ntl_pos: String,
626 pub liquidated_account_value: String,
627}
628
629#[derive(Debug, Clone, Deserialize)]
631pub struct WsNonUserCancelData {
632 pub coin: Ustr,
633 pub oid: u64,
634}
635
636#[derive(Debug, Clone, Deserialize)]
638pub struct WsTriggerActivatedData {
639 pub coin: Ustr,
640 pub oid: u64,
641 pub time: u64,
642 #[serde(rename = "triggerPx")]
643 pub trigger_px: String,
644 pub tpsl: HyperliquidTpSl,
645}
646
647#[derive(Debug, Clone, Deserialize)]
649pub struct WsTriggerTriggeredData {
650 pub coin: Ustr,
651 pub oid: u64,
652 pub time: u64,
653 #[serde(rename = "triggerPx")]
654 pub trigger_px: String,
655 #[serde(rename = "marketPx")]
656 pub market_px: String,
657 pub tpsl: HyperliquidTpSl,
658 #[serde(rename = "resultingOid")]
660 pub resulting_oid: Option<u64>,
661}
662
663#[derive(Debug, Clone, Deserialize)]
665pub struct WsUserFillsData {
666 #[serde(rename = "isSnapshot")]
667 pub is_snapshot: Option<bool>,
668 pub user: String,
669 pub fills: Vec<WsFillData>,
670}
671
672#[derive(Debug, Clone, Deserialize)]
674pub struct WsUserFundingsData {
675 #[serde(rename = "isSnapshot")]
676 pub is_snapshot: Option<bool>,
677 pub user: String,
678 pub fundings: Vec<WsUserFundingData>,
679}
680
681#[derive(Debug, Clone, Deserialize)]
683#[serde(untagged)]
684pub enum WsActiveAssetCtxData {
685 Perp { coin: Ustr, ctx: PerpsAssetCtx },
686 Spot { coin: Ustr, ctx: SpotAssetCtx },
687}
688
689#[derive(Debug, Clone, Deserialize)]
691pub struct SharedAssetCtx {
692 #[serde(rename = "dayNtlVlm")]
693 pub day_ntl_vlm: String,
694 #[serde(rename = "prevDayPx")]
695 pub prev_day_px: String,
696 #[serde(rename = "markPx")]
697 pub mark_px: String,
698 #[serde(rename = "midPx")]
699 pub mid_px: Option<String>,
700 #[serde(rename = "impactPxs")]
701 pub impact_pxs: Option<Vec<String>>,
702 #[serde(rename = "dayBaseVlm")]
703 pub day_base_vlm: Option<String>,
704}
705
706#[derive(Debug, Clone, Deserialize)]
708pub struct PerpsAssetCtx {
709 #[serde(flatten)]
710 pub shared: SharedAssetCtx,
711 pub funding: String,
712 #[serde(rename = "openInterest")]
713 pub open_interest: String,
714 #[serde(rename = "oraclePx")]
715 pub oracle_px: String,
716 pub premium: Option<String>,
717}
718
719#[derive(Debug, Clone, Deserialize)]
721pub struct SpotAssetCtx {
722 #[serde(flatten)]
723 pub shared: SharedAssetCtx,
724 #[serde(rename = "circulatingSupply")]
725 pub circulating_supply: String,
726}
727
728#[derive(Debug, Clone, Deserialize)]
730pub struct WsActiveAssetData {
731 pub user: String,
732 pub coin: Ustr,
733 pub leverage: LeverageData,
734 #[serde(rename = "maxTradeSzs")]
735 pub max_trade_szs: [f64; 2],
736 #[serde(rename = "availableToTrade")]
737 pub available_to_trade: [f64; 2],
738}
739
740#[derive(Debug, Clone, Deserialize)]
742pub struct LeverageData {
743 pub value: f64,
744 pub type_: String,
745}
746
747#[derive(Debug, Clone, Deserialize)]
749pub struct WsUserTwapSliceFillsData {
750 #[serde(rename = "isSnapshot")]
751 pub is_snapshot: Option<bool>,
752 pub user: String,
753 #[serde(rename = "twapSliceFills")]
754 pub twap_slice_fills: Vec<WsTwapSliceFillData>,
755}
756
757#[derive(Debug, Clone, Deserialize)]
759pub struct WsTwapSliceFillData {
760 pub fill: WsFillData,
761 #[serde(rename = "twapId")]
762 pub twap_id: u64,
763}
764
765#[derive(Debug, Clone, Deserialize)]
767pub struct WsUserTwapHistoryData {
768 #[serde(rename = "isSnapshot")]
769 pub is_snapshot: Option<bool>,
770 pub user: String,
771 pub history: Vec<WsTwapHistoryData>,
772}
773
774#[derive(Debug, Clone, Deserialize)]
776pub struct WsTwapHistoryData {
777 pub state: TwapStateData,
778 pub status: TwapStatusData,
779 pub time: u64,
780}
781
782#[derive(Debug, Clone, Deserialize)]
784pub struct TwapStateData {
785 pub coin: Ustr,
786 pub user: String,
787 pub side: HyperliquidSide,
788 pub sz: f64,
789 #[serde(rename = "executedSz")]
790 pub executed_sz: f64,
791 #[serde(rename = "executedNtl")]
792 pub executed_ntl: f64,
793 pub minutes: u32,
794 #[serde(rename = "reduceOnly")]
795 pub reduce_only: bool,
796 pub randomize: bool,
797 pub timestamp: u64,
798}
799
800#[derive(Debug, Clone, Deserialize)]
802pub struct TwapStatusData {
803 pub status: HyperliquidTwapStatus,
804 pub description: String,
805}
806
807#[derive(Debug, Clone, Deserialize)]
809pub struct WsBboData {
810 pub coin: Ustr,
811 pub time: u64,
812 pub bbo: [Option<WsLevelData>; 2], }
814
815#[cfg(test)]
816mod tests {
817 use rstest::rstest;
818 use serde_json;
819
820 use super::*;
821
822 #[rstest]
823 fn test_subscription_request_serialization() {
824 let sub = SubscriptionRequest::L2Book {
825 coin: Ustr::from("BTC"),
826 n_sig_figs: Some(5),
827 mantissa: None,
828 };
829
830 let json = serde_json::to_string(&sub).unwrap();
831 assert!(json.contains(r#""type":"l2Book""#));
832 assert!(json.contains(r#""coin":"BTC""#));
833 }
834
835 #[rstest]
836 fn test_hyperliquid_ws_request_serialization() {
837 let req = HyperliquidWsRequest::Subscribe {
838 subscription: SubscriptionRequest::Trades {
839 coin: Ustr::from("ETH"),
840 },
841 };
842
843 let json = serde_json::to_string(&req).unwrap();
844 assert!(json.contains(r#""method":"subscribe""#));
845 assert!(json.contains(r#""type":"trades""#));
846 }
847
848 #[rstest]
849 fn test_order_request_serialization() {
850 let order = OrderRequest {
851 a: 0, b: true, p: "50000.0".to_string(),
854 s: "0.1".to_string(),
855 r: false,
856 t: OrderTypeRequest::Limit {
857 tif: TimeInForceRequest::Gtc,
858 },
859 c: Some("client-123".to_string()),
860 };
861
862 let json = serde_json::to_string(&order).unwrap();
863 assert!(json.contains(r#""a":0"#));
864 assert!(json.contains(r#""b":true"#));
865 assert!(json.contains(r#""p":"50000.0""#));
866 }
867
868 #[rstest]
869 fn test_ws_trade_data_deserialization() {
870 let json = r#"{
871 "coin": "BTC",
872 "side": "B",
873 "px": "50000.0",
874 "sz": "0.1",
875 "hash": "0x123",
876 "time": 1234567890,
877 "tid": 12345,
878 "users": ["0xabc", "0xdef"]
879 }"#;
880
881 let trade: WsTradeData = serde_json::from_str(json).unwrap();
882 assert_eq!(trade.coin, "BTC");
883 assert_eq!(trade.side, HyperliquidSide::Buy);
884 assert_eq!(trade.px, "50000.0");
885 }
886
887 #[rstest]
888 fn test_ws_book_data_deserialization() {
889 let json = r#"{
890 "coin": "ETH",
891 "levels": [
892 [{"px": "3000.0", "sz": "1.0", "n": 1}],
893 [{"px": "3001.0", "sz": "2.0", "n": 2}]
894 ],
895 "time": 1234567890
896 }"#;
897
898 let book: WsBookData = serde_json::from_str(json).unwrap();
899 assert_eq!(book.coin, "ETH");
900 assert_eq!(book.levels[0].len(), 1);
901 assert_eq!(book.levels[1].len(), 1);
902 }
903
904 #[rstest]
905 fn test_ws_trailing_stop_data_deserialization() {
906 let json = r#"{
907 "offset": "100.0",
908 "offsetType": "price",
909 "callbackPrice": "50000.0"
910 }"#;
911
912 let data: WsTrailingStopData = serde_json::from_str(json).unwrap();
913 assert_eq!(data.offset, "100.0");
914 assert_eq!(data.offset_type, TrailingOffsetType::Price);
915 assert_eq!(data.callback_price.unwrap(), "50000.0");
916 }
917
918 #[rstest]
919 fn test_ws_trigger_activated_data_deserialization() {
920 let json = r#"{
921 "coin": "BTC",
922 "oid": 12345,
923 "time": 1704470400000,
924 "triggerPx": "50000.0",
925 "tpsl": "sl"
926 }"#;
927
928 let data: WsTriggerActivatedData = serde_json::from_str(json).unwrap();
929 assert_eq!(data.coin, Ustr::from("BTC"));
930 assert_eq!(data.oid, 12345);
931 assert_eq!(data.trigger_px, "50000.0");
932 assert_eq!(data.tpsl, HyperliquidTpSl::Sl);
933 assert_eq!(data.time, 1704470400000);
934 }
935
936 #[rstest]
937 fn test_ws_trigger_triggered_data_deserialization() {
938 let json = r#"{
939 "coin": "ETH",
940 "oid": 67890,
941 "time": 1704470500000,
942 "triggerPx": "3000.0",
943 "marketPx": "3001.0",
944 "tpsl": "tp",
945 "resultingOid": 99999
946 }"#;
947
948 let data: WsTriggerTriggeredData = serde_json::from_str(json).unwrap();
949 assert_eq!(data.coin, Ustr::from("ETH"));
950 assert_eq!(data.oid, 67890);
951 assert_eq!(data.trigger_px, "3000.0");
952 assert_eq!(data.market_px, "3001.0");
953 assert_eq!(data.tpsl, HyperliquidTpSl::Tp);
954 assert_eq!(data.resulting_oid, Some(99999));
955 }
956
957 #[rstest]
958 fn test_ws_fill_data_deserialization_with_cloid_and_twap() {
959 let json = r#"{
960 "coin": "@107",
961 "px": "31.737",
962 "sz": "0.31",
963 "side": "B",
964 "time": 1769920606068,
965 "startPosition": "0.0",
966 "dir": "Buy",
967 "closedPnl": "0.0",
968 "hash": "0xc731e7561e5334a0c8ab043472ce7d01d400ff3bb95653726afa92a8dd570e8b",
969 "oid": 308086083674,
970 "crossed": true,
971 "fee": "0.00021699",
972 "tid": 812806034449156,
973 "cloid": "0xd211f1c27288259290850338d22132a0",
974 "feeToken": "HYPE",
975 "twapId": null
976 }"#;
977
978 let fill: WsFillData = serde_json::from_str(json).unwrap();
979 assert_eq!(fill.coin, "@107");
980 assert_eq!(fill.px, "31.737");
981 assert_eq!(fill.sz, "0.31");
982 assert_eq!(fill.side, HyperliquidSide::Buy);
983 assert_eq!(fill.oid, 308086083674);
984 assert!(fill.crossed);
985 assert_eq!(fill.fee, "0.00021699");
986 assert_eq!(fill.fee_token, "HYPE");
987 assert_eq!(
988 fill.cloid,
989 Some("0xd211f1c27288259290850338d22132a0".to_string())
990 );
991 assert!(fill.twap_id.is_none() || fill.twap_id == Some(serde_json::Value::Null));
992 }
993
994 #[rstest]
995 fn test_ws_user_fills_message_deserialization() {
996 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}]}}"#;
997
998 let msg: HyperliquidWsMessage = serde_json::from_str(json).unwrap();
999
1000 match msg {
1001 HyperliquidWsMessage::User { data } => match data {
1002 WsUserEventData::Fills { fills } => {
1003 assert_eq!(fills.len(), 1);
1004 let fill = &fills[0];
1005 assert_eq!(fill.coin, "@107");
1006 assert_eq!(fill.px, "31.737");
1007 assert_eq!(
1008 fill.cloid,
1009 Some("0xd211f1c27288259290850338d22132a0".to_string())
1010 );
1011 }
1012 _ => panic!("Expected Fills variant"),
1013 },
1014 _ => panic!("Expected User channel message"),
1015 }
1016 }
1017
1018 #[rstest]
1019 fn test_ws_user_fills_message_with_builder_fee() {
1020 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}]}}"#;
1022
1023 let msg: HyperliquidWsMessage = serde_json::from_str(json).unwrap();
1024
1025 match msg {
1026 HyperliquidWsMessage::User { data } => match data {
1027 WsUserEventData::Fills { fills } => {
1028 assert_eq!(fills.len(), 1);
1029 let fill = &fills[0];
1030 assert_eq!(fill.coin, "BTC");
1031 assert_eq!(fill.px, "79146.0");
1032 assert_eq!(fill.side, HyperliquidSide::Sell);
1033 assert_eq!(fill.builder_fee, Some("0.007914".to_string()));
1034 assert_eq!(fill.fee_token, "USDC");
1035 }
1036 _ => panic!("Expected Fills variant"),
1037 },
1038 _ => panic!("Expected User channel message"),
1039 }
1040 }
1041}
1042
1043#[derive(Debug, Clone)]
1050pub enum NautilusWsMessage {
1051 ExecutionReports(Vec<ExecutionReport>),
1053 Trades(Vec<TradeTick>),
1055 Quote(QuoteTick),
1057 Deltas(OrderBookDeltas),
1059 Depth10(Box<OrderBookDepth10>),
1061 Candle(Bar),
1063 MarkPrice(MarkPriceUpdate),
1065 IndexPrice(IndexPriceUpdate),
1067 FundingRate(FundingRateUpdate),
1069 CustomData(Data),
1071 Error(String),
1073 Reconnected,
1075}
1076
1077#[derive(Debug, Clone)]
1082#[expect(clippy::large_enum_variant)]
1083pub enum ExecutionReport {
1084 Order(OrderStatusReport),
1086 Fill(FillReport),
1088}