1use ahash::AHashMap;
17use derive_builder::Builder;
18use nautilus_model::{
19 data::{
20 Bar, FundingRateUpdate, IndexPriceUpdate, MarkPriceUpdate, OrderBookDeltas, QuoteTick,
21 TradeTick,
22 },
23 reports::{FillReport, OrderStatusReport},
24};
25use serde::{Deserialize, Serialize};
26use ustr::Ustr;
27
28use crate::common::enums::{
29 HyperliquidBarInterval, HyperliquidFillDirection, HyperliquidLiquidationMethod,
30 HyperliquidOrderStatus as HyperliquidOrderStatusEnum, HyperliquidSide, HyperliquidTpSl,
31 HyperliquidTwapStatus,
32};
33
34#[derive(Debug, Clone, Serialize)]
36#[serde(tag = "method")]
37#[serde(rename_all = "lowercase")]
38pub enum HyperliquidWsRequest {
39 Subscribe {
41 subscription: SubscriptionRequest,
43 },
44 Unsubscribe {
46 subscription: SubscriptionRequest,
48 },
49 Post {
51 id: u64,
53 request: PostRequest,
55 },
56 Ping,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
62#[serde(tag = "type")]
63#[serde(rename_all = "camelCase")]
64pub enum SubscriptionRequest {
65 AllMids {
67 #[serde(skip_serializing_if = "Option::is_none")]
68 dex: Option<String>,
69 },
70 Notification { user: String },
72 WebData2 { user: String },
74 Candle {
76 coin: Ustr,
77 interval: HyperliquidBarInterval,
78 },
79 L2Book {
81 coin: Ustr,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 #[serde(rename = "nSigFigs")]
84 n_sig_figs: Option<u32>,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 mantissa: Option<u32>,
87 },
88 Trades { coin: Ustr },
90 OrderUpdates { user: String },
92 UserEvents { user: String },
94 UserFills {
96 user: String,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 #[serde(rename = "aggregateByTime")]
99 aggregate_by_time: Option<bool>,
100 },
101 UserFundings { user: String },
103 UserNonFundingLedgerUpdates { user: String },
105 ActiveAssetCtx { coin: Ustr },
107 ActiveSpotAssetCtx { coin: Ustr },
109 ActiveAssetData { user: String, coin: String },
111 UserTwapSliceFills { user: String },
113 UserTwapHistory { user: String },
115 Bbo { coin: Ustr },
117}
118
119#[derive(Debug, Clone, Serialize)]
121#[serde(tag = "type")]
122#[serde(rename_all = "lowercase")]
123pub enum PostRequest {
124 Info { payload: serde_json::Value },
126 Action { payload: ActionPayload },
128}
129
130#[derive(Debug, Clone, Serialize)]
132pub struct ActionPayload {
133 pub action: ActionRequest,
134 pub nonce: u64,
135 pub signature: SignatureData,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 #[serde(rename = "vaultAddress")]
138 pub vault_address: Option<String>,
139}
140
141#[derive(Debug, Clone, Serialize)]
143pub struct SignatureData {
144 pub r: String,
145 pub s: String,
146 pub v: String,
147}
148
149#[derive(Debug, Clone, Serialize)]
151#[serde(tag = "type")]
152#[serde(rename_all = "lowercase")]
153pub enum ActionRequest {
154 Order {
156 orders: Vec<OrderRequest>,
157 grouping: String,
158 },
159 Cancel { cancels: Vec<CancelRequest> },
161 CancelByCloid { cancels: Vec<CancelByCloidRequest> },
163 Modify { modifies: Vec<ModifyRequest> },
165}
166
167impl ActionRequest {
168 pub fn order(orders: Vec<OrderRequest>, grouping: impl Into<String>) -> Self {
175 Self::Order {
176 orders,
177 grouping: grouping.into(),
178 }
179 }
180
181 pub fn cancel(cancels: Vec<CancelRequest>) -> Self {
191 Self::Cancel { cancels }
192 }
193
194 pub fn cancel_by_cloid(cancels: Vec<CancelByCloidRequest>) -> Self {
203 Self::CancelByCloid { cancels }
204 }
205
206 pub fn modify(modifies: Vec<ModifyRequest>) -> Self {
215 Self::Modify { modifies }
216 }
217}
218
219#[derive(Debug, Clone, Serialize, Builder)]
221pub struct OrderRequest {
222 pub a: u32,
224 pub b: bool,
226 pub p: String,
228 pub s: String,
230 pub r: bool,
232 pub t: OrderTypeRequest,
234 #[serde(skip_serializing_if = "Option::is_none")]
236 pub c: Option<String>,
237}
238
239#[derive(Debug, Clone, Serialize)]
241#[serde(tag = "type")]
242#[serde(rename_all = "lowercase")]
243pub enum OrderTypeRequest {
244 Limit {
245 tif: TimeInForceRequest,
246 },
247 Trigger {
248 #[serde(rename = "isMarket")]
249 is_market: bool,
250 #[serde(rename = "triggerPx")]
251 trigger_px: String,
252 tpsl: TpSlRequest,
253 },
254}
255
256#[derive(Debug, Clone, Serialize)]
258#[serde(rename_all = "PascalCase")]
259pub enum TimeInForceRequest {
260 Alo,
261 Ioc,
262 Gtc,
263}
264
265#[derive(Debug, Clone, Serialize)]
267#[serde(rename_all = "lowercase")]
268pub enum TpSlRequest {
269 Tp,
270 Sl,
271}
272
273#[derive(Debug, Clone, Serialize)]
275pub struct CancelRequest {
276 pub a: u32,
278 pub o: u64,
280}
281
282#[derive(Debug, Clone, Serialize)]
284pub struct CancelByCloidRequest {
285 pub asset: u32,
287 pub cloid: String,
289}
290
291#[derive(Debug, Clone, Serialize)]
293pub struct ModifyRequest {
294 pub oid: u64,
296 pub order: OrderRequest,
298}
299
300#[derive(Debug, Clone, Deserialize)]
302pub struct SubscriptionResponseData {
303 pub method: String,
304 pub subscription: SubscriptionRequest,
305}
306
307#[derive(Debug, Clone, Deserialize)]
309#[serde(tag = "channel")]
310#[serde(rename_all = "camelCase")]
311pub enum HyperliquidWsMessage {
312 SubscriptionResponse { data: SubscriptionResponseData },
314 Post { data: PostResponse },
316 AllMids { data: AllMidsData },
318 Notification { data: NotificationData },
320 WebData2 { data: serde_json::Value },
322 Candle { data: CandleData },
324 L2Book { data: WsBookData },
326 Trades { data: Vec<WsTradeData> },
328 OrderUpdates { data: Vec<WsOrderData> },
330 UserEvents { data: WsUserEventData },
332 #[serde(rename = "user")]
334 User { data: WsUserEventData },
335 UserFills { data: WsUserFillsData },
337 UserFundings { data: WsUserFundingsData },
339 UserNonFundingLedgerUpdates { data: serde_json::Value },
341 ActiveAssetCtx { data: WsActiveAssetCtxData },
343 ActiveSpotAssetCtx { data: WsActiveAssetCtxData },
345 ActiveAssetData { data: WsActiveAssetData },
347 UserTwapSliceFills { data: WsUserTwapSliceFillsData },
349 UserTwapHistory { data: WsUserTwapHistoryData },
351 Bbo { data: WsBboData },
353 Error { data: String },
355 Pong,
357}
358
359#[derive(Debug, Clone, Deserialize)]
361pub struct PostResponse {
362 pub id: u64,
363 pub response: PostResponsePayload,
364}
365
366#[derive(Debug, Clone, Deserialize)]
368#[serde(tag = "type")]
369#[serde(rename_all = "lowercase")]
370pub enum PostResponsePayload {
371 Info { payload: serde_json::Value },
372 Action { payload: serde_json::Value },
373 Error { payload: String },
374}
375
376#[derive(Debug, Clone, Deserialize)]
378pub struct AllMidsData {
379 pub mids: AHashMap<String, String>,
380}
381
382#[derive(Debug, Clone, Deserialize)]
384pub struct NotificationData {
385 pub notification: String,
386}
387
388#[derive(Debug, Clone, Deserialize)]
390pub struct CandleData {
391 pub t: u64,
393 #[serde(rename = "T")]
395 pub close_time: u64,
396 pub s: Ustr,
398 pub i: String,
400 pub o: String,
402 pub c: String,
404 pub h: String,
406 pub l: String,
408 pub v: String,
410 pub n: u32,
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct WsBookData {
417 pub coin: Ustr,
418 pub levels: [Vec<WsLevelData>; 2], pub time: u64,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct WsLevelData {
425 pub px: String,
427 pub sz: String,
429 pub n: u32,
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
435pub struct WsTradeData {
436 pub coin: Ustr,
437 pub side: HyperliquidSide,
438 pub px: String,
439 pub sz: String,
440 pub hash: String,
441 pub time: u64,
442 pub tid: u64,
443 pub users: [String; 2], }
445
446#[derive(Debug, Clone, Deserialize)]
448pub struct WsOrderData {
449 pub order: WsBasicOrderData,
450 pub status: HyperliquidOrderStatusEnum,
451 #[serde(rename = "statusTimestamp")]
452 pub status_timestamp: u64,
453}
454
455#[derive(Debug, Clone, Deserialize)]
457pub struct WsBasicOrderData {
458 pub coin: Ustr,
459 pub side: HyperliquidSide,
460 #[serde(rename = "limitPx")]
461 pub limit_px: String,
462 pub sz: String,
463 pub oid: u64,
464 pub timestamp: u64,
465 #[serde(rename = "origSz")]
466 pub orig_sz: String,
467 pub cloid: Option<String>,
468 #[serde(rename = "triggerPx")]
470 pub trigger_px: Option<String>,
471 #[serde(rename = "isMarket")]
473 pub is_market: Option<bool>,
474 pub tpsl: Option<HyperliquidTpSl>,
476 #[serde(rename = "triggerActivated")]
478 pub trigger_activated: Option<bool>,
479 #[serde(rename = "trailingStop")]
481 pub trailing_stop: Option<WsTrailingStopData>,
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
486#[serde(rename_all = "camelCase")]
487pub enum TrailingOffsetType {
488 Price,
490 Percentage,
492 BasisPoints,
494}
495
496impl TrailingOffsetType {
497 pub fn format_offset(&self, offset: &str) -> String {
499 match self {
500 Self::Price => offset.to_string(),
501 Self::Percentage => format!("{offset}%"),
502 Self::BasisPoints => format!("{offset} bps"),
503 }
504 }
505}
506
507#[derive(Debug, Clone, Deserialize)]
509pub struct WsTrailingStopData {
510 pub offset: String,
512 #[serde(rename = "offsetType")]
514 pub offset_type: TrailingOffsetType,
515 #[serde(rename = "callbackPrice")]
517 pub callback_price: Option<String>,
518}
519
520#[derive(Debug, Clone, Deserialize)]
522#[serde(untagged)]
523pub enum WsUserEventData {
524 Fills {
525 fills: Vec<WsFillData>,
526 },
527 Funding {
528 funding: WsUserFundingData,
529 },
530 Liquidation {
531 liquidation: WsLiquidationData,
532 },
533 NonUserCancel {
534 #[serde(rename = "nonUserCancel")]
535 non_user_cancel: Vec<WsNonUserCancelData>,
536 },
537 TriggerActivated {
539 #[serde(rename = "triggerActivated")]
540 trigger_activated: WsTriggerActivatedData,
541 },
542 TriggerTriggered {
544 #[serde(rename = "triggerTriggered")]
545 trigger_triggered: WsTriggerTriggeredData,
546 },
547}
548
549#[derive(Debug, Clone, Deserialize)]
551pub struct WsFillData {
552 pub coin: Ustr,
553 pub px: String,
554 pub sz: String,
555 pub side: HyperliquidSide,
556 pub time: u64,
557 #[serde(rename = "startPosition")]
558 pub start_position: String,
559 pub dir: HyperliquidFillDirection,
560 #[serde(rename = "closedPnl")]
561 pub closed_pnl: String,
562 pub hash: String,
563 pub oid: u64,
564 pub crossed: bool,
565 pub fee: String,
566 pub tid: u64,
567 #[serde(default)]
568 pub liquidation: Option<FillLiquidationData>,
569 #[serde(rename = "feeToken")]
570 pub fee_token: String,
571 #[serde(rename = "builderFee")]
572 pub builder_fee: Option<String>,
573 pub cloid: Option<String>,
575 #[serde(rename = "twapId")]
577 pub twap_id: Option<serde_json::Value>,
578}
579
580#[derive(Debug, Clone, Deserialize)]
582pub struct FillLiquidationData {
583 #[serde(rename = "liquidatedUser")]
584 pub liquidated_user: Option<String>,
585 #[serde(rename = "markPx")]
586 pub mark_px: f64,
587 pub method: HyperliquidLiquidationMethod,
588}
589
590#[derive(Debug, Clone, Deserialize)]
592pub struct WsUserFundingData {
593 pub time: u64,
594 pub coin: Ustr,
595 pub usdc: String,
596 pub szi: String,
597 #[serde(rename = "fundingRate")]
598 pub funding_rate: String,
599}
600
601#[derive(Debug, Clone, Deserialize)]
603pub struct WsLiquidationData {
604 pub lid: u64,
605 pub liquidator: String,
606 pub liquidated_user: String,
607 pub liquidated_ntl_pos: String,
608 pub liquidated_account_value: String,
609}
610
611#[derive(Debug, Clone, Deserialize)]
613pub struct WsNonUserCancelData {
614 pub coin: Ustr,
615 pub oid: u64,
616}
617
618#[derive(Debug, Clone, Deserialize)]
620pub struct WsTriggerActivatedData {
621 pub coin: Ustr,
622 pub oid: u64,
623 pub time: u64,
624 #[serde(rename = "triggerPx")]
625 pub trigger_px: String,
626 pub tpsl: HyperliquidTpSl,
627}
628
629#[derive(Debug, Clone, Deserialize)]
631pub struct WsTriggerTriggeredData {
632 pub coin: Ustr,
633 pub oid: u64,
634 pub time: u64,
635 #[serde(rename = "triggerPx")]
636 pub trigger_px: String,
637 #[serde(rename = "marketPx")]
638 pub market_px: String,
639 pub tpsl: HyperliquidTpSl,
640 #[serde(rename = "resultingOid")]
642 pub resulting_oid: Option<u64>,
643}
644
645#[derive(Debug, Clone, Deserialize)]
647pub struct WsUserFillsData {
648 #[serde(rename = "isSnapshot")]
649 pub is_snapshot: Option<bool>,
650 pub user: String,
651 pub fills: Vec<WsFillData>,
652}
653
654#[derive(Debug, Clone, Deserialize)]
656pub struct WsUserFundingsData {
657 #[serde(rename = "isSnapshot")]
658 pub is_snapshot: Option<bool>,
659 pub user: String,
660 pub fundings: Vec<WsUserFundingData>,
661}
662
663#[derive(Debug, Clone, Deserialize)]
665#[serde(untagged)]
666pub enum WsActiveAssetCtxData {
667 Perp { coin: Ustr, ctx: PerpsAssetCtx },
668 Spot { coin: Ustr, ctx: SpotAssetCtx },
669}
670
671#[derive(Debug, Clone, Deserialize)]
673pub struct SharedAssetCtx {
674 #[serde(rename = "dayNtlVlm")]
675 pub day_ntl_vlm: String,
676 #[serde(rename = "prevDayPx")]
677 pub prev_day_px: String,
678 #[serde(rename = "markPx")]
679 pub mark_px: String,
680 #[serde(rename = "midPx")]
681 pub mid_px: Option<String>,
682 #[serde(rename = "impactPxs")]
683 pub impact_pxs: Option<Vec<String>>,
684 #[serde(rename = "dayBaseVlm")]
685 pub day_base_vlm: Option<String>,
686}
687
688#[derive(Debug, Clone, Deserialize)]
690pub struct PerpsAssetCtx {
691 #[serde(flatten)]
692 pub shared: SharedAssetCtx,
693 pub funding: String,
694 #[serde(rename = "openInterest")]
695 pub open_interest: String,
696 #[serde(rename = "oraclePx")]
697 pub oracle_px: String,
698 pub premium: Option<String>,
699}
700
701#[derive(Debug, Clone, Deserialize)]
703pub struct SpotAssetCtx {
704 #[serde(flatten)]
705 pub shared: SharedAssetCtx,
706 #[serde(rename = "circulatingSupply")]
707 pub circulating_supply: String,
708}
709
710#[derive(Debug, Clone, Deserialize)]
712pub struct WsActiveAssetData {
713 pub user: String,
714 pub coin: Ustr,
715 pub leverage: LeverageData,
716 #[serde(rename = "maxTradeSzs")]
717 pub max_trade_szs: [f64; 2],
718 #[serde(rename = "availableToTrade")]
719 pub available_to_trade: [f64; 2],
720}
721
722#[derive(Debug, Clone, Deserialize)]
724pub struct LeverageData {
725 pub value: f64,
726 pub type_: String,
727}
728
729#[derive(Debug, Clone, Deserialize)]
731pub struct WsUserTwapSliceFillsData {
732 #[serde(rename = "isSnapshot")]
733 pub is_snapshot: Option<bool>,
734 pub user: String,
735 #[serde(rename = "twapSliceFills")]
736 pub twap_slice_fills: Vec<WsTwapSliceFillData>,
737}
738
739#[derive(Debug, Clone, Deserialize)]
741pub struct WsTwapSliceFillData {
742 pub fill: WsFillData,
743 #[serde(rename = "twapId")]
744 pub twap_id: u64,
745}
746
747#[derive(Debug, Clone, Deserialize)]
749pub struct WsUserTwapHistoryData {
750 #[serde(rename = "isSnapshot")]
751 pub is_snapshot: Option<bool>,
752 pub user: String,
753 pub history: Vec<WsTwapHistoryData>,
754}
755
756#[derive(Debug, Clone, Deserialize)]
758pub struct WsTwapHistoryData {
759 pub state: TwapStateData,
760 pub status: TwapStatusData,
761 pub time: u64,
762}
763
764#[derive(Debug, Clone, Deserialize)]
766pub struct TwapStateData {
767 pub coin: Ustr,
768 pub user: String,
769 pub side: HyperliquidSide,
770 pub sz: f64,
771 #[serde(rename = "executedSz")]
772 pub executed_sz: f64,
773 #[serde(rename = "executedNtl")]
774 pub executed_ntl: f64,
775 pub minutes: u32,
776 #[serde(rename = "reduceOnly")]
777 pub reduce_only: bool,
778 pub randomize: bool,
779 pub timestamp: u64,
780}
781
782#[derive(Debug, Clone, Deserialize)]
784pub struct TwapStatusData {
785 pub status: HyperliquidTwapStatus,
786 pub description: String,
787}
788
789#[derive(Debug, Clone, Deserialize)]
791pub struct WsBboData {
792 pub coin: Ustr,
793 pub time: u64,
794 pub bbo: [Option<WsLevelData>; 2], }
796
797#[cfg(test)]
798mod tests {
799 use rstest::rstest;
800 use serde_json;
801
802 use super::*;
803
804 #[rstest]
805 fn test_subscription_request_serialization() {
806 let sub = SubscriptionRequest::L2Book {
807 coin: Ustr::from("BTC"),
808 n_sig_figs: Some(5),
809 mantissa: None,
810 };
811
812 let json = serde_json::to_string(&sub).unwrap();
813 assert!(json.contains(r#""type":"l2Book""#));
814 assert!(json.contains(r#""coin":"BTC""#));
815 }
816
817 #[rstest]
818 fn test_hyperliquid_ws_request_serialization() {
819 let req = HyperliquidWsRequest::Subscribe {
820 subscription: SubscriptionRequest::Trades {
821 coin: Ustr::from("ETH"),
822 },
823 };
824
825 let json = serde_json::to_string(&req).unwrap();
826 assert!(json.contains(r#""method":"subscribe""#));
827 assert!(json.contains(r#""type":"trades""#));
828 }
829
830 #[rstest]
831 fn test_order_request_serialization() {
832 let order = OrderRequest {
833 a: 0, b: true, p: "50000.0".to_string(),
836 s: "0.1".to_string(),
837 r: false,
838 t: OrderTypeRequest::Limit {
839 tif: TimeInForceRequest::Gtc,
840 },
841 c: Some("client-123".to_string()),
842 };
843
844 let json = serde_json::to_string(&order).unwrap();
845 assert!(json.contains(r#""a":0"#));
846 assert!(json.contains(r#""b":true"#));
847 assert!(json.contains(r#""p":"50000.0""#));
848 }
849
850 #[rstest]
851 fn test_ws_trade_data_deserialization() {
852 let json = r#"{
853 "coin": "BTC",
854 "side": "B",
855 "px": "50000.0",
856 "sz": "0.1",
857 "hash": "0x123",
858 "time": 1234567890,
859 "tid": 12345,
860 "users": ["0xabc", "0xdef"]
861 }"#;
862
863 let trade: WsTradeData = serde_json::from_str(json).unwrap();
864 assert_eq!(trade.coin, "BTC");
865 assert_eq!(trade.side, HyperliquidSide::Buy);
866 assert_eq!(trade.px, "50000.0");
867 }
868
869 #[rstest]
870 fn test_ws_book_data_deserialization() {
871 let json = r#"{
872 "coin": "ETH",
873 "levels": [
874 [{"px": "3000.0", "sz": "1.0", "n": 1}],
875 [{"px": "3001.0", "sz": "2.0", "n": 2}]
876 ],
877 "time": 1234567890
878 }"#;
879
880 let book: WsBookData = serde_json::from_str(json).unwrap();
881 assert_eq!(book.coin, "ETH");
882 assert_eq!(book.levels[0].len(), 1);
883 assert_eq!(book.levels[1].len(), 1);
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 #[rstest]
940 fn test_ws_fill_data_deserialization_with_cloid_and_twap() {
941 let json = r#"{
942 "coin": "@107",
943 "px": "31.737",
944 "sz": "0.31",
945 "side": "B",
946 "time": 1769920606068,
947 "startPosition": "0.0",
948 "dir": "Buy",
949 "closedPnl": "0.0",
950 "hash": "0xc731e7561e5334a0c8ab043472ce7d01d400ff3bb95653726afa92a8dd570e8b",
951 "oid": 308086083674,
952 "crossed": true,
953 "fee": "0.00021699",
954 "tid": 812806034449156,
955 "cloid": "0xd211f1c27288259290850338d22132a0",
956 "feeToken": "HYPE",
957 "twapId": null
958 }"#;
959
960 let fill: WsFillData = serde_json::from_str(json).unwrap();
961 assert_eq!(fill.coin, "@107");
962 assert_eq!(fill.px, "31.737");
963 assert_eq!(fill.sz, "0.31");
964 assert_eq!(fill.side, HyperliquidSide::Buy);
965 assert_eq!(fill.oid, 308086083674);
966 assert!(fill.crossed);
967 assert_eq!(fill.fee, "0.00021699");
968 assert_eq!(fill.fee_token, "HYPE");
969 assert_eq!(
970 fill.cloid,
971 Some("0xd211f1c27288259290850338d22132a0".to_string())
972 );
973 assert!(fill.twap_id.is_none() || fill.twap_id == Some(serde_json::Value::Null));
974 }
975
976 #[rstest]
977 fn test_ws_user_fills_message_deserialization() {
978 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}]}}"#;
979
980 let msg: HyperliquidWsMessage = serde_json::from_str(json).unwrap();
981
982 match msg {
983 HyperliquidWsMessage::User { data } => match data {
984 WsUserEventData::Fills { fills } => {
985 assert_eq!(fills.len(), 1);
986 let fill = &fills[0];
987 assert_eq!(fill.coin, "@107");
988 assert_eq!(fill.px, "31.737");
989 assert_eq!(
990 fill.cloid,
991 Some("0xd211f1c27288259290850338d22132a0".to_string())
992 );
993 }
994 _ => panic!("Expected Fills variant"),
995 },
996 _ => panic!("Expected User channel message"),
997 }
998 }
999
1000 #[rstest]
1001 fn test_ws_user_fills_message_with_builder_fee() {
1002 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}]}}"#;
1004
1005 let msg: HyperliquidWsMessage = serde_json::from_str(json).unwrap();
1006
1007 match msg {
1008 HyperliquidWsMessage::User { data } => match data {
1009 WsUserEventData::Fills { fills } => {
1010 assert_eq!(fills.len(), 1);
1011 let fill = &fills[0];
1012 assert_eq!(fill.coin, "BTC");
1013 assert_eq!(fill.px, "79146.0");
1014 assert_eq!(fill.side, HyperliquidSide::Sell);
1015 assert_eq!(fill.builder_fee, Some("0.007914".to_string()));
1016 assert_eq!(fill.fee_token, "USDC");
1017 }
1018 _ => panic!("Expected Fills variant"),
1019 },
1020 _ => panic!("Expected User channel message"),
1021 }
1022 }
1023}
1024
1025#[derive(Debug, Clone)]
1032pub enum NautilusWsMessage {
1033 ExecutionReports(Vec<ExecutionReport>),
1035 Trades(Vec<TradeTick>),
1037 Quote(QuoteTick),
1039 Deltas(OrderBookDeltas),
1041 Candle(Bar),
1043 MarkPrice(MarkPriceUpdate),
1045 IndexPrice(IndexPriceUpdate),
1047 FundingRate(FundingRateUpdate),
1049 Error(String),
1051 Reconnected,
1053}
1054
1055#[derive(Debug, Clone)]
1060#[allow(clippy::large_enum_variant)]
1061pub enum ExecutionReport {
1062 Order(OrderStatusReport),
1064 Fill(FillReport),
1066}