1use std::collections::HashMap;
17
18use derive_builder::Builder;
19use nautilus_model::reports::{FillReport, OrderStatusReport};
20use serde::{Deserialize, Serialize};
21use ustr::Ustr;
22
23#[derive(Debug, Clone, Serialize)]
25#[serde(tag = "method")]
26#[serde(rename_all = "lowercase")]
27pub enum HyperliquidWsRequest {
28 Subscribe {
30 subscription: SubscriptionRequest,
32 },
33 Unsubscribe {
35 subscription: SubscriptionRequest,
37 },
38 Post {
40 id: u64,
42 request: PostRequest,
44 },
45 Ping,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
51#[serde(tag = "type")]
52#[serde(rename_all = "camelCase")]
53pub enum SubscriptionRequest {
54 AllMids {
56 #[serde(skip_serializing_if = "Option::is_none")]
57 dex: Option<String>,
58 },
59 Notification { user: String },
61 WebData2 { user: String },
63 Candle { coin: Ustr, interval: String },
65 L2Book {
67 coin: Ustr,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 #[serde(rename = "nSigFigs")]
70 n_sig_figs: Option<u32>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 mantissa: Option<u32>,
73 },
74 Trades { coin: Ustr },
76 OrderUpdates { user: String },
78 UserEvents { user: String },
80 UserFills {
82 user: String,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 #[serde(rename = "aggregateByTime")]
85 aggregate_by_time: Option<bool>,
86 },
87 UserFundings { user: String },
89 UserNonFundingLedgerUpdates { user: String },
91 ActiveAssetCtx { coin: Ustr },
93 ActiveAssetData { user: String, coin: String },
95 UserTwapSliceFills { user: String },
97 UserTwapHistory { user: String },
99 Bbo { coin: Ustr },
101}
102
103#[derive(Debug, Clone, Serialize)]
105#[serde(tag = "type")]
106#[serde(rename_all = "lowercase")]
107pub enum PostRequest {
108 Info { payload: serde_json::Value },
110 Action { payload: ActionPayload },
112}
113
114#[derive(Debug, Clone, Serialize)]
116pub struct ActionPayload {
117 pub action: ActionRequest,
118 pub nonce: u64,
119 pub signature: SignatureData,
120 #[serde(skip_serializing_if = "Option::is_none")]
121 #[serde(rename = "vaultAddress")]
122 pub vault_address: Option<String>,
123}
124
125#[derive(Debug, Clone, Serialize)]
127pub struct SignatureData {
128 pub r: String,
129 pub s: String,
130 pub v: String,
131}
132
133#[derive(Debug, Clone, Serialize)]
135#[serde(tag = "type")]
136#[serde(rename_all = "lowercase")]
137pub enum ActionRequest {
138 Order {
140 orders: Vec<OrderRequest>,
141 grouping: String,
142 },
143 Cancel { cancels: Vec<CancelRequest> },
145 CancelByCloid { cancels: Vec<CancelByCloidRequest> },
147 Modify { modifies: Vec<ModifyRequest> },
149}
150
151impl ActionRequest {
152 pub fn order(orders: Vec<OrderRequest>, grouping: impl Into<String>) -> Self {
159 Self::Order {
160 orders,
161 grouping: grouping.into(),
162 }
163 }
164
165 pub fn cancel(cancels: Vec<CancelRequest>) -> Self {
175 Self::Cancel { cancels }
176 }
177
178 pub fn cancel_by_cloid(cancels: Vec<CancelByCloidRequest>) -> Self {
187 Self::CancelByCloid { cancels }
188 }
189
190 pub fn modify(modifies: Vec<ModifyRequest>) -> Self {
199 Self::Modify { modifies }
200 }
201}
202
203#[derive(Debug, Clone, Serialize, Builder)]
205pub struct OrderRequest {
206 pub a: u32,
208 pub b: bool,
210 pub p: String,
212 pub s: String,
214 pub r: bool,
216 pub t: OrderTypeRequest,
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub c: Option<String>,
221}
222
223#[derive(Debug, Clone, Serialize)]
225#[serde(tag = "type")]
226#[serde(rename_all = "lowercase")]
227pub enum OrderTypeRequest {
228 Limit {
229 tif: TimeInForceRequest,
230 },
231 Trigger {
232 #[serde(rename = "isMarket")]
233 is_market: bool,
234 #[serde(rename = "triggerPx")]
235 trigger_px: String,
236 tpsl: TpSlRequest,
237 },
238}
239
240#[derive(Debug, Clone, Serialize)]
242#[serde(rename_all = "PascalCase")]
243pub enum TimeInForceRequest {
244 Alo,
245 Ioc,
246 Gtc,
247}
248
249#[derive(Debug, Clone, Serialize)]
251#[serde(rename_all = "lowercase")]
252pub enum TpSlRequest {
253 Tp,
254 Sl,
255}
256
257#[derive(Debug, Clone, Serialize)]
259pub struct CancelRequest {
260 pub a: u32,
262 pub o: u64,
264}
265
266#[derive(Debug, Clone, Serialize)]
268pub struct CancelByCloidRequest {
269 pub asset: u32,
271 pub cloid: String,
273}
274
275#[derive(Debug, Clone, Serialize)]
277pub struct ModifyRequest {
278 pub oid: u64,
280 pub order: OrderRequest,
282}
283
284#[derive(Debug, Clone, Deserialize)]
286pub struct SubscriptionResponseData {
287 pub method: String,
288 pub subscription: SubscriptionRequest,
289}
290
291#[derive(Debug, Clone, Deserialize)]
293#[serde(tag = "channel")]
294#[serde(rename_all = "camelCase")]
295pub enum HyperliquidWsMessage {
296 SubscriptionResponse { data: SubscriptionResponseData },
298 Post { data: PostResponse },
300 AllMids { data: AllMidsData },
302 Notification { data: NotificationData },
304 WebData2 { data: serde_json::Value },
306 Candle { data: CandleData },
308 L2Book { data: WsBookData },
310 Trades { data: Vec<WsTradeData> },
312 OrderUpdates { data: Vec<WsOrderData> },
314 UserEvents { data: WsUserEventData },
316 UserFills { data: WsUserFillsData },
318 UserFundings { data: WsUserFundingsData },
320 UserNonFundingLedgerUpdates { data: serde_json::Value },
322 ActiveAssetCtx { data: WsActiveAssetCtxData },
324 ActiveAssetData { data: WsActiveAssetData },
326 UserTwapSliceFills { data: WsUserTwapSliceFillsData },
328 UserTwapHistory { data: WsUserTwapHistoryData },
330 Bbo { data: WsBboData },
332 Pong,
334}
335
336#[derive(Debug, Clone, Deserialize)]
338pub struct PostResponse {
339 pub id: u64,
340 pub response: PostResponsePayload,
341}
342
343#[derive(Debug, Clone, Deserialize)]
345#[serde(tag = "type")]
346#[serde(rename_all = "lowercase")]
347pub enum PostResponsePayload {
348 Info { payload: serde_json::Value },
349 Action { payload: serde_json::Value },
350 Error { payload: String },
351}
352
353#[derive(Debug, Clone, Deserialize)]
355pub struct AllMidsData {
356 pub mids: HashMap<String, String>,
357}
358
359#[derive(Debug, Clone, Deserialize)]
361pub struct NotificationData {
362 pub notification: String,
363}
364
365#[derive(Debug, Clone, Deserialize)]
367pub struct CandleData {
368 pub t: u64,
370 #[serde(rename = "T")]
372 pub close_time: u64,
373 pub s: Ustr,
375 pub i: String,
377 pub o: String,
379 pub c: String,
381 pub h: String,
383 pub l: String,
385 pub v: String,
387 pub n: u32,
389}
390
391#[derive(Debug, Clone, Deserialize)]
393pub struct WsBookData {
394 pub coin: Ustr,
395 pub levels: [Vec<WsLevelData>; 2], pub time: u64,
397}
398
399#[derive(Debug, Clone, Deserialize)]
401pub struct WsLevelData {
402 pub px: String,
404 pub sz: String,
406 pub n: u32,
408}
409
410#[derive(Debug, Clone, Deserialize)]
412pub struct WsTradeData {
413 pub coin: Ustr,
414 pub side: String,
415 pub px: String,
416 pub sz: String,
417 pub hash: String,
418 pub time: u64,
419 pub tid: u64,
420 pub users: [String; 2], }
422
423#[derive(Debug, Clone, Deserialize)]
425pub struct WsOrderData {
426 pub order: WsBasicOrderData,
427 pub status: String,
428 #[serde(rename = "statusTimestamp")]
429 pub status_timestamp: u64,
430}
431
432#[derive(Debug, Clone, Deserialize)]
434pub struct WsBasicOrderData {
435 pub coin: Ustr,
436 pub side: String,
437 #[serde(rename = "limitPx")]
438 pub limit_px: String,
439 pub sz: String,
440 pub oid: u64,
441 pub timestamp: u64,
442 #[serde(rename = "origSz")]
443 pub orig_sz: String,
444 pub cloid: Option<String>,
445 #[serde(rename = "triggerPx")]
447 pub trigger_px: Option<String>,
448 #[serde(rename = "isMarket")]
450 pub is_market: Option<bool>,
451 pub tpsl: Option<String>,
453 #[serde(rename = "triggerActivated")]
455 pub trigger_activated: Option<bool>,
456 #[serde(rename = "trailingStop")]
458 pub trailing_stop: Option<WsTrailingStopData>,
459}
460
461#[derive(Debug, Clone, Deserialize)]
463pub struct WsTrailingStopData {
464 pub offset: String,
466 #[serde(rename = "offsetType")]
468 pub offset_type: String,
469 #[serde(rename = "callbackPrice")]
471 pub callback_price: Option<String>,
472}
473
474#[derive(Debug, Clone, Deserialize)]
476#[serde(untagged)]
477pub enum WsUserEventData {
478 Fills {
479 fills: Vec<WsFillData>,
480 },
481 Funding {
482 funding: WsUserFundingData,
483 },
484 Liquidation {
485 liquidation: WsLiquidationData,
486 },
487 NonUserCancel {
488 #[serde(rename = "nonUserCancel")]
489 non_user_cancel: Vec<WsNonUserCancelData>,
490 },
491 TriggerActivated {
493 #[serde(rename = "triggerActivated")]
494 trigger_activated: WsTriggerActivatedData,
495 },
496 TriggerTriggered {
498 #[serde(rename = "triggerTriggered")]
499 trigger_triggered: WsTriggerTriggeredData,
500 },
501}
502
503#[derive(Debug, Clone, Deserialize)]
505pub struct WsFillData {
506 pub coin: Ustr,
507 pub px: String,
508 pub sz: String,
509 pub side: String,
510 pub time: u64,
511 #[serde(rename = "startPosition")]
512 pub start_position: String,
513 pub dir: String,
514 #[serde(rename = "closedPnl")]
515 pub closed_pnl: String,
516 pub hash: String,
517 pub oid: u64,
518 pub crossed: bool,
519 pub fee: String,
520 pub tid: u64,
521 pub liquidation: Option<FillLiquidationData>,
522 #[serde(rename = "feeToken")]
523 pub fee_token: String,
524 #[serde(rename = "builderFee")]
525 pub builder_fee: Option<String>,
526}
527
528#[derive(Debug, Clone, Deserialize)]
530pub struct FillLiquidationData {
531 #[serde(rename = "liquidatedUser")]
532 pub liquidated_user: Option<String>,
533 #[serde(rename = "markPx")]
534 pub mark_px: f64,
535 pub method: String, }
537
538#[derive(Debug, Clone, Deserialize)]
540pub struct WsUserFundingData {
541 pub time: u64,
542 pub coin: Ustr,
543 pub usdc: String,
544 pub szi: String,
545 #[serde(rename = "fundingRate")]
546 pub funding_rate: String,
547}
548
549#[derive(Debug, Clone, Deserialize)]
551pub struct WsLiquidationData {
552 pub lid: u64,
553 pub liquidator: String,
554 pub liquidated_user: String,
555 pub liquidated_ntl_pos: String,
556 pub liquidated_account_value: String,
557}
558
559#[derive(Debug, Clone, Deserialize)]
561pub struct WsNonUserCancelData {
562 pub coin: Ustr,
563 pub oid: u64,
564}
565
566#[derive(Debug, Clone, Deserialize)]
568pub struct WsTriggerActivatedData {
569 pub coin: Ustr,
570 pub oid: u64,
571 pub time: u64,
572 #[serde(rename = "triggerPx")]
573 pub trigger_px: String,
574 pub tpsl: String,
575}
576
577#[derive(Debug, Clone, Deserialize)]
579pub struct WsTriggerTriggeredData {
580 pub coin: Ustr,
581 pub oid: u64,
582 pub time: u64,
583 #[serde(rename = "triggerPx")]
584 pub trigger_px: String,
585 #[serde(rename = "marketPx")]
586 pub market_px: String,
587 pub tpsl: String,
588 #[serde(rename = "resultingOid")]
590 pub resulting_oid: Option<u64>,
591}
592
593#[derive(Debug, Clone, Deserialize)]
595pub struct WsUserFillsData {
596 #[serde(rename = "isSnapshot")]
597 pub is_snapshot: Option<bool>,
598 pub user: String,
599 pub fills: Vec<WsFillData>,
600}
601
602#[derive(Debug, Clone, Deserialize)]
604pub struct WsUserFundingsData {
605 #[serde(rename = "isSnapshot")]
606 pub is_snapshot: Option<bool>,
607 pub user: String,
608 pub fundings: Vec<WsUserFundingData>,
609}
610
611#[derive(Debug, Clone, Deserialize)]
613#[serde(untagged)]
614pub enum WsActiveAssetCtxData {
615 Perp { coin: Ustr, ctx: PerpsAssetCtx },
616 Spot { coin: Ustr, ctx: SpotAssetCtx },
617}
618
619#[derive(Debug, Clone, Deserialize)]
621pub struct SharedAssetCtx {
622 #[serde(rename = "dayNtlVlm")]
623 pub day_ntl_vlm: f64,
624 #[serde(rename = "prevDayPx")]
625 pub prev_day_px: f64,
626 #[serde(rename = "markPx")]
627 pub mark_px: f64,
628 #[serde(rename = "midPx")]
629 pub mid_px: Option<f64>,
630}
631
632#[derive(Debug, Clone, Deserialize)]
634pub struct PerpsAssetCtx {
635 #[serde(flatten)]
636 pub shared: SharedAssetCtx,
637 pub funding: f64,
638 #[serde(rename = "openInterest")]
639 pub open_interest: f64,
640 #[serde(rename = "oraclePx")]
641 pub oracle_px: f64,
642}
643
644#[derive(Debug, Clone, Deserialize)]
646pub struct SpotAssetCtx {
647 #[serde(flatten)]
648 pub shared: SharedAssetCtx,
649 #[serde(rename = "circulatingSupply")]
650 pub circulating_supply: f64,
651}
652
653#[derive(Debug, Clone, Deserialize)]
655pub struct WsActiveAssetData {
656 pub user: String,
657 pub coin: Ustr,
658 pub leverage: LeverageData,
659 #[serde(rename = "maxTradeSzs")]
660 pub max_trade_szs: [f64; 2],
661 #[serde(rename = "availableToTrade")]
662 pub available_to_trade: [f64; 2],
663}
664
665#[derive(Debug, Clone, Deserialize)]
667pub struct LeverageData {
668 pub value: f64,
669 pub type_: String,
670}
671
672#[derive(Debug, Clone, Deserialize)]
674pub struct WsUserTwapSliceFillsData {
675 #[serde(rename = "isSnapshot")]
676 pub is_snapshot: Option<bool>,
677 pub user: String,
678 #[serde(rename = "twapSliceFills")]
679 pub twap_slice_fills: Vec<WsTwapSliceFillData>,
680}
681
682#[derive(Debug, Clone, Deserialize)]
684pub struct WsTwapSliceFillData {
685 pub fill: WsFillData,
686 #[serde(rename = "twapId")]
687 pub twap_id: u64,
688}
689
690#[derive(Debug, Clone, Deserialize)]
692pub struct WsUserTwapHistoryData {
693 #[serde(rename = "isSnapshot")]
694 pub is_snapshot: Option<bool>,
695 pub user: String,
696 pub history: Vec<WsTwapHistoryData>,
697}
698
699#[derive(Debug, Clone, Deserialize)]
701pub struct WsTwapHistoryData {
702 pub state: TwapStateData,
703 pub status: TwapStatusData,
704 pub time: u64,
705}
706
707#[derive(Debug, Clone, Deserialize)]
709pub struct TwapStateData {
710 pub coin: Ustr,
711 pub user: String,
712 pub side: String,
713 pub sz: f64,
714 #[serde(rename = "executedSz")]
715 pub executed_sz: f64,
716 #[serde(rename = "executedNtl")]
717 pub executed_ntl: f64,
718 pub minutes: u32,
719 #[serde(rename = "reduceOnly")]
720 pub reduce_only: bool,
721 pub randomize: bool,
722 pub timestamp: u64,
723}
724
725#[derive(Debug, Clone, Deserialize)]
727pub struct TwapStatusData {
728 pub status: String, pub description: String,
730}
731
732#[derive(Debug, Clone, Deserialize)]
734pub struct WsBboData {
735 pub coin: Ustr,
736 pub time: u64,
737 pub bbo: [Option<WsLevelData>; 2], }
739
740#[cfg(test)]
745mod tests {
746 use rstest::rstest;
747 use serde_json;
748
749 use super::*;
750
751 #[rstest]
752 fn test_subscription_request_serialization() {
753 let sub = SubscriptionRequest::L2Book {
754 coin: Ustr::from("BTC"),
755 n_sig_figs: Some(5),
756 mantissa: None,
757 };
758
759 let json = serde_json::to_string(&sub).unwrap();
760 assert!(json.contains(r#""type":"l2Book""#));
761 assert!(json.contains(r#""coin":"BTC""#));
762 }
763
764 #[rstest]
765 fn test_hyperliquid_ws_request_serialization() {
766 let req = HyperliquidWsRequest::Subscribe {
767 subscription: SubscriptionRequest::Trades {
768 coin: Ustr::from("ETH"),
769 },
770 };
771
772 let json = serde_json::to_string(&req).unwrap();
773 assert!(json.contains(r#""method":"subscribe""#));
774 assert!(json.contains(r#""type":"trades""#));
775 }
776
777 #[rstest]
778 fn test_order_request_serialization() {
779 let order = OrderRequest {
780 a: 0, b: true, p: "50000.0".to_string(),
783 s: "0.1".to_string(),
784 r: false,
785 t: OrderTypeRequest::Limit {
786 tif: TimeInForceRequest::Gtc,
787 },
788 c: Some("client-123".to_string()),
789 };
790
791 let json = serde_json::to_string(&order).unwrap();
792 assert!(json.contains(r#""a":0"#));
793 assert!(json.contains(r#""b":true"#));
794 assert!(json.contains(r#""p":"50000.0""#));
795 }
796
797 #[rstest]
798 fn test_ws_trade_data_deserialization() {
799 let json = r#"{
800 "coin": "BTC",
801 "side": "B",
802 "px": "50000.0",
803 "sz": "0.1",
804 "hash": "0x123",
805 "time": 1234567890,
806 "tid": 12345,
807 "users": ["0xabc", "0xdef"]
808 }"#;
809
810 let trade: WsTradeData = serde_json::from_str(json).unwrap();
811 assert_eq!(trade.coin, "BTC");
812 assert_eq!(trade.side, "B");
813 assert_eq!(trade.px, "50000.0");
814 }
815
816 #[rstest]
817 fn test_ws_book_data_deserialization() {
818 let json = r#"{
819 "coin": "ETH",
820 "levels": [
821 [{"px": "3000.0", "sz": "1.0", "n": 1}],
822 [{"px": "3001.0", "sz": "2.0", "n": 2}]
823 ],
824 "time": 1234567890
825 }"#;
826
827 let book: WsBookData = serde_json::from_str(json).unwrap();
828 assert_eq!(book.coin, "ETH");
829 assert_eq!(book.levels[0].len(), 1);
830 assert_eq!(book.levels[1].len(), 1);
831 }
832
833 #[rstest]
838 fn test_ws_trailing_stop_data_deserialization() {
839 let json = r#"{
840 "offset": "100.0",
841 "offsetType": "price",
842 "callbackPrice": "50000.0"
843 }"#;
844
845 let data: WsTrailingStopData = serde_json::from_str(json).unwrap();
846 assert_eq!(data.offset, "100.0");
847 assert_eq!(data.offset_type, "price");
848 assert_eq!(data.callback_price.unwrap(), "50000.0");
849 }
850
851 #[rstest]
852 fn test_ws_trigger_activated_data_deserialization() {
853 let json = r#"{
854 "coin": "BTC",
855 "oid": 12345,
856 "time": 1704470400000,
857 "triggerPx": "50000.0",
858 "tpsl": "sl"
859 }"#;
860
861 let data: WsTriggerActivatedData = serde_json::from_str(json).unwrap();
862 assert_eq!(data.coin, Ustr::from("BTC"));
863 assert_eq!(data.oid, 12345);
864 assert_eq!(data.trigger_px, "50000.0");
865 assert_eq!(data.tpsl, "sl");
866 assert_eq!(data.time, 1704470400000);
867 }
868
869 #[rstest]
870 fn test_ws_trigger_triggered_data_deserialization() {
871 let json = r#"{
872 "coin": "ETH",
873 "oid": 67890,
874 "time": 1704470500000,
875 "triggerPx": "3000.0",
876 "marketPx": "3001.0",
877 "tpsl": "tp",
878 "resultingOid": 99999
879 }"#;
880
881 let data: WsTriggerTriggeredData = serde_json::from_str(json).unwrap();
882 assert_eq!(data.coin, Ustr::from("ETH"));
883 assert_eq!(data.oid, 67890);
884 assert_eq!(data.trigger_px, "3000.0");
885 assert_eq!(data.market_px, "3001.0");
886 assert_eq!(data.tpsl, "tp");
887 assert_eq!(data.resulting_oid, Some(99999));
888 }
889}
890
891#[derive(Debug, Clone)]
897pub enum NautilusWsMessage {
898 ExecutionReports(Vec<ExecutionReport>),
900 Data(HyperliquidWsMessage),
902 Error(String),
904 Reconnected,
906}
907
908#[derive(Debug, Clone)]
913#[allow(clippy::large_enum_variant)]
914pub enum ExecutionReport {
915 Order(OrderStatusReport),
917 Fill(FillReport),
919}