1use std::fmt::Display;
17
18use alloy_primitives::{Address, keccak256};
19use nautilus_model::identifiers::ClientOrderId;
20use rust_decimal::Decimal;
21use serde::{Deserialize, Deserializer, Serialize, Serializer};
22use ustr::Ustr;
23
24use crate::common::enums::{
25 HyperliquidFillDirection, HyperliquidLeverageType,
26 HyperliquidOrderStatus as HyperliquidOrderStatusEnum, HyperliquidPositionType, HyperliquidSide,
27};
28
29pub type HyperliquidCandleSnapshot = Vec<HyperliquidCandle>;
31
32#[derive(Clone, PartialEq, Eq, Hash, Debug)]
34pub struct Cloid(pub [u8; 16]);
35
36impl Cloid {
37 pub fn from_hex<S: AsRef<str>>(s: S) -> Result<Self, String> {
43 let hex_str = s.as_ref();
44 let without_prefix = hex_str
45 .strip_prefix("0x")
46 .ok_or("CLOID must start with '0x'")?;
47
48 if without_prefix.len() != 32 {
49 return Err("CLOID must be exactly 32 hex characters (128 bits)".to_string());
50 }
51
52 let mut bytes = [0u8; 16];
53
54 for i in 0..16 {
55 let byte_str = &without_prefix[i * 2..i * 2 + 2];
56 bytes[i] = u8::from_str_radix(byte_str, 16)
57 .map_err(|_| "Invalid hex character in CLOID".to_string())?;
58 }
59
60 Ok(Self(bytes))
61 }
62
63 #[must_use]
68 pub fn from_client_order_id(client_order_id: ClientOrderId) -> Self {
69 let hash = keccak256(client_order_id.as_str().as_bytes());
70 let mut bytes = [0u8; 16];
71 bytes.copy_from_slice(&hash[..16]);
72 Self(bytes)
73 }
74
75 pub fn to_hex(&self) -> String {
77 let mut result = String::with_capacity(34);
78 result.push_str("0x");
79 for byte in &self.0 {
80 result.push_str(&format!("{byte:02x}"));
81 }
82 result
83 }
84}
85
86impl Display for Cloid {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 write!(f, "{}", self.to_hex())
89 }
90}
91
92impl Serialize for Cloid {
93 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
94 where
95 S: Serializer,
96 {
97 serializer.serialize_str(&self.to_hex())
98 }
99}
100
101impl<'de> Deserialize<'de> for Cloid {
102 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
103 where
104 D: Deserializer<'de>,
105 {
106 let s = String::deserialize(deserializer)?;
107 Self::from_hex(&s).map_err(serde::de::Error::custom)
108 }
109}
110
111pub type AssetId = u32;
116
117pub type OrderId = u64;
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase")]
123pub struct HyperliquidAssetInfo {
124 pub name: Ustr,
126 pub sz_decimals: u32,
128 #[serde(default)]
130 pub max_leverage: Option<u32>,
131 #[serde(default)]
133 pub only_isolated: Option<bool>,
134 #[serde(default)]
136 pub is_delisted: Option<bool>,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub struct PerpMeta {
143 pub universe: Vec<PerpAsset>,
145 #[serde(default)]
147 pub margin_tables: Vec<(u32, MarginTable)>,
148}
149
150#[derive(Debug, Clone, Default, Serialize, Deserialize)]
152#[serde(rename_all = "camelCase")]
153pub struct PerpAsset {
154 pub name: String,
156 pub sz_decimals: u32,
158 #[serde(default)]
160 pub max_leverage: Option<u32>,
161 #[serde(default)]
163 pub only_isolated: Option<bool>,
164 #[serde(default)]
166 pub is_delisted: Option<bool>,
167 #[serde(default)]
169 pub growth_mode: Option<String>,
170 #[serde(default)]
172 pub margin_mode: Option<String>,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177#[serde(rename_all = "camelCase")]
178pub struct MarginTable {
179 pub description: String,
181 #[serde(default)]
183 pub margin_tiers: Vec<MarginTier>,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
188#[serde(rename_all = "camelCase")]
189pub struct MarginTier {
190 pub lower_bound: String,
192 pub max_leverage: u32,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
198#[serde(rename_all = "camelCase")]
199pub struct SpotMeta {
200 pub tokens: Vec<SpotToken>,
202 pub universe: Vec<SpotPair>,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
208#[serde(rename_all = "snake_case")]
209pub struct EvmContract {
210 pub address: Address,
212 pub evm_extra_wei_decimals: i32,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218#[serde(rename_all = "camelCase")]
219pub struct SpotToken {
220 pub name: String,
222 pub sz_decimals: u32,
224 pub wei_decimals: u32,
226 pub index: u32,
228 pub token_id: String,
230 pub is_canonical: bool,
232 #[serde(default)]
234 pub evm_contract: Option<EvmContract>,
235 #[serde(default)]
237 pub full_name: Option<String>,
238 #[serde(default)]
240 pub deployer_trading_fee_share: Option<String>,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
245#[serde(rename_all = "camelCase")]
246pub struct SpotPair {
247 pub name: String,
249 pub tokens: [u32; 2],
251 pub index: u32,
253 pub is_canonical: bool,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
260#[serde(untagged)]
261pub enum PerpMetaAndCtxs {
262 Payload(Box<(PerpMeta, Vec<PerpAssetCtx>)>),
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
268#[serde(rename_all = "camelCase")]
269pub struct PerpAssetCtx {
270 #[serde(default)]
272 pub mark_px: Option<String>,
273 #[serde(default)]
275 pub mid_px: Option<String>,
276 #[serde(default)]
278 pub funding: Option<String>,
279 #[serde(default)]
281 pub open_interest: Option<String>,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
287#[serde(untagged)]
288pub enum SpotMetaAndCtxs {
289 Payload(Box<(SpotMeta, Vec<SpotAssetCtx>)>),
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
295#[serde(rename_all = "camelCase")]
296pub struct SpotAssetCtx {
297 #[serde(default)]
299 pub mark_px: Option<String>,
300 #[serde(default)]
302 pub mid_px: Option<String>,
303 #[serde(default)]
305 pub day_volume: Option<String>,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct HyperliquidL2Book {
311 pub coin: Ustr,
313 pub levels: Vec<Vec<HyperliquidLevel>>,
315 pub time: u64,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct HyperliquidLevel {
322 pub px: String,
324 pub sz: String,
326}
327
328pub type HyperliquidFills = Vec<HyperliquidFill>;
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct HyperliquidMeta {
336 #[serde(default)]
337 pub universe: Vec<HyperliquidAssetInfo>,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
342#[serde(rename_all = "camelCase")]
343pub struct HyperliquidCandle {
344 #[serde(rename = "t")]
346 pub timestamp: u64,
347 #[serde(rename = "T")]
349 pub end_timestamp: u64,
350 #[serde(rename = "o")]
352 pub open: String,
353 #[serde(rename = "h")]
355 pub high: String,
356 #[serde(rename = "l")]
358 pub low: String,
359 #[serde(rename = "c")]
361 pub close: String,
362 #[serde(rename = "v")]
364 pub volume: String,
365 #[serde(rename = "n", default)]
367 pub num_trades: Option<u64>,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct HyperliquidFundingHistoryEntry {
373 pub coin: Ustr,
375 #[serde(rename = "fundingRate")]
377 pub funding_rate: String,
378 #[serde(default)]
380 pub premium: Option<String>,
381 pub time: u64,
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct HyperliquidFill {
388 pub coin: Ustr,
390 pub px: String,
392 pub sz: String,
394 pub side: HyperliquidSide,
396 pub time: u64,
398 #[serde(rename = "startPosition")]
400 pub start_position: String,
401 pub dir: HyperliquidFillDirection,
403 #[serde(rename = "closedPnl")]
405 pub closed_pnl: String,
406 pub hash: String,
408 pub oid: u64,
410 pub crossed: bool,
412 pub fee: String,
414 #[serde(rename = "feeToken")]
416 pub fee_token: Ustr,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize)]
424#[serde(tag = "status", rename_all = "camelCase")]
425pub enum HyperliquidOrderStatus {
426 Order { order: HyperliquidOrderStatusEntry },
427 UnknownOid,
428}
429
430impl HyperliquidOrderStatus {
431 #[must_use]
433 pub fn into_order(self) -> Option<HyperliquidOrderStatusEntry> {
434 match self {
435 Self::Order { order } => Some(order),
436 Self::UnknownOid => None,
437 }
438 }
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize)]
443pub struct HyperliquidOrderStatusEntry {
444 pub order: HyperliquidOrderInfo,
446 pub status: HyperliquidOrderStatusEnum,
448 #[serde(rename = "statusTimestamp")]
450 pub status_timestamp: u64,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct HyperliquidOrderInfo {
456 pub coin: Ustr,
458 pub side: HyperliquidSide,
460 #[serde(rename = "limitPx")]
462 pub limit_px: String,
463 pub sz: String,
465 pub oid: u64,
467 pub timestamp: u64,
469 #[serde(rename = "origSz")]
471 pub orig_sz: String,
472 #[serde(default)]
474 pub cloid: Option<String>,
475}
476
477#[derive(Debug, Clone, Serialize)]
479pub struct HyperliquidSignature {
480 pub r: String,
482 pub s: String,
484 pub v: u64,
486}
487
488impl HyperliquidSignature {
489 #[must_use]
491 pub fn new(r: String, s: String, v: u64) -> Self {
492 Self { r, s, v }
493 }
494
495 #[must_use]
497 pub fn to_hex(&self) -> String {
498 let r = self.r.strip_prefix("0x").unwrap_or(&self.r);
499 let s = self.s.strip_prefix("0x").unwrap_or(&self.s);
500 format!("0x{r}{s}{:02x}", self.v)
501 }
502
503 pub fn from_hex(sig_hex: &str) -> Result<Self, String> {
505 let sig_hex = sig_hex.strip_prefix("0x").unwrap_or(sig_hex);
506
507 if sig_hex.len() != 130 {
508 return Err(format!(
509 "Invalid signature length: expected 130 hex chars, was {}",
510 sig_hex.len()
511 ));
512 }
513
514 let r = format!("0x{}", &sig_hex[0..64]);
515 let s = format!("0x{}", &sig_hex[64..128]);
516 let v = u64::from_str_radix(&sig_hex[128..130], 16)
517 .map_err(|e| format!("Failed to parse v component: {e}"))?;
518
519 Ok(Self { r, s, v })
520 }
521}
522
523#[derive(Debug, Clone, Serialize)]
525pub struct HyperliquidExchangeRequest<T> {
526 #[serde(rename = "action")]
528 pub action: T,
529 #[serde(rename = "nonce")]
531 pub nonce: u64,
532 #[serde(rename = "signature")]
534 pub signature: HyperliquidSignature,
535 #[serde(rename = "vaultAddress", skip_serializing_if = "Option::is_none")]
537 pub vault_address: Option<String>,
538 #[serde(rename = "expiresAfter", skip_serializing_if = "Option::is_none")]
540 pub expires_after: Option<u64>,
541}
542
543impl<T> HyperliquidExchangeRequest<T>
544where
545 T: Serialize,
546{
547 #[must_use]
549 pub fn new(action: T, nonce: u64, signature: HyperliquidSignature) -> Self {
550 Self {
551 action,
552 nonce,
553 signature,
554 vault_address: None,
555 expires_after: None,
556 }
557 }
558
559 #[must_use]
561 pub fn with_vault(
562 action: T,
563 nonce: u64,
564 signature: HyperliquidSignature,
565 vault_address: String,
566 ) -> Self {
567 Self {
568 action,
569 nonce,
570 signature,
571 vault_address: Some(vault_address),
572 expires_after: None,
573 }
574 }
575
576 pub fn to_sign_value(&self) -> serde_json::Result<serde_json::Value> {
578 serde_json::to_value(self)
579 }
580}
581
582#[derive(Debug, Clone, Serialize, Deserialize)]
584#[serde(untagged)]
585pub enum HyperliquidExchangeResponse {
586 Status {
588 status: String,
590 response: serde_json::Value,
592 },
593 Error {
595 error: String,
597 },
598}
599
600impl HyperliquidExchangeResponse {
601 pub fn is_ok(&self) -> bool {
602 matches!(self, Self::Status { status, .. } if status == RESPONSE_STATUS_OK)
603 }
604}
605
606pub const RESPONSE_STATUS_OK: &str = "ok";
608
609#[cfg(test)]
610mod tests {
611 use rstest::rstest;
612
613 use super::*;
614
615 #[rstest]
616 fn test_meta_deserialization() {
617 let json = r#"{"universe": [{"name": "BTC", "szDecimals": 5}]}"#;
618
619 let meta: HyperliquidMeta = serde_json::from_str(json).unwrap();
620
621 assert_eq!(meta.universe.len(), 1);
622 assert_eq!(meta.universe[0].name, "BTC");
623 assert_eq!(meta.universe[0].sz_decimals, 5);
624 }
625
626 #[rstest]
627 fn test_funding_history_entry_with_premium() {
628 let json = r#"{
629 "coin": "BTC",
630 "fundingRate": "0.0000125",
631 "premium": "0.00029005",
632 "time": 1769908800000
633 }"#;
634
635 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
636
637 assert_eq!(entry.coin.as_str(), "BTC");
638 assert_eq!(entry.funding_rate, "0.0000125");
639 assert_eq!(entry.premium.as_deref(), Some("0.00029005"));
640 assert_eq!(entry.time, 1769908800000);
641 }
642
643 #[rstest]
644 fn test_funding_history_entry_without_premium() {
645 let json = r#"{
648 "coin": "BTC",
649 "fundingRate": "0.0000033",
650 "time": 1769916000000
651 }"#;
652
653 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
654
655 assert!(entry.premium.is_none());
656 assert_eq!(entry.funding_rate, "0.0000033");
657 }
658
659 #[rstest]
660 fn test_perp_asset_hip3_fields() {
661 let json = r#"{
662 "name": "xyz:TSLA",
663 "szDecimals": 3,
664 "maxLeverage": 10,
665 "onlyIsolated": true,
666 "growthMode": "enabled",
667 "marginMode": "strictIsolated"
668 }"#;
669
670 let asset: PerpAsset = serde_json::from_str(json).unwrap();
671
672 assert_eq!(asset.name, "xyz:TSLA");
673 assert_eq!(asset.sz_decimals, 3);
674 assert_eq!(asset.max_leverage, Some(10));
675 assert_eq!(asset.only_isolated, Some(true));
676 assert_eq!(asset.growth_mode.as_deref(), Some("enabled"));
677 assert_eq!(asset.margin_mode.as_deref(), Some("strictIsolated"));
678 }
679
680 #[rstest]
681 fn test_perp_asset_hip3_fields_absent() {
682 let json = r#"{"name": "BTC", "szDecimals": 5}"#;
683
684 let asset: PerpAsset = serde_json::from_str(json).unwrap();
685
686 assert_eq!(asset.growth_mode, None);
687 assert_eq!(asset.margin_mode, None);
688 }
689
690 #[rstest]
691 fn test_l2_book_deserialization() {
692 let json = r#"{"coin": "BTC", "levels": [[{"px": "50000", "sz": "1.5"}], [{"px": "50100", "sz": "2.0"}]], "time": 1234567890}"#;
693
694 let book: HyperliquidL2Book = serde_json::from_str(json).unwrap();
695
696 assert_eq!(book.coin, "BTC");
697 assert_eq!(book.levels.len(), 2);
698 assert_eq!(book.time, 1234567890);
699 }
700
701 #[rstest]
702 fn test_exchange_response_deserialization() {
703 let json = r#"{"status": "ok", "response": {"type": "order"}}"#;
704
705 let response: HyperliquidExchangeResponse = serde_json::from_str(json).unwrap();
706 assert!(response.is_ok());
707 }
708
709 #[rstest]
710 fn test_spot_clearinghouse_state_deserialization() {
711 let json = r#"{
712 "balances": [
713 {"coin": "USDC", "token": 0, "total": "14.625485", "hold": "0.0", "entryNtl": "0.0"},
714 {"coin": "PURR", "token": 1, "total": "2000", "hold": "100", "entryNtl": "1234.56"}
715 ]
716 }"#;
717
718 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
719
720 assert_eq!(state.balances.len(), 2);
721 let usdc = &state.balances[0];
722 assert_eq!(usdc.coin.as_str(), "USDC");
723 assert_eq!(usdc.token, 0);
724 assert_eq!(usdc.total.to_string(), "14.625485");
725 assert_eq!(usdc.hold, rust_decimal::Decimal::ZERO);
726 assert_eq!(usdc.free().to_string(), "14.625485");
727 assert_eq!(usdc.avg_entry_px(), None);
728
729 let purr = &state.balances[1];
730 assert_eq!(purr.coin.as_str(), "PURR");
731 assert_eq!(purr.token, 1);
732 assert_eq!(purr.free().to_string(), "1900");
733 assert_eq!(
734 purr.avg_entry_px().unwrap(),
735 rust_decimal_macros::dec!(0.61728)
736 );
737 }
738
739 #[rstest]
740 fn test_spot_clearinghouse_state_empty() {
741 let json = r#"{"balances": []}"#;
742 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
743 assert!(state.balances.is_empty());
744 }
745
746 #[rstest]
747 fn test_spot_balance_handles_missing_entry_ntl() {
748 let json = r#"{"coin": "HYPE", "token": 150, "total": "5", "hold": "0"}"#;
749 let balance: SpotBalance = serde_json::from_str(json).unwrap();
750 assert_eq!(balance.entry_ntl, None);
751 assert_eq!(balance.avg_entry_px(), None);
752 }
753
754 #[rstest]
755 fn test_msgpack_serialization_matches_python() {
756 let action = HyperliquidExecAction::Order {
761 orders: vec![],
762 grouping: HyperliquidExecGrouping::Na,
763 builder: None,
764 };
765
766 let json = serde_json::to_string(&action).unwrap();
768 assert!(
769 json.contains(r#""type":"order""#),
770 "JSON should have type tag: {json}"
771 );
772
773 let msgpack_bytes = rmp_serde::to_vec_named(&action).unwrap();
775
776 let decoded: serde_json::Value = rmp_serde::from_slice(&msgpack_bytes).unwrap();
778
779 assert!(
781 decoded.get("type").is_some(),
782 "MsgPack should have type tag. Decoded: {decoded:?}"
783 );
784 assert_eq!(
785 decoded.get("type").unwrap().as_str().unwrap(),
786 "order",
787 "Type should be 'order'"
788 );
789 assert!(decoded.get("orders").is_some(), "Should have orders field");
790 assert!(
791 decoded.get("grouping").is_some(),
792 "Should have grouping field"
793 );
794 }
795}
796
797#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
801pub enum HyperliquidExecTif {
802 #[serde(rename = "Alo")]
804 Alo,
805 #[serde(rename = "Ioc")]
807 Ioc,
808 #[serde(rename = "Gtc")]
810 Gtc,
811}
812
813#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
815pub enum HyperliquidExecTpSl {
816 #[serde(rename = "tp")]
818 Tp,
819 #[serde(rename = "sl")]
821 Sl,
822}
823
824#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
826pub enum HyperliquidExecGrouping {
827 #[serde(rename = "na")]
829 #[default]
830 Na,
831 #[serde(rename = "normalTpsl")]
833 NormalTpsl,
834 #[serde(rename = "positionTpsl")]
836 PositionTpsl,
837}
838
839#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
841#[serde(untagged)]
842pub enum HyperliquidExecOrderKind {
843 Limit {
845 limit: HyperliquidExecLimitParams,
847 },
848 Trigger {
850 trigger: HyperliquidExecTriggerParams,
852 },
853}
854
855#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
857pub struct HyperliquidExecLimitParams {
858 pub tif: HyperliquidExecTif,
860}
861
862#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
864#[serde(rename_all = "camelCase")]
865pub struct HyperliquidExecTriggerParams {
866 pub is_market: bool,
868 #[serde(
870 serialize_with = "crate::common::parse::serialize_decimal_as_str",
871 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
872 )]
873 pub trigger_px: Decimal,
874 pub tpsl: HyperliquidExecTpSl,
876}
877
878#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
883pub struct HyperliquidExecBuilderFee {
884 #[serde(rename = "b")]
886 pub address: String,
887 #[serde(rename = "f")]
889 pub fee_tenths_bp: u32,
890}
891
892#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
897pub struct HyperliquidExecPlaceOrderRequest {
898 #[serde(rename = "a")]
900 pub asset: AssetId,
901 #[serde(rename = "b")]
903 pub is_buy: bool,
904 #[serde(
906 rename = "p",
907 serialize_with = "crate::common::parse::serialize_decimal_as_str",
908 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
909 )]
910 pub price: Decimal,
911 #[serde(
913 rename = "s",
914 serialize_with = "crate::common::parse::serialize_decimal_as_str",
915 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
916 )]
917 pub size: Decimal,
918 #[serde(rename = "r")]
920 pub reduce_only: bool,
921 #[serde(rename = "t")]
923 pub kind: HyperliquidExecOrderKind,
924 #[serde(rename = "c", skip_serializing_if = "Option::is_none")]
926 pub cloid: Option<Cloid>,
927}
928
929#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
931pub struct HyperliquidExecCancelOrderRequest {
932 #[serde(rename = "a")]
934 pub asset: AssetId,
935 #[serde(rename = "o")]
937 pub oid: OrderId,
938}
939
940#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
945pub struct HyperliquidExecCancelByCloidRequest {
946 pub asset: AssetId,
948 pub cloid: Cloid,
950}
951
952#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
957pub struct HyperliquidExecModifyOrderRequest {
958 pub oid: OrderId,
960 pub order: HyperliquidExecPlaceOrderRequest,
962}
963
964#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
966pub struct HyperliquidExecTwapRequest {
967 #[serde(rename = "a")]
969 pub asset: AssetId,
970 #[serde(rename = "b")]
972 pub is_buy: bool,
973 #[serde(
975 rename = "s",
976 serialize_with = "crate::common::parse::serialize_decimal_as_str",
977 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
978 )]
979 pub size: Decimal,
980 #[serde(rename = "m")]
982 pub duration_ms: u64,
983}
984
985#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
991#[serde(tag = "type")]
992pub enum HyperliquidExecAction {
993 #[serde(rename = "order")]
995 Order {
996 orders: Vec<HyperliquidExecPlaceOrderRequest>,
998 #[serde(default)]
1000 grouping: HyperliquidExecGrouping,
1001 #[serde(skip_serializing_if = "Option::is_none")]
1003 builder: Option<HyperliquidExecBuilderFee>,
1004 },
1005
1006 #[serde(rename = "cancel")]
1008 Cancel {
1009 cancels: Vec<HyperliquidExecCancelOrderRequest>,
1011 },
1012
1013 #[serde(rename = "cancelByCloid")]
1015 CancelByCloid {
1016 cancels: Vec<HyperliquidExecCancelByCloidRequest>,
1018 },
1019
1020 #[serde(rename = "modify")]
1022 Modify {
1023 #[serde(flatten)]
1025 modify: HyperliquidExecModifyOrderRequest,
1026 },
1027
1028 #[serde(rename = "batchModify")]
1030 BatchModify {
1031 modifies: Vec<HyperliquidExecModifyOrderRequest>,
1033 },
1034
1035 #[serde(rename = "scheduleCancel")]
1037 ScheduleCancel {
1038 #[serde(skip_serializing_if = "Option::is_none")]
1041 time: Option<u64>,
1042 },
1043
1044 #[serde(rename = "updateLeverage")]
1046 UpdateLeverage {
1047 #[serde(rename = "a")]
1049 asset: AssetId,
1050 #[serde(rename = "isCross")]
1052 is_cross: bool,
1053 #[serde(rename = "leverage")]
1055 leverage: u32,
1056 },
1057
1058 #[serde(rename = "updateIsolatedMargin")]
1060 UpdateIsolatedMargin {
1061 #[serde(rename = "a")]
1063 asset: AssetId,
1064 #[serde(
1066 rename = "delta",
1067 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1068 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1069 )]
1070 delta: Decimal,
1071 },
1072
1073 #[serde(rename = "usdClassTransfer")]
1075 UsdClassTransfer {
1076 from: String,
1078 to: String,
1080 #[serde(
1082 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1083 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1084 )]
1085 amount: Decimal,
1086 },
1087
1088 #[serde(rename = "twapPlace")]
1090 TwapPlace {
1091 #[serde(flatten)]
1093 twap: HyperliquidExecTwapRequest,
1094 },
1095
1096 #[serde(rename = "twapCancel")]
1098 TwapCancel {
1099 #[serde(rename = "a")]
1101 asset: AssetId,
1102 #[serde(rename = "t")]
1104 twap_id: u64,
1105 },
1106
1107 #[serde(rename = "noop")]
1109 Noop,
1110}
1111
1112#[derive(Debug, Clone, Serialize)]
1117#[serde(rename_all = "camelCase")]
1118pub struct HyperliquidExecRequest {
1119 pub action: HyperliquidExecAction,
1121 pub nonce: u64,
1123 pub signature: String,
1125 #[serde(skip_serializing_if = "Option::is_none")]
1127 pub vault_address: Option<String>,
1128 #[serde(skip_serializing_if = "Option::is_none")]
1131 pub expires_after: Option<u64>,
1132}
1133
1134#[derive(Debug, Clone, Serialize, Deserialize)]
1136pub struct HyperliquidExecResponse {
1137 pub status: String,
1139 pub response: HyperliquidExecResponseData,
1141}
1142
1143#[derive(Debug, Clone, Serialize, Deserialize)]
1145#[serde(tag = "type")]
1146pub enum HyperliquidExecResponseData {
1147 #[serde(rename = "order")]
1149 Order {
1150 data: HyperliquidExecOrderResponseData,
1152 },
1153 #[serde(rename = "cancel")]
1155 Cancel {
1156 data: HyperliquidExecCancelResponseData,
1158 },
1159 #[serde(rename = "modify")]
1161 Modify {
1162 data: HyperliquidExecModifyResponseData,
1164 },
1165 #[serde(rename = "default")]
1167 Default,
1168 #[serde(other)]
1170 Unknown,
1171}
1172
1173#[derive(Debug, Clone, Serialize, Deserialize)]
1175pub struct HyperliquidExecOrderResponseData {
1176 pub statuses: Vec<HyperliquidExecOrderStatus>,
1178}
1179
1180#[derive(Debug, Clone, Serialize, Deserialize)]
1182pub struct HyperliquidExecCancelResponseData {
1183 pub statuses: Vec<HyperliquidExecCancelStatus>,
1185}
1186
1187#[derive(Debug, Clone, Serialize, Deserialize)]
1189pub struct HyperliquidExecModifyResponseData {
1190 pub statuses: Vec<HyperliquidExecModifyStatus>,
1192}
1193
1194#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1196#[serde(untagged)]
1197pub enum HyperliquidExecOrderStatus {
1198 Resting {
1200 resting: HyperliquidExecRestingInfo,
1202 },
1203 Filled {
1205 filled: HyperliquidExecFilledInfo,
1207 },
1208 Error {
1210 error: String,
1212 },
1213}
1214
1215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1217pub struct HyperliquidExecRestingInfo {
1218 pub oid: OrderId,
1220}
1221
1222#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1224pub struct HyperliquidExecFilledInfo {
1225 #[serde(
1227 rename = "totalSz",
1228 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1229 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1230 )]
1231 pub total_sz: Decimal,
1232 #[serde(
1234 rename = "avgPx",
1235 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1236 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1237 )]
1238 pub avg_px: Decimal,
1239 pub oid: OrderId,
1241}
1242
1243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1245#[serde(untagged)]
1246pub enum HyperliquidExecCancelStatus {
1247 Success(String), Error {
1251 error: String,
1253 },
1254}
1255
1256#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1258#[serde(untagged)]
1259pub enum HyperliquidExecModifyStatus {
1260 Success(String), Error {
1264 error: String,
1266 },
1267}
1268
1269#[derive(Debug, Clone, Serialize, Deserialize)]
1272#[serde(rename_all = "camelCase")]
1273pub struct ClearinghouseState {
1274 #[serde(default)]
1276 pub asset_positions: Vec<AssetPosition>,
1277 #[serde(default)]
1279 pub cross_margin_summary: Option<CrossMarginSummary>,
1280 #[serde(
1282 default,
1283 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1284 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
1285 )]
1286 pub withdrawable: Option<Decimal>,
1287 #[serde(default)]
1289 pub time: Option<u64>,
1290}
1291
1292#[derive(Debug, Clone, Serialize, Deserialize)]
1294#[serde(rename_all = "camelCase")]
1295pub struct AssetPosition {
1296 pub position: PositionData,
1298 #[serde(rename = "type")]
1300 pub position_type: HyperliquidPositionType,
1301}
1302
1303#[derive(Debug, Clone, Serialize, Deserialize)]
1305#[serde(rename_all = "camelCase")]
1306pub struct LeverageInfo {
1307 #[serde(rename = "type")]
1308 pub leverage_type: HyperliquidLeverageType,
1309 pub value: u32,
1311}
1312
1313#[derive(Debug, Clone, Serialize, Deserialize)]
1315#[serde(rename_all = "camelCase")]
1316pub struct CumFundingInfo {
1317 #[serde(
1319 rename = "allTime",
1320 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1321 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1322 )]
1323 pub all_time: Decimal,
1324 #[serde(
1326 rename = "sinceOpen",
1327 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1328 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1329 )]
1330 pub since_open: Decimal,
1331 #[serde(
1333 rename = "sinceChange",
1334 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1335 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1336 )]
1337 pub since_change: Decimal,
1338}
1339
1340#[derive(Debug, Clone, Serialize, Deserialize)]
1342#[serde(rename_all = "camelCase")]
1343pub struct PositionData {
1344 pub coin: Ustr,
1346 #[serde(rename = "cumFunding")]
1348 pub cum_funding: CumFundingInfo,
1349 #[serde(
1351 rename = "entryPx",
1352 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1353 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str",
1354 default
1355 )]
1356 pub entry_px: Option<Decimal>,
1357 pub leverage: LeverageInfo,
1359 #[serde(
1361 rename = "liquidationPx",
1362 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1363 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str",
1364 default
1365 )]
1366 pub liquidation_px: Option<Decimal>,
1367 #[serde(
1369 rename = "marginUsed",
1370 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1371 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1372 )]
1373 pub margin_used: Decimal,
1374 #[serde(rename = "maxLeverage", default)]
1376 pub max_leverage: Option<u32>,
1377 #[serde(
1379 rename = "positionValue",
1380 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1381 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1382 )]
1383 pub position_value: Decimal,
1384 #[serde(
1386 rename = "returnOnEquity",
1387 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1388 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1389 )]
1390 pub return_on_equity: Decimal,
1391 #[serde(
1393 rename = "szi",
1394 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1395 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1396 )]
1397 pub szi: Decimal,
1398 #[serde(
1400 rename = "unrealizedPnl",
1401 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1402 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1403 )]
1404 pub unrealized_pnl: Decimal,
1405}
1406
1407#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1413#[serde(rename_all = "camelCase")]
1414pub struct SpotClearinghouseState {
1415 #[serde(default)]
1417 pub balances: Vec<SpotBalance>,
1418}
1419
1420#[derive(Debug, Clone, Serialize, Deserialize)]
1422#[serde(rename_all = "camelCase")]
1423pub struct SpotBalance {
1424 pub coin: Ustr,
1426 pub token: u32,
1428 #[serde(
1430 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1431 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1432 )]
1433 pub total: Decimal,
1434 #[serde(
1436 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1437 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1438 )]
1439 pub hold: Decimal,
1440 #[serde(
1442 default,
1443 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1444 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
1445 )]
1446 pub entry_ntl: Option<Decimal>,
1447}
1448
1449impl SpotBalance {
1450 #[must_use]
1452 pub fn free(&self) -> Decimal {
1453 (self.total - self.hold).max(Decimal::ZERO)
1454 }
1455
1456 #[must_use]
1458 pub fn avg_entry_px(&self) -> Option<Decimal> {
1459 let entry_ntl = self.entry_ntl?;
1460
1461 if entry_ntl.is_zero() || self.total.is_zero() {
1462 return None;
1463 }
1464
1465 Some(entry_ntl / self.total)
1466 }
1467}
1468
1469#[derive(Debug, Clone, Serialize, Deserialize)]
1471#[serde(rename_all = "camelCase")]
1472pub struct CrossMarginSummary {
1473 #[serde(
1475 rename = "accountValue",
1476 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1477 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1478 )]
1479 pub account_value: Decimal,
1480 #[serde(
1482 rename = "totalNtlPos",
1483 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1484 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1485 )]
1486 pub total_ntl_pos: Decimal,
1487 #[serde(
1489 rename = "totalRawUsd",
1490 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1491 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1492 )]
1493 pub total_raw_usd: Decimal,
1494 #[serde(
1496 rename = "totalMarginUsed",
1497 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1498 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1499 )]
1500 pub total_margin_used: Decimal,
1501 #[serde(
1503 rename = "withdrawable",
1504 default,
1505 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1506 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
1507 )]
1508 pub withdrawable: Option<Decimal>,
1509}