1use std::borrow::Cow;
17
18use ahash::AHashMap;
19use derive_builder::Builder;
20#[cfg(test)]
21use nautilus_core::string::secret::REDACTED;
22use nautilus_core::{
23 serialization::{
24 decimal, deserialize_decimal, deserialize_decimal_from_str,
25 deserialize_optional_decimal_from_str, serialize_decimal_as_str,
26 },
27 string::secret::SecretString,
28};
29use nautilus_model::{
30 data::{
31 Bar, Data, FundingRateUpdate, IndexPriceUpdate, MarkPriceUpdate, OrderBookDeltas,
32 OrderBookDepth, QuoteTick, TradeTick,
33 },
34 identifiers::InstrumentId,
35 reports::{FillReport, OrderStatusReport},
36};
37use rust_decimal::Decimal;
38use serde::{Deserialize, Deserializer, Serialize, de::Error};
39use serde_json::value::RawValue;
40use ustr::Ustr;
41
42use crate::{
43 common::{
44 enums::{
45 HyperliquidBarInterval, HyperliquidFillDirection, HyperliquidLiquidationMethod,
46 HyperliquidOrderStatus as HyperliquidOrderStatusEnum, HyperliquidSide,
47 HyperliquidTimeInForce, HyperliquidTpSl, HyperliquidTwapStatus,
48 },
49 parse::is_false_or_none,
50 },
51 http::models::{HyperliquidExchangeAction, HyperliquidExchangeRequest, LeverageInfo},
52};
53
54#[derive(Debug, Clone, Serialize)]
56#[serde(tag = "method")]
57#[serde(rename_all = "lowercase")]
58pub enum HyperliquidWsRequest {
59 Subscribe {
61 subscription: SubscriptionRequest,
63 },
64 Unsubscribe {
66 subscription: SubscriptionRequest,
68 },
69 Post {
71 id: u64,
73 request: PostRequest,
75 },
76 Ping,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
82#[serde(tag = "type")]
83#[serde(rename_all = "camelCase")]
84pub enum SubscriptionRequest {
85 AllMids {
87 #[serde(skip_serializing_if = "Option::is_none")]
88 dex: Option<String>,
89 },
90 AllDexsAssetCtxs,
92 Notification { user: String },
94 WebData2 { user: String },
96 Candle {
98 coin: Ustr,
99 interval: HyperliquidBarInterval,
100 },
101 L2Book {
103 coin: Ustr,
104 #[serde(skip_serializing_if = "Option::is_none")]
105 #[serde(rename = "nSigFigs")]
106 n_sig_figs: Option<u32>,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 mantissa: Option<u32>,
109 },
110 Trades { coin: Ustr },
112 OrderUpdates { user: String },
114 UserEvents { user: String },
116 UserFills {
118 user: String,
119 #[serde(skip_serializing_if = "Option::is_none")]
120 #[serde(rename = "aggregateByTime")]
121 aggregate_by_time: Option<bool>,
122 },
123 UserFundings { user: String },
125 UserNonFundingLedgerUpdates { user: String },
127 ActiveAssetCtx { coin: Ustr },
129 ActiveSpotAssetCtx { coin: Ustr },
131 ActiveAssetData { user: String, coin: String },
133 UserTwapSliceFills { user: String },
135 UserTwapHistory { user: String },
137 Bbo { coin: Ustr },
139}
140
141#[derive(Debug, Clone, Serialize)]
143#[serde(tag = "type")]
144#[serde(rename_all = "lowercase")]
145pub enum PostRequest {
146 Info { payload: serde_json::Value },
148 Action {
150 payload: HyperliquidExchangeRequest<HyperliquidExchangeAction>,
151 },
152}
153
154#[derive(Debug, Clone, Serialize)]
156pub struct ActionPayload {
157 pub action: ActionRequest,
158 pub nonce: u64,
159 pub signature: SignatureData,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 #[serde(rename = "vaultAddress")]
162 pub vault_address: Option<String>,
163}
164
165#[derive(Debug, Clone, Serialize)]
167pub struct SignatureData {
168 pub r: SecretString,
169 pub s: SecretString,
170 pub v: SecretString,
171}
172
173#[derive(Debug, Clone, Serialize)]
177#[serde(tag = "type")]
178#[serde(rename_all = "lowercase")]
179pub enum ActionRequest {
180 Order {
182 orders: Vec<OrderRequest>,
183 grouping: String,
184 },
185 Cancel {
187 cancels: Vec<CancelRequest>,
188 #[serde(rename = "f", skip_serializing_if = "is_false_or_none")]
189 fast: Option<bool>,
190 },
191 CancelByCloid {
193 cancels: Vec<CancelByCloidRequest>,
194 #[serde(rename = "f", skip_serializing_if = "is_false_or_none")]
195 fast: Option<bool>,
196 },
197 Modify { modifies: Vec<ModifyRequest> },
199}
200
201impl ActionRequest {
202 pub fn order(orders: Vec<OrderRequest>, grouping: impl Into<String>) -> Self {
209 Self::Order {
210 orders,
211 grouping: grouping.into(),
212 }
213 }
214
215 pub fn cancel(cancels: Vec<CancelRequest>) -> Self {
225 Self::Cancel {
226 cancels,
227 fast: None,
228 }
229 }
230
231 pub fn cancel_by_cloid(cancels: Vec<CancelByCloidRequest>) -> Self {
240 Self::CancelByCloid {
241 cancels,
242 fast: None,
243 }
244 }
245
246 pub fn modify(modifies: Vec<ModifyRequest>) -> Self {
255 Self::Modify { modifies }
256 }
257}
258
259#[derive(Debug, Clone, Serialize, Builder)]
261pub struct OrderRequest {
262 pub a: u32,
264 pub b: bool,
266 pub p: String,
268 pub s: String,
270 pub r: bool,
272 pub t: OrderTypeRequest,
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub c: Option<String>,
277}
278
279#[derive(Debug, Clone, Serialize)]
281#[serde(tag = "type")]
282#[serde(rename_all = "lowercase")]
283pub enum OrderTypeRequest {
284 Limit {
285 tif: TimeInForceRequest,
286 },
287 Trigger {
288 #[serde(rename = "isMarket")]
289 is_market: bool,
290 #[serde(rename = "triggerPx")]
291 trigger_px: String,
292 tpsl: TpSlRequest,
293 },
294}
295
296#[derive(Debug, Clone, Serialize)]
298#[serde(rename_all = "PascalCase")]
299pub enum TimeInForceRequest {
300 Alo,
301 Ioc,
302 Gtc,
303}
304
305#[derive(Debug, Clone, Serialize)]
307#[serde(rename_all = "lowercase")]
308pub enum TpSlRequest {
309 Tp,
310 Sl,
311}
312
313#[derive(Debug, Clone, Serialize)]
315pub struct CancelRequest {
316 pub a: u32,
318 pub o: u64,
320}
321
322#[derive(Debug, Clone, Serialize)]
324pub struct CancelByCloidRequest {
325 pub asset: u32,
327 pub cloid: String,
329}
330
331#[derive(Debug, Clone, Serialize)]
333pub struct ModifyRequest {
334 pub oid: u64,
336 pub order: OrderRequest,
338}
339
340#[derive(Debug, Clone, Deserialize)]
342pub struct SubscriptionResponseData {
343 pub method: String,
344 pub subscription: SubscriptionRequest,
345}
346
347#[derive(Debug, Clone, Deserialize)]
349#[serde(remote = "Self", tag = "channel")]
350#[serde(rename_all = "camelCase")]
351pub enum HyperliquidWsMessage {
352 SubscriptionResponse { data: SubscriptionResponseData },
354 Post { data: PostResponse },
356 AllMids { data: AllMidsData },
358 AllDexsAssetCtxs { data: WsAllDexsAssetCtxsData },
360 Notification { data: NotificationData },
362 WebData2 { data: serde_json::Value },
364 Candle { data: CandleData },
366 L2Book { data: WsBookData },
368 Trades { data: Vec<WsTradeData> },
370 OrderUpdates { data: Vec<WsOrderData> },
372 UserEvents { data: WsUserEventData },
374 #[serde(rename = "user")]
376 User { data: WsUserEventData },
377 UserFills { data: WsUserFillsData },
379 UserFundings { data: WsUserFundingsData },
381 UserNonFundingLedgerUpdates { data: serde_json::Value },
383 ActiveAssetCtx { data: WsActiveAssetCtxData },
385 ActiveSpotAssetCtx { data: WsActiveAssetCtxData },
387 ActiveAssetData { data: WsActiveAssetData },
389 UserTwapSliceFills { data: WsUserTwapSliceFillsData },
391 UserTwapHistory { data: WsUserTwapHistoryData },
393 Bbo { data: WsBboData },
395 Error { data: String },
397 Pong,
399}
400
401impl<'de> Deserialize<'de> for HyperliquidWsMessage {
402 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
403 let raw = Box::<RawValue>::deserialize(deserializer)?;
404 decode_ws_message(raw.get()).map_err(D::Error::custom)
405 }
406}
407
408pub(crate) fn decode_ws_message(text: &str) -> serde_json::Result<HyperliquidWsMessage> {
409 let header: WsMessageHeader = serde_json::from_str(text)?;
410
411 if header.channel == "activeAssetData" {
413 let message: WsActiveAssetDataMessage = serde_json::from_str(text)?;
414 return Ok(HyperliquidWsMessage::ActiveAssetData { data: message.data });
415 }
416
417 HyperliquidWsMessage::deserialize(&mut serde_json::Deserializer::from_str(text))
418}
419
420#[derive(Deserialize)]
421struct WsMessageHeader<'a> {
422 #[serde(borrow)]
423 channel: Cow<'a, str>,
424}
425
426#[derive(Deserialize)]
427struct WsActiveAssetDataMessage {
428 data: WsActiveAssetData,
429}
430
431#[derive(Debug, Clone, Deserialize)]
433pub struct PostResponse {
434 pub id: u64,
435 pub response: PostResponsePayload,
436}
437
438#[derive(Debug, Clone, Deserialize)]
440#[serde(tag = "type")]
441#[serde(rename_all = "lowercase")]
442pub enum PostResponsePayload {
443 Info { payload: serde_json::Value },
444 Action { payload: serde_json::Value },
445 Error { payload: String },
446}
447
448#[derive(Debug, Clone, Deserialize)]
450pub struct AllMidsData {
451 pub mids: AHashMap<Ustr, String>,
452}
453
454#[derive(Debug, Clone, Deserialize)]
456pub struct WsAllDexsAssetCtxsData {
457 pub ctxs: Vec<(String, Vec<PerpsAssetCtx>)>,
458}
459
460#[derive(Debug, Clone, Deserialize)]
462pub struct NotificationData {
463 pub notification: String,
464}
465
466#[derive(Debug, Clone, Deserialize)]
468pub struct CandleData {
469 pub t: u64,
471 #[serde(rename = "T")]
473 pub close_time: u64,
474 pub s: Ustr,
476 pub i: Ustr,
478 #[serde(deserialize_with = "deserialize_decimal_from_str")]
480 pub o: Decimal,
481 #[serde(deserialize_with = "deserialize_decimal_from_str")]
483 pub c: Decimal,
484 #[serde(deserialize_with = "deserialize_decimal_from_str")]
486 pub h: Decimal,
487 #[serde(deserialize_with = "deserialize_decimal_from_str")]
489 pub l: Decimal,
490 #[serde(deserialize_with = "deserialize_decimal_from_str")]
492 pub v: Decimal,
493 pub n: u32,
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct WsBookData {
500 pub coin: Ustr,
501 pub levels: [Vec<WsLevelData>; 2], pub time: u64,
503}
504
505#[derive(Debug, Clone, Serialize, Deserialize)]
507pub struct WsLevelData {
508 #[serde(
510 deserialize_with = "deserialize_decimal_from_str",
511 serialize_with = "serialize_decimal_as_str"
512 )]
513 pub px: Decimal,
514 #[serde(
516 deserialize_with = "deserialize_decimal_from_str",
517 serialize_with = "serialize_decimal_as_str"
518 )]
519 pub sz: Decimal,
520 pub n: u32,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct WsTradeData {
527 pub coin: Ustr,
528 pub side: HyperliquidSide,
529 #[serde(
530 deserialize_with = "deserialize_decimal_from_str",
531 serialize_with = "serialize_decimal_as_str"
532 )]
533 pub px: Decimal,
534 #[serde(
535 deserialize_with = "deserialize_decimal_from_str",
536 serialize_with = "serialize_decimal_as_str"
537 )]
538 pub sz: Decimal,
539 pub hash: String,
540 pub time: u64,
541 pub tid: u64,
542 pub users: [String; 2], }
544
545#[derive(Debug, Clone, Deserialize)]
547pub struct WsOrderData {
548 pub order: WsBasicOrderData,
549 pub status: HyperliquidOrderStatusEnum,
550 #[serde(rename = "statusTimestamp")]
551 pub status_timestamp: u64,
552}
553
554#[derive(Debug, Clone, Deserialize)]
556pub struct WsBasicOrderData {
557 pub coin: Ustr,
558 pub side: HyperliquidSide,
559 #[serde(rename = "limitPx", deserialize_with = "deserialize_decimal_from_str")]
560 pub limit_px: Decimal,
561 #[serde(deserialize_with = "deserialize_decimal_from_str")]
562 pub sz: Decimal,
563 pub oid: u64,
564 pub timestamp: u64,
565 #[serde(rename = "origSz", deserialize_with = "deserialize_decimal_from_str")]
566 pub orig_sz: Decimal,
567 pub cloid: Option<String>,
568 pub tif: Option<HyperliquidTimeInForce>,
569 #[serde(rename = "reduceOnly")]
570 pub reduce_only: Option<bool>,
571 #[serde(
573 rename = "triggerPx",
574 default,
575 deserialize_with = "deserialize_optional_decimal_from_str"
576 )]
577 pub trigger_px: Option<Decimal>,
578 #[serde(rename = "isMarket")]
580 pub is_market: Option<bool>,
581 pub tpsl: Option<HyperliquidTpSl>,
583 #[serde(rename = "triggerActivated")]
585 pub trigger_activated: Option<bool>,
586 #[serde(rename = "trailingStop")]
588 pub trailing_stop: Option<WsTrailingStopData>,
589 #[serde(rename = "orderType", default)]
592 pub order_type: Option<String>,
593}
594
595#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
597#[serde(rename_all = "camelCase")]
598pub enum TrailingOffsetType {
599 Price,
601 Percentage,
603 BasisPoints,
605}
606
607impl TrailingOffsetType {
608 pub fn format_offset(&self, offset: &str) -> String {
610 match self {
611 Self::Price => offset.to_string(),
612 Self::Percentage => format!("{offset}%"),
613 Self::BasisPoints => format!("{offset} bps"),
614 }
615 }
616}
617
618#[derive(Debug, Clone, Deserialize)]
620pub struct WsTrailingStopData {
621 #[serde(deserialize_with = "deserialize_decimal_from_str")]
623 pub offset: Decimal,
624 #[serde(rename = "offsetType")]
626 pub offset_type: TrailingOffsetType,
627 #[serde(
629 rename = "callbackPrice",
630 default,
631 deserialize_with = "deserialize_optional_decimal_from_str"
632 )]
633 pub callback_price: Option<Decimal>,
634}
635
636#[derive(Debug, Clone, Deserialize)]
638#[serde(untagged)]
639pub enum WsUserEventData {
640 Fills {
641 fills: Vec<WsFillData>,
642 },
643 Funding {
644 funding: WsUserFundingData,
645 },
646 Liquidation {
647 liquidation: WsLiquidationData,
648 },
649 NonUserCancel {
650 #[serde(rename = "nonUserCancel")]
651 non_user_cancel: Vec<WsNonUserCancelData>,
652 },
653 TriggerActivated {
655 #[serde(rename = "triggerActivated")]
656 trigger_activated: WsTriggerActivatedData,
657 },
658 TriggerTriggered {
660 #[serde(rename = "triggerTriggered")]
661 trigger_triggered: WsTriggerTriggeredData,
662 },
663}
664
665#[derive(Debug, Clone, Deserialize)]
667pub struct WsFillData {
668 pub coin: Ustr,
669 #[serde(deserialize_with = "deserialize_decimal_from_str")]
670 pub px: Decimal,
671 #[serde(deserialize_with = "deserialize_decimal_from_str")]
672 pub sz: Decimal,
673 pub side: HyperliquidSide,
674 pub time: u64,
675 #[serde(
676 rename = "startPosition",
677 deserialize_with = "deserialize_decimal_from_str"
678 )]
679 pub start_position: Decimal,
680 pub dir: HyperliquidFillDirection,
681 #[serde(
682 rename = "closedPnl",
683 deserialize_with = "deserialize_decimal_from_str"
684 )]
685 pub closed_pnl: Decimal,
686 pub hash: String,
687 pub oid: u64,
688 pub crossed: bool,
689 #[serde(deserialize_with = "deserialize_decimal_from_str")]
690 pub fee: Decimal,
691 pub tid: u64,
692 #[serde(default)]
693 pub liquidation: Option<FillLiquidationData>,
694 #[serde(rename = "feeToken")]
695 pub fee_token: Ustr,
696 #[serde(
697 rename = "builderFee",
698 default,
699 deserialize_with = "deserialize_optional_decimal_from_str"
700 )]
701 pub builder_fee: Option<Decimal>,
702 pub cloid: Option<String>,
704 #[serde(rename = "twapId")]
706 pub twap_id: Option<serde_json::Value>,
707}
708
709#[derive(Debug, Clone, Deserialize)]
711pub struct FillLiquidationData {
712 #[serde(rename = "liquidatedUser")]
713 pub liquidated_user: Option<String>,
714 #[serde(rename = "markPx", deserialize_with = "deserialize_decimal_from_str")]
715 pub mark_px: Decimal,
716 pub method: HyperliquidLiquidationMethod,
717}
718
719#[derive(Debug, Clone, Deserialize)]
721pub struct WsUserFundingData {
722 pub time: u64,
723 pub coin: Ustr,
724 #[serde(deserialize_with = "deserialize_decimal_from_str")]
725 pub usdc: Decimal,
726 #[serde(deserialize_with = "deserialize_decimal_from_str")]
727 pub szi: Decimal,
728 #[serde(
729 rename = "fundingRate",
730 deserialize_with = "deserialize_decimal_from_str"
731 )]
732 pub funding_rate: Decimal,
733}
734
735#[derive(Debug, Clone, Deserialize)]
737pub struct WsLiquidationData {
738 pub lid: u64,
739 pub liquidator: String,
740 pub liquidated_user: String,
741 #[serde(deserialize_with = "deserialize_decimal_from_str")]
742 pub liquidated_ntl_pos: Decimal,
743 #[serde(deserialize_with = "deserialize_decimal_from_str")]
744 pub liquidated_account_value: Decimal,
745}
746
747#[derive(Debug, Clone, Deserialize)]
749pub struct WsNonUserCancelData {
750 pub coin: Ustr,
751 pub oid: u64,
752}
753
754#[derive(Debug, Clone, Deserialize)]
756pub struct WsTriggerActivatedData {
757 pub coin: Ustr,
758 pub oid: u64,
759 pub time: u64,
760 #[serde(
761 rename = "triggerPx",
762 deserialize_with = "deserialize_decimal_from_str"
763 )]
764 pub trigger_px: Decimal,
765 pub tpsl: HyperliquidTpSl,
766}
767
768#[derive(Debug, Clone, Deserialize)]
770pub struct WsTriggerTriggeredData {
771 pub coin: Ustr,
772 pub oid: u64,
773 pub time: u64,
774 #[serde(
775 rename = "triggerPx",
776 deserialize_with = "deserialize_decimal_from_str"
777 )]
778 pub trigger_px: Decimal,
779 #[serde(rename = "marketPx", deserialize_with = "deserialize_decimal_from_str")]
780 pub market_px: Decimal,
781 pub tpsl: HyperliquidTpSl,
782 #[serde(rename = "resultingOid")]
784 pub resulting_oid: Option<u64>,
785}
786
787#[derive(Debug, Clone, Deserialize)]
789pub struct WsUserFillsData {
790 #[serde(rename = "isSnapshot")]
791 pub is_snapshot: Option<bool>,
792 pub user: String,
793 pub fills: Vec<WsFillData>,
794}
795
796#[derive(Debug, Clone, Deserialize)]
798pub struct WsUserFundingsData {
799 #[serde(rename = "isSnapshot")]
800 pub is_snapshot: Option<bool>,
801 pub user: String,
802 pub fundings: Vec<WsUserFundingData>,
803}
804
805#[derive(Debug, Clone, Deserialize)]
807#[serde(untagged)]
808pub enum WsActiveAssetCtxData {
809 Perp { coin: Ustr, ctx: PerpsAssetCtx },
810 Spot { coin: Ustr, ctx: SpotAssetCtx },
811}
812
813#[derive(Debug, Clone, Deserialize)]
815pub struct SharedAssetCtx {
816 #[serde(
817 rename = "dayNtlVlm",
818 deserialize_with = "deserialize_decimal_from_str"
819 )]
820 pub day_ntl_vlm: Decimal,
821 #[serde(
822 rename = "prevDayPx",
823 deserialize_with = "deserialize_decimal_from_str"
824 )]
825 pub prev_day_px: Decimal,
826 #[serde(rename = "markPx", deserialize_with = "deserialize_decimal_from_str")]
827 pub mark_px: Decimal,
828 #[serde(
829 rename = "midPx",
830 default,
831 deserialize_with = "deserialize_optional_decimal_from_str"
832 )]
833 pub mid_px: Option<Decimal>,
834 #[serde(rename = "impactPxs")]
835 pub impact_pxs: Option<Vec<Decimal>>,
836 #[serde(
837 rename = "dayBaseVlm",
838 default,
839 deserialize_with = "deserialize_optional_decimal_from_str"
840 )]
841 pub day_base_vlm: Option<Decimal>,
842}
843
844#[derive(Debug, Clone, Deserialize)]
846pub struct PerpsAssetCtx {
847 #[serde(flatten)]
848 pub shared: SharedAssetCtx,
849 #[serde(deserialize_with = "deserialize_decimal_from_str")]
850 pub funding: Decimal,
851 #[serde(
852 rename = "openInterest",
853 deserialize_with = "deserialize_decimal_from_str"
854 )]
855 pub open_interest: Decimal,
856 #[serde(rename = "oraclePx", deserialize_with = "deserialize_decimal_from_str")]
857 pub oracle_px: Decimal,
858 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
859 pub premium: Option<Decimal>,
860}
861
862#[derive(Debug, Clone, Deserialize)]
864pub struct SpotAssetCtx {
865 #[serde(flatten)]
866 pub shared: SharedAssetCtx,
867 #[serde(
868 rename = "circulatingSupply",
869 deserialize_with = "deserialize_decimal_from_str"
870 )]
871 pub circulating_supply: Decimal,
872}
873
874#[derive(Debug, Clone, Deserialize)]
876pub struct WsActiveAssetData {
877 pub user: String,
878 pub coin: Ustr,
879 pub leverage: LeverageInfo,
880 #[serde(rename = "maxTradeSzs", deserialize_with = "deserialize_trade_limits")]
881 pub max_trade_szs: [Decimal; 2],
882 #[serde(
883 rename = "availableToTrade",
884 deserialize_with = "deserialize_trade_limits"
885 )]
886 pub available_to_trade: [Decimal; 2],
887}
888
889fn deserialize_trade_limits<'de, D: Deserializer<'de>>(
890 deserializer: D,
891) -> Result<[Decimal; 2], D::Error> {
892 <[DecimalToken; 2]>::deserialize(deserializer).map(|values| values.map(|value| value.0))
893}
894
895#[derive(Deserialize)]
896struct DecimalToken(#[serde(deserialize_with = "decimal::deserialize_json")] Decimal);
897
898#[derive(Debug, Clone, Deserialize)]
900pub struct WsUserTwapSliceFillsData {
901 #[serde(rename = "isSnapshot")]
902 pub is_snapshot: Option<bool>,
903 pub user: String,
904 #[serde(rename = "twapSliceFills")]
905 pub twap_slice_fills: Vec<WsTwapSliceFillData>,
906}
907
908#[derive(Debug, Clone, Deserialize)]
910pub struct WsTwapSliceFillData {
911 pub fill: WsFillData,
912 #[serde(rename = "twapId")]
913 pub twap_id: u64,
914}
915
916#[derive(Debug, Clone, Deserialize)]
918pub struct WsUserTwapHistoryData {
919 #[serde(rename = "isSnapshot")]
920 pub is_snapshot: Option<bool>,
921 pub user: String,
922 pub history: Vec<WsTwapHistoryData>,
923}
924
925#[derive(Debug, Clone, Deserialize)]
927pub struct WsTwapHistoryData {
928 pub state: TwapStateData,
929 pub status: TwapStatusData,
930 pub time: u64,
931 #[serde(default, rename = "twapId")]
932 pub twap_id: Option<u64>,
933}
934
935#[derive(Debug, Clone, Deserialize)]
937pub struct TwapStateData {
938 pub coin: Ustr,
939 pub user: String,
940 pub side: HyperliquidSide,
941 #[serde(deserialize_with = "deserialize_decimal")]
943 pub sz: Decimal,
944 #[serde(rename = "executedSz", deserialize_with = "deserialize_decimal")]
945 pub executed_sz: Decimal,
946 #[serde(rename = "executedNtl", deserialize_with = "deserialize_decimal")]
947 pub executed_ntl: Decimal,
948 pub minutes: u32,
949 #[serde(rename = "reduceOnly")]
950 pub reduce_only: bool,
951 pub randomize: bool,
952 pub timestamp: u64,
953}
954
955#[derive(Debug, Clone, Deserialize)]
957pub struct TwapStatusData {
958 pub status: HyperliquidTwapStatus,
959 #[serde(default)]
961 pub description: String,
962}
963
964#[derive(Debug, Clone, Deserialize)]
966pub struct WsBboData {
967 pub coin: Ustr,
968 pub time: u64,
969 pub bbo: [Option<WsLevelData>; 2], }
971
972#[cfg(test)]
973mod tests {
974 use rstest::rstest;
975 use rust_decimal_macros::dec;
976 use serde_json;
977
978 use super::*;
979 use crate::common::enums::HyperliquidLeverageType;
980
981 #[rstest]
982 fn test_cancel_action_serialization(
983 #[values(None, Some(false), Some(true))] fast: Option<bool>,
984 #[values(false, true)] by_cloid: bool,
985 ) {
986 let (action, name) = if by_cloid {
987 (
988 ActionRequest::CancelByCloid {
989 cancels: vec![CancelByCloidRequest {
990 asset: 7,
991 cloid: "0x0123456789abcdef0123456789abcdef".to_string(),
992 }],
993 fast,
994 },
995 "ws_cancel_by_cloid",
996 )
997 } else {
998 (
999 ActionRequest::Cancel {
1000 cancels: vec![CancelRequest { a: 7, o: 12345 }],
1001 fast,
1002 },
1003 "cancel",
1004 )
1005 };
1006
1007 let fixtures: serde_json::Value =
1008 serde_json::from_str(include_str!("../../test_data/cancel_actions.json")).unwrap();
1009 let expected = &fixtures[name][if fast == Some(true) { "fast" } else { "normal" }];
1010 let json = serde_json::to_value(&action).unwrap();
1011 let msgpack = rmp_serde::to_vec_named(&action).unwrap();
1012
1013 assert_eq!(json, expected["json"]);
1014 assert_eq!(
1015 nautilus_core::hex::encode(msgpack),
1016 expected["msgpack"].as_str().unwrap()
1017 );
1018 }
1019
1020 #[rstest]
1021 #[case("cross", HyperliquidLeverageType::Cross)]
1022 #[case("isolated", HyperliquidLeverageType::Isolated)]
1023 fn test_ws_active_asset_data_preserves_decimals(
1024 #[case] leverage_type: &str,
1025 #[case] expected_type: HyperliquidLeverageType,
1026 #[values("number", "string", "scientific")] encoding: &str,
1027 #[values(false, true)] serde_entry: bool,
1028 #[values(false, true)] channel_last: bool,
1029 ) {
1030 let max_trade_szs = [
1031 dec!(9007199254740993.01),
1032 dec!(0.0000000000000000000000000001),
1033 ];
1034 let available_to_trade = [
1035 dec!(0.1234567890123456789012345678),
1036 dec!(1234567890.1234567890123456789),
1037 ];
1038 let mut json = include_str!("../../test_data/ws_active_asset_data_decimal.json")
1039 .replace("\"cross\"", &format!("\"{leverage_type}\""));
1040
1041 if encoding != "number" {
1042 for value in max_trade_szs.into_iter().chain(available_to_trade) {
1043 let text = value.to_string();
1044
1045 let encoded = match encoding {
1046 "string" => format!("\"{text}\""),
1047 "scientific" => format!("{text}e0"),
1048 _ => unreachable!(),
1049 };
1050
1051 json = json.replace(&text, &encoded);
1052 }
1053 }
1054
1055 if channel_last {
1056 json = json
1057 .replace(" \"channel\": \"activeAssetData\",\n", "")
1058 .trim_end()
1059 .strip_suffix('}')
1060 .unwrap()
1061 .to_string()
1062 + ",\"channel\":\"activeAssetData\"}";
1063 }
1064
1065 let msg = if serde_entry {
1066 serde_json::from_str::<HyperliquidWsMessage>(&json).unwrap()
1067 } else {
1068 decode_ws_message(&json).unwrap()
1069 };
1070
1071 let HyperliquidWsMessage::ActiveAssetData { data } = msg else {
1072 panic!("Expected ActiveAssetData message");
1073 };
1074
1075 assert_eq!(data.user, "0x0000000000000000000000000000000000000001");
1076 assert_eq!(data.coin, "BTC");
1077 assert_eq!(data.leverage.leverage_type, expected_type);
1078 assert_eq!(data.leverage.value, 7);
1079 assert_eq!(data.max_trade_szs, max_trade_szs);
1080 assert_eq!(data.available_to_trade, available_to_trade);
1081 }
1082
1083 #[rstest]
1084 #[case(
1085 "[1]",
1086 serde_json::error::Category::Data,
1087 "invalid length 1, expected an array of length 2"
1088 )]
1089 #[case(
1090 "[1, 2, 3]",
1091 serde_json::error::Category::Syntax,
1092 "trailing characters"
1093 )]
1094 #[case(
1095 "[null, 2]",
1096 serde_json::error::Category::Data,
1097 "invalid decimal significand"
1098 )]
1099 #[case(
1100 "[\"not-a-number\", 2]",
1101 serde_json::error::Category::Data,
1102 "invalid decimal exponent"
1103 )]
1104 #[case(
1105 "[\"\", 2]",
1106 serde_json::error::Category::Data,
1107 "invalid decimal significand"
1108 )]
1109 #[case(
1110 "[true, 2]",
1111 serde_json::error::Category::Data,
1112 "invalid decimal exponent"
1113 )]
1114 #[case(
1115 "[{}, 2]",
1116 serde_json::error::Category::Data,
1117 "invalid decimal significand"
1118 )]
1119 #[case(
1120 "[0.00000000000000000000000000001, 2]",
1121 serde_json::error::Category::Data,
1122 "Number has a high precision that can not be represented."
1123 )]
1124 fn test_ws_active_asset_data_rejects_invalid_trade_limits(
1125 #[values("maxTradeSzs", "availableToTrade")] field: &str,
1126 #[case] values: &str,
1127 #[case] category: serde_json::error::Category,
1128 #[case] expected: &str,
1129 ) {
1130 let mut json =
1131 include_str!("../../test_data/ws_active_asset_data_decimal.json").to_string();
1132 let key = format!("\"{field}\": ");
1133 let start = json.find(&key).unwrap() + key.len();
1134 let end = start + json[start..].find(']').unwrap() + 1;
1135 json.replace_range(start..end, values);
1136 let error = decode_ws_message(&json).unwrap_err();
1137
1138 assert_eq!(error.classify(), category);
1139 assert_eq!(
1140 error.to_string().split(" at line ").next().unwrap(),
1141 expected
1142 );
1143 }
1144
1145 #[rstest]
1146 #[case(r#"{"channel":"pong"}"#)]
1147 #[case(r#"{"channel":"p\u006fng"}"#)]
1148 fn test_decode_ws_message_pong(#[case] json: &str) {
1149 let message = decode_ws_message(json).unwrap();
1150
1151 assert!(matches!(message, HyperliquidWsMessage::Pong));
1152 }
1153
1154 #[rstest]
1155 #[case("{}", "missing field `channel`")]
1156 #[case(r#"{"channel":"activeAssetData"}"#, "missing field `data`")]
1157 #[case(
1158 r#"{"channel":"pong","channel":"activeAssetData"}"#,
1159 "duplicate field `channel`"
1160 )]
1161 fn test_decode_ws_message_rejects_invalid_envelope(#[case] json: &str, #[case] expected: &str) {
1162 let error = decode_ws_message(json).unwrap_err();
1163
1164 assert_eq!(error.classify(), serde_json::error::Category::Data);
1165 assert_eq!(
1166 error.to_string().split(" at line ").next().unwrap(),
1167 expected
1168 );
1169 }
1170
1171 #[rstest]
1172 fn test_ws_active_asset_ctx_preserves_decimals() {
1173 let json = include_str!("../../test_data/ws_active_asset_ctx_decimal.json");
1174 let msg: HyperliquidWsMessage = serde_json::from_str(json).unwrap();
1175
1176 let HyperliquidWsMessage::ActiveAssetCtx {
1177 data: WsActiveAssetCtxData::Perp { coin, ctx },
1178 } = msg
1179 else {
1180 panic!("Expected perpetual ActiveAssetCtx message");
1181 };
1182
1183 assert_eq!(coin, "BTC");
1184 assert_eq!(ctx.shared.day_ntl_vlm, dec!(1234567890.1234567890123456789));
1185 assert_eq!(ctx.shared.prev_day_px, dec!(49000.12345678901234567890123));
1186 assert_eq!(ctx.shared.mark_px, dec!(50000.12345678901234567890123));
1187 assert_eq!(ctx.shared.mid_px, Some(dec!(50001.12345678901234567890123)));
1188 assert_eq!(
1189 ctx.shared.impact_pxs,
1190 Some(vec![
1191 dec!(50002.12345678901234567890123),
1192 dec!(50003.12345678901234567890123),
1193 ])
1194 );
1195 assert_eq!(
1196 ctx.shared.day_base_vlm,
1197 Some(dec!(12345.12345678901234567890123))
1198 );
1199 assert_eq!(ctx.funding, dec!(-0.0000000000000000000000000001));
1200 assert_eq!(ctx.open_interest, dec!(9007199254740993.01));
1201 assert_eq!(ctx.oracle_px, dec!(50004.12345678901234567890123));
1202 assert_eq!(ctx.premium, Some(dec!(0.0000000000000000000000000002)));
1203 }
1204
1205 #[rstest]
1206 #[case(true)]
1207 #[case(false)]
1208 fn test_ws_active_asset_ctx_optional_decimals(#[case] explicit_null: bool) {
1209 let mut json: serde_json::Value = serde_json::from_str(include_str!(
1210 "../../test_data/ws_active_asset_ctx_decimal.json"
1211 ))
1212 .unwrap();
1213 let ctx = json["data"]["ctx"].as_object_mut().unwrap();
1214
1215 for field in ["midPx", "impactPxs", "dayBaseVlm", "premium"] {
1216 if explicit_null {
1217 ctx.insert(field.to_string(), serde_json::Value::Null);
1218 } else {
1219 ctx.remove(field);
1220 }
1221 }
1222
1223 let msg: HyperliquidWsMessage = serde_json::from_value(json).unwrap();
1224
1225 let HyperliquidWsMessage::ActiveAssetCtx {
1226 data: WsActiveAssetCtxData::Perp { ctx, .. },
1227 } = msg
1228 else {
1229 panic!("Expected perpetual ActiveAssetCtx message");
1230 };
1231
1232 assert_eq!(ctx.shared.mid_px, None);
1233 assert_eq!(ctx.shared.impact_pxs, None);
1234 assert_eq!(ctx.shared.day_base_vlm, None);
1235 assert_eq!(ctx.premium, None);
1236 }
1237
1238 #[rstest]
1239 fn test_ws_active_spot_asset_ctx_preserves_decimals() {
1240 let json = include_str!("../../test_data/ws_active_spot_asset_ctx_decimal.json");
1241 let msg: HyperliquidWsMessage = serde_json::from_str(json).unwrap();
1242
1243 let HyperliquidWsMessage::ActiveSpotAssetCtx {
1244 data: WsActiveAssetCtxData::Spot { coin, ctx },
1245 } = msg
1246 else {
1247 panic!("Expected spot ActiveAssetCtx message");
1248 };
1249
1250 assert_eq!(coin, "@107");
1251 assert_eq!(ctx.shared.day_ntl_vlm, dec!(1234567890.1234567890123456789));
1252 assert_eq!(ctx.shared.prev_day_px, dec!(31.12345678901234567890123456));
1253 assert_eq!(ctx.shared.mark_px, dec!(32.12345678901234567890123456));
1254 assert_eq!(ctx.shared.mid_px, None);
1255 assert_eq!(ctx.shared.impact_pxs, None);
1256 assert_eq!(ctx.shared.day_base_vlm, None);
1257 assert_eq!(ctx.circulating_supply, dec!(9007199254740993.01));
1258 }
1259
1260 #[rstest]
1261 fn test_signature_data_serialization_and_debug_redaction() {
1262 let signature = SignatureData {
1263 r: SecretString::from("0xsignature-r"),
1264 s: SecretString::from("0xsignature-s"),
1265 v: SecretString::from("0x1b"),
1266 };
1267 let wire = serde_json::to_value(&signature).unwrap();
1268 let debug = format!("{signature:?}");
1269
1270 assert_eq!(wire["r"], "0xsignature-r");
1271 assert_eq!(wire["s"], "0xsignature-s");
1272 assert_eq!(wire["v"], "0x1b");
1273 assert_eq!(debug.matches(REDACTED).count(), 3);
1274 assert!(!debug.contains("0xsignature-r"));
1275 assert!(!debug.contains("0xsignature-s"));
1276 assert!(!debug.contains("0x1b"));
1277 }
1278
1279 #[rstest]
1280 fn test_subscription_request_serialization() {
1281 let sub = SubscriptionRequest::L2Book {
1282 coin: Ustr::from("BTC"),
1283 n_sig_figs: Some(5),
1284 mantissa: None,
1285 };
1286
1287 let json = serde_json::to_string(&sub).unwrap();
1288 assert!(json.contains(r#""type":"l2Book""#));
1289 assert!(json.contains(r#""coin":"BTC""#));
1290 }
1291
1292 #[rstest]
1293 fn test_hyperliquid_ws_request_serialization() {
1294 let req = HyperliquidWsRequest::Subscribe {
1295 subscription: SubscriptionRequest::Trades {
1296 coin: Ustr::from("ETH"),
1297 },
1298 };
1299
1300 let json = serde_json::to_string(&req).unwrap();
1301 assert!(json.contains(r#""method":"subscribe""#));
1302 assert!(json.contains(r#""type":"trades""#));
1303 }
1304
1305 #[rstest]
1306 fn test_order_request_serialization() {
1307 let order = OrderRequest {
1308 a: 0, b: true, p: "50000.0".to_string(),
1311 s: "0.1".to_string(),
1312 r: false,
1313 t: OrderTypeRequest::Limit {
1314 tif: TimeInForceRequest::Gtc,
1315 },
1316 c: Some("client-123".to_string()),
1317 };
1318
1319 let json = serde_json::to_string(&order).unwrap();
1320 assert!(json.contains(r#""a":0"#));
1321 assert!(json.contains(r#""b":true"#));
1322 assert!(json.contains(r#""p":"50000.0""#));
1323 }
1324
1325 #[rstest]
1326 fn test_ws_trade_data_deserialization() {
1327 let json = r#"{
1328 "coin": "BTC",
1329 "side": "B",
1330 "px": "50000.0",
1331 "sz": "0.1",
1332 "hash": "0x123",
1333 "time": 1234567890,
1334 "tid": 12345,
1335 "users": ["0xabc", "0xdef"]
1336 }"#;
1337
1338 let trade: WsTradeData = serde_json::from_str(json).unwrap();
1339 assert_eq!(trade.coin, "BTC");
1340 assert_eq!(trade.side, HyperliquidSide::Buy);
1341 assert_eq!(trade.px, dec!(50000.0));
1342 }
1343
1344 #[rstest]
1345 fn test_ws_book_data_deserialization() {
1346 let json = r#"{
1347 "coin": "ETH",
1348 "levels": [
1349 [{"px": "3000.0", "sz": "1.0", "n": 1}],
1350 [{"px": "3001.0", "sz": "2.0", "n": 2}]
1351 ],
1352 "time": 1234567890
1353 }"#;
1354
1355 let book: WsBookData = serde_json::from_str(json).unwrap();
1356 assert_eq!(book.coin, "ETH");
1357 assert_eq!(book.levels[0].len(), 1);
1358 assert_eq!(book.levels[1].len(), 1);
1359 }
1360
1361 #[rstest]
1362 fn test_ws_trailing_stop_data_deserialization() {
1363 let json = r#"{
1364 "offset": "100.0",
1365 "offsetType": "price",
1366 "callbackPrice": "50000.0"
1367 }"#;
1368
1369 let data: WsTrailingStopData = serde_json::from_str(json).unwrap();
1370 assert_eq!(data.offset, dec!(100.0));
1371 assert_eq!(data.offset_type, TrailingOffsetType::Price);
1372 assert_eq!(data.callback_price.unwrap(), dec!(50000.0));
1373 }
1374
1375 #[rstest]
1376 fn test_ws_trigger_activated_data_deserialization() {
1377 let json = r#"{
1378 "coin": "BTC",
1379 "oid": 12345,
1380 "time": 1704470400000,
1381 "triggerPx": "50000.0",
1382 "tpsl": "sl"
1383 }"#;
1384
1385 let data: WsTriggerActivatedData = serde_json::from_str(json).unwrap();
1386 assert_eq!(data.coin, Ustr::from("BTC"));
1387 assert_eq!(data.oid, 12345);
1388 assert_eq!(data.trigger_px, dec!(50000.0));
1389 assert_eq!(data.tpsl, HyperliquidTpSl::Sl);
1390 assert_eq!(data.time, 1704470400000);
1391 }
1392
1393 #[rstest]
1394 fn test_ws_trigger_triggered_data_deserialization() {
1395 let json = r#"{
1396 "coin": "ETH",
1397 "oid": 67890,
1398 "time": 1704470500000,
1399 "triggerPx": "3000.0",
1400 "marketPx": "3001.0",
1401 "tpsl": "tp",
1402 "resultingOid": 99999
1403 }"#;
1404
1405 let data: WsTriggerTriggeredData = serde_json::from_str(json).unwrap();
1406 assert_eq!(data.coin, Ustr::from("ETH"));
1407 assert_eq!(data.oid, 67890);
1408 assert_eq!(data.trigger_px, dec!(3000.0));
1409 assert_eq!(data.market_px, dec!(3001.0));
1410 assert_eq!(data.tpsl, HyperliquidTpSl::Tp);
1411 assert_eq!(data.resulting_oid, Some(99999));
1412 }
1413
1414 #[rstest]
1415 fn test_ws_fill_data_deserialization_with_cloid_and_twap() {
1416 let json = r#"{
1417 "coin": "@107",
1418 "px": "31.737",
1419 "sz": "0.31",
1420 "side": "B",
1421 "time": 1769920606068,
1422 "startPosition": "0.0",
1423 "dir": "Buy",
1424 "closedPnl": "0.0",
1425 "hash": "0xc731e7561e5334a0c8ab043472ce7d01d400ff3bb95653726afa92a8dd570e8b",
1426 "oid": 308086083674,
1427 "crossed": true,
1428 "fee": "0.00021699",
1429 "tid": 812806034449156,
1430 "cloid": "0xd211f1c27288259290850338d22132a0",
1431 "feeToken": "HYPE",
1432 "twapId": null
1433 }"#;
1434
1435 let fill: WsFillData = serde_json::from_str(json).unwrap();
1436 assert_eq!(fill.coin, "@107");
1437 assert_eq!(fill.px, dec!(31.737));
1438 assert_eq!(fill.sz, dec!(0.31));
1439 assert_eq!(fill.side, HyperliquidSide::Buy);
1440 assert_eq!(fill.oid, 308086083674);
1441 assert!(fill.crossed);
1442 assert_eq!(fill.fee, dec!(0.00021699));
1443 assert_eq!(fill.fee_token, "HYPE");
1444 assert_eq!(
1445 fill.cloid,
1446 Some("0xd211f1c27288259290850338d22132a0".to_string())
1447 );
1448 assert!(fill.twap_id.is_none() || fill.twap_id == Some(serde_json::Value::Null));
1449 }
1450
1451 #[rstest]
1452 fn test_ws_user_fills_message_deserialization() {
1453 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}]}}"#;
1454
1455 let msg: HyperliquidWsMessage = serde_json::from_str(json).unwrap();
1456
1457 match msg {
1458 HyperliquidWsMessage::User { data } => match data {
1459 WsUserEventData::Fills { fills } => {
1460 assert_eq!(fills.len(), 1);
1461 let fill = &fills[0];
1462 assert_eq!(fill.coin, "@107");
1463 assert_eq!(fill.px, dec!(31.737));
1464 assert_eq!(
1465 fill.cloid,
1466 Some("0xd211f1c27288259290850338d22132a0".to_string())
1467 );
1468 }
1469 _ => panic!("Expected Fills variant"),
1470 },
1471 _ => panic!("Expected User channel message"),
1472 }
1473 }
1474
1475 #[rstest]
1476 fn test_ws_user_fills_message_with_builder_fee() {
1477 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}]}}"#;
1479
1480 let msg: HyperliquidWsMessage = serde_json::from_str(json).unwrap();
1481
1482 match msg {
1483 HyperliquidWsMessage::User { data } => match data {
1484 WsUserEventData::Fills { fills } => {
1485 assert_eq!(fills.len(), 1);
1486 let fill = &fills[0];
1487 assert_eq!(fill.coin, "BTC");
1488 assert_eq!(fill.px, dec!(79146.0));
1489 assert_eq!(fill.side, HyperliquidSide::Sell);
1490 assert_eq!(fill.builder_fee, Some(dec!(0.007914)));
1491 assert_eq!(fill.fee_token, "USDC");
1492 }
1493 _ => panic!("Expected Fills variant"),
1494 },
1495 _ => panic!("Expected User channel message"),
1496 }
1497 }
1498
1499 #[rstest]
1500 fn test_ws_user_fills_message_with_liquidation() {
1501 let json = include_str!("../../test_data/ws_user_fill_liquidation.json");
1504
1505 let msg: HyperliquidWsMessage = serde_json::from_str(json).unwrap();
1506
1507 match msg {
1508 HyperliquidWsMessage::User { data } => match data {
1509 WsUserEventData::Fills { fills } => {
1510 assert_eq!(fills.len(), 1);
1511 let fill = &fills[0];
1512 let liquidation = fill.liquidation.as_ref().expect("expected liquidation");
1513 assert_eq!(fill.coin, "BTC");
1514 assert_eq!(fill.side, HyperliquidSide::Sell);
1515 assert_eq!(liquidation.mark_px, dec!(66607.0));
1516 assert_eq!(liquidation.method, HyperliquidLiquidationMethod::Market);
1517 assert_eq!(
1518 liquidation.liquidated_user.as_deref(),
1519 Some("0x360878d351f05975e25f1807a27895e1e5e004fb"),
1520 );
1521 }
1522 _ => panic!("Expected Fills variant"),
1523 },
1524 _ => panic!("Expected User channel message"),
1525 }
1526 }
1527
1528 #[rstest]
1529 fn test_ws_trade_data_round_trips_decimals_as_strings() {
1530 let json = r#"{"coin":"BTC","side":"B","px":"66653.0","sz":"0.001","hash":"0xabc","time":1,"tid":2,"users":["0xa","0xb"]}"#;
1533
1534 let trade: WsTradeData = serde_json::from_str(json).unwrap();
1535 assert_eq!(trade.px, dec!(66653.0));
1536 assert_eq!(trade.sz, dec!(0.001));
1537
1538 let value = serde_json::to_value(&trade).unwrap();
1539 assert_eq!(value["px"], serde_json::Value::from("66653.0"));
1540 assert_eq!(value["sz"], serde_json::Value::from("0.001"));
1541 }
1542}
1543
1544#[derive(Debug, Clone)]
1551pub enum NautilusWsMessage {
1552 ExecutionReports(Vec<ExecutionReport>),
1554 Trades(Vec<TradeTick>),
1556 Quote(QuoteTick),
1558 Deltas(OrderBookDeltas),
1560 Depth(Box<OrderBookDepth>),
1562 BookInvalid(InstrumentId),
1564 Candle(Bar),
1566 MarkPrice(MarkPriceUpdate),
1568 IndexPrice(IndexPriceUpdate),
1570 FundingRate(FundingRateUpdate),
1572 CustomData(Data),
1574 Error(String),
1576 Reconnected,
1578}
1579
1580#[derive(Debug, Clone)]
1585#[allow(
1586 clippy::large_enum_variant,
1587 reason = "the variant size gap only crosses the threshold when high-precision widens the raw types"
1588)]
1589pub enum ExecutionReport {
1590 Order(OrderStatusReport),
1592 Fill(FillReport),
1594}