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
32const CLOID_MARKER_PREFIX_BYTES: [u8; 2] = [0x6e, 0x42];
33
34#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
36pub struct Cloid(pub [u8; 16]);
37
38impl Cloid {
39 pub fn from_hex<S: AsRef<str>>(s: S) -> Result<Self, String> {
45 let hex_str = s.as_ref();
46 let without_prefix = hex_str
47 .strip_prefix("0x")
48 .ok_or("CLOID must start with '0x'")?;
49
50 if without_prefix.len() != 32 {
51 return Err("CLOID must be exactly 32 hex characters (128 bits)".to_string());
52 }
53
54 let mut bytes = [0u8; 16];
55
56 for i in 0..16 {
57 let byte_str = &without_prefix[i * 2..i * 2 + 2];
58 bytes[i] = u8::from_str_radix(byte_str, 16)
59 .map_err(|_| "Invalid hex character in CLOID".to_string())?;
60 }
61
62 Ok(Self(bytes))
63 }
64
65 #[must_use]
67 pub fn from_client_order_id(client_order_id: ClientOrderId) -> Self {
68 let hash = keccak256(client_order_id.as_str().as_bytes());
69 let mut bytes = [0u8; 16];
70 bytes.copy_from_slice(&hash[..16]);
71 bytes[..CLOID_MARKER_PREFIX_BYTES.len()].copy_from_slice(&CLOID_MARKER_PREFIX_BYTES);
72 bytes[6] = (bytes[6] & 0x0f) | 0x40;
73 bytes[8] = (bytes[8] & 0x3f) | 0x80;
74 Self(bytes)
75 }
76
77 #[must_use]
79 pub fn from_legacy_client_order_id(client_order_id: ClientOrderId) -> Self {
80 let hash = keccak256(client_order_id.as_str().as_bytes());
81 let mut bytes = [0u8; 16];
82 bytes.copy_from_slice(&hash[..16]);
83 Self(bytes)
84 }
85
86 #[must_use]
88 pub fn is_uuid_v4(&self) -> bool {
89 self.0[6] >> 4 == 4 && matches!(self.0[8] >> 4, 8..=11)
90 }
91
92 pub fn to_hex(&self) -> String {
94 let mut result = String::with_capacity(34);
95 result.push_str("0x");
96 for byte in &self.0 {
97 result.push_str(&format!("{byte:02x}"));
98 }
99 result
100 }
101}
102
103impl Display for Cloid {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 write!(f, "{}", self.to_hex())
106 }
107}
108
109impl Serialize for Cloid {
110 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
111 where
112 S: Serializer,
113 {
114 serializer.serialize_str(&self.to_hex())
115 }
116}
117
118impl<'de> Deserialize<'de> for Cloid {
119 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
120 where
121 D: Deserializer<'de>,
122 {
123 let s = String::deserialize(deserializer)?;
124 Self::from_hex(&s).map_err(serde::de::Error::custom)
125 }
126}
127
128pub type AssetId = u32;
133
134pub type OrderId = u64;
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub struct HyperliquidAssetInfo {
141 pub name: Ustr,
143 pub sz_decimals: u32,
145 #[serde(default)]
147 pub max_leverage: Option<u32>,
148 #[serde(default)]
150 pub only_isolated: Option<bool>,
151 #[serde(default)]
153 pub is_delisted: Option<bool>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub struct PerpMeta {
160 pub universe: Vec<PerpAsset>,
162 #[serde(default)]
164 pub margin_tables: Vec<(u32, MarginTable)>,
165}
166
167#[derive(Debug, Clone, Default, Serialize, Deserialize)]
169#[serde(rename_all = "camelCase")]
170pub struct PerpAsset {
171 pub name: String,
173 pub sz_decimals: u32,
175 #[serde(default)]
177 pub max_leverage: Option<u32>,
178 #[serde(default)]
180 pub only_isolated: Option<bool>,
181 #[serde(default)]
183 pub is_delisted: Option<bool>,
184 #[serde(default)]
186 pub growth_mode: Option<String>,
187 #[serde(default)]
189 pub margin_mode: Option<String>,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194#[serde(rename_all = "camelCase")]
195pub struct MarginTable {
196 pub description: String,
198 #[serde(default)]
200 pub margin_tiers: Vec<MarginTier>,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205#[serde(rename_all = "camelCase")]
206pub struct MarginTier {
207 pub lower_bound: String,
209 pub max_leverage: u32,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
216#[serde(rename_all = "camelCase")]
217pub struct PerpDex {
218 pub name: String,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct SpotMeta {
226 pub tokens: Vec<SpotToken>,
228 pub universe: Vec<SpotPair>,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234#[serde(rename_all = "snake_case")]
235pub struct EvmContract {
236 pub address: Address,
238 pub evm_extra_wei_decimals: i32,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase")]
245pub struct SpotToken {
246 pub name: String,
248 pub sz_decimals: u32,
250 pub wei_decimals: u32,
252 pub index: u32,
254 pub token_id: String,
256 pub is_canonical: bool,
258 #[serde(default)]
260 pub evm_contract: Option<EvmContract>,
261 #[serde(default)]
263 pub full_name: Option<String>,
264 #[serde(default)]
266 pub deployer_trading_fee_share: Option<String>,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271#[serde(rename_all = "camelCase")]
272pub struct SpotPair {
273 pub name: String,
275 pub tokens: [u32; 2],
277 pub index: u32,
279 pub is_canonical: bool,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285#[serde(rename_all = "camelCase")]
286pub struct OutcomeMeta {
287 pub outcomes: Vec<OutcomeMarket>,
289 #[serde(default)]
293 pub questions: Vec<OutcomeQuestion>,
294}
295
296impl OutcomeMeta {
297 #[must_use]
300 pub fn parent_question(&self, outcome_index: u32) -> Option<&OutcomeQuestion> {
301 self.questions.iter().find(|q| {
302 q.fallback_outcome == Some(outcome_index) || q.named_outcomes.contains(&outcome_index)
303 })
304 }
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309#[serde(rename_all = "camelCase")]
310pub struct OutcomeMarket {
311 pub outcome: u32,
313 pub name: String,
315 pub description: String,
317 #[serde(default)]
319 pub side_specs: Vec<OutcomeSideSpec>,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
324#[serde(rename_all = "camelCase")]
325pub struct OutcomeSideSpec {
326 pub name: String,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize)]
336#[serde(rename_all = "camelCase")]
337pub struct OutcomeQuestion {
338 pub question: u32,
340 pub name: String,
342 pub description: String,
344 #[serde(default)]
346 pub fallback_outcome: Option<u32>,
347 #[serde(default)]
349 pub named_outcomes: Vec<u32>,
350 #[serde(default)]
352 pub settled_named_outcomes: Vec<u32>,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
358#[serde(untagged)]
359pub enum PerpMetaAndCtxs {
360 Payload(Box<(PerpMeta, Vec<PerpAssetCtx>)>),
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize)]
366#[serde(rename_all = "camelCase")]
367pub struct PerpAssetCtx {
368 #[serde(default)]
370 pub mark_px: Option<String>,
371 #[serde(default)]
373 pub mid_px: Option<String>,
374 #[serde(default)]
376 pub funding: Option<String>,
377 #[serde(default)]
379 pub open_interest: Option<String>,
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize)]
385#[serde(untagged)]
386pub enum SpotMetaAndCtxs {
387 Payload(Box<(SpotMeta, Vec<SpotAssetCtx>)>),
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
393#[serde(rename_all = "camelCase")]
394pub struct SpotAssetCtx {
395 #[serde(default)]
397 pub mark_px: Option<String>,
398 #[serde(default)]
400 pub mid_px: Option<String>,
401 #[serde(default)]
403 pub day_volume: Option<String>,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct HyperliquidL2Book {
409 pub coin: Ustr,
411 pub levels: Vec<Vec<HyperliquidLevel>>,
413 pub time: u64,
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct HyperliquidLevel {
420 pub px: String,
422 pub sz: String,
424}
425
426pub type HyperliquidFills = Vec<HyperliquidFill>;
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct HyperliquidMeta {
434 #[serde(default)]
435 pub universe: Vec<HyperliquidAssetInfo>,
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
440#[serde(rename_all = "camelCase")]
441pub struct HyperliquidCandle {
442 #[serde(rename = "t")]
444 pub timestamp: u64,
445 #[serde(rename = "T")]
447 pub end_timestamp: u64,
448 #[serde(rename = "o")]
450 pub open: String,
451 #[serde(rename = "h")]
453 pub high: String,
454 #[serde(rename = "l")]
456 pub low: String,
457 #[serde(rename = "c")]
459 pub close: String,
460 #[serde(rename = "v")]
462 pub volume: String,
463 #[serde(rename = "n", default)]
465 pub num_trades: Option<u64>,
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct HyperliquidFundingHistoryEntry {
471 pub coin: Ustr,
473 #[serde(rename = "fundingRate")]
475 pub funding_rate: String,
476 #[serde(default)]
478 pub premium: Option<String>,
479 pub time: u64,
481}
482
483#[derive(Debug, Clone, Serialize, Deserialize)]
485pub struct HyperliquidFill {
486 pub coin: Ustr,
488 pub px: String,
490 pub sz: String,
492 pub side: HyperliquidSide,
494 pub time: u64,
496 #[serde(rename = "startPosition")]
498 pub start_position: String,
499 pub dir: HyperliquidFillDirection,
501 #[serde(rename = "closedPnl")]
503 pub closed_pnl: String,
504 pub hash: String,
506 pub oid: u64,
508 pub crossed: bool,
510 pub fee: String,
512 #[serde(rename = "feeToken")]
514 pub fee_token: Ustr,
515}
516
517#[derive(Debug, Clone, Serialize, Deserialize)]
522#[serde(tag = "status", rename_all = "camelCase")]
523pub enum HyperliquidOrderStatus {
524 Order { order: HyperliquidOrderStatusEntry },
525 UnknownOid,
526}
527
528impl HyperliquidOrderStatus {
529 #[must_use]
531 pub fn into_order(self) -> Option<HyperliquidOrderStatusEntry> {
532 match self {
533 Self::Order { order } => Some(order),
534 Self::UnknownOid => None,
535 }
536 }
537}
538
539#[derive(Debug, Clone, Serialize, Deserialize)]
541pub struct HyperliquidOrderStatusEntry {
542 pub order: HyperliquidOrderInfo,
544 pub status: HyperliquidOrderStatusEnum,
546 #[serde(rename = "statusTimestamp")]
548 pub status_timestamp: u64,
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize)]
553pub struct HyperliquidOrderInfo {
554 pub coin: Ustr,
556 pub side: HyperliquidSide,
558 #[serde(rename = "limitPx")]
560 pub limit_px: String,
561 pub sz: String,
563 pub oid: u64,
565 pub timestamp: u64,
567 #[serde(rename = "origSz")]
569 pub orig_sz: String,
570 #[serde(default)]
572 pub cloid: Option<String>,
573}
574
575#[derive(Debug, Clone, Serialize)]
577pub struct HyperliquidSignature {
578 pub r: String,
580 pub s: String,
582 pub v: u64,
584}
585
586impl HyperliquidSignature {
587 #[must_use]
589 pub fn new(r: String, s: String, v: u64) -> Self {
590 Self { r, s, v }
591 }
592
593 #[must_use]
595 pub fn to_hex(&self) -> String {
596 let r = self.r.strip_prefix("0x").unwrap_or(&self.r);
597 let s = self.s.strip_prefix("0x").unwrap_or(&self.s);
598 format!("0x{r}{s}{:02x}", self.v)
599 }
600
601 pub fn from_hex(sig_hex: &str) -> Result<Self, String> {
603 let sig_hex = sig_hex.strip_prefix("0x").unwrap_or(sig_hex);
604
605 if sig_hex.len() != 130 {
606 return Err(format!(
607 "Invalid signature length: expected 130 hex chars, was {}",
608 sig_hex.len()
609 ));
610 }
611
612 let r = format!("0x{}", &sig_hex[0..64]);
613 let s = format!("0x{}", &sig_hex[64..128]);
614 let v = u64::from_str_radix(&sig_hex[128..130], 16)
615 .map_err(|e| format!("Failed to parse v component: {e}"))?;
616
617 Ok(Self { r, s, v })
618 }
619}
620
621#[derive(Debug, Clone, Serialize)]
623pub struct HyperliquidExchangeRequest<T> {
624 #[serde(rename = "action")]
626 pub action: T,
627 #[serde(rename = "nonce")]
629 pub nonce: u64,
630 #[serde(rename = "signature")]
632 pub signature: HyperliquidSignature,
633 #[serde(rename = "vaultAddress", skip_serializing_if = "Option::is_none")]
635 pub vault_address: Option<String>,
636 #[serde(rename = "expiresAfter", skip_serializing_if = "Option::is_none")]
638 pub expires_after: Option<u64>,
639}
640
641impl<T> HyperliquidExchangeRequest<T>
642where
643 T: Serialize,
644{
645 #[must_use]
647 pub fn new(action: T, nonce: u64, signature: HyperliquidSignature) -> Self {
648 Self {
649 action,
650 nonce,
651 signature,
652 vault_address: None,
653 expires_after: None,
654 }
655 }
656
657 #[must_use]
659 pub fn with_vault(
660 action: T,
661 nonce: u64,
662 signature: HyperliquidSignature,
663 vault_address: String,
664 ) -> Self {
665 Self {
666 action,
667 nonce,
668 signature,
669 vault_address: Some(vault_address),
670 expires_after: None,
671 }
672 }
673
674 pub fn to_sign_value(&self) -> serde_json::Result<serde_json::Value> {
676 serde_json::to_value(self)
677 }
678}
679
680#[derive(Debug, Clone, Serialize, Deserialize)]
682#[serde(untagged)]
683pub enum HyperliquidExchangeResponse {
684 Status {
686 status: String,
688 response: serde_json::Value,
690 },
691 Error {
693 error: String,
695 },
696}
697
698impl HyperliquidExchangeResponse {
699 pub fn is_ok(&self) -> bool {
700 matches!(self, Self::Status { status, .. } if status == RESPONSE_STATUS_OK)
701 }
702}
703
704pub const RESPONSE_STATUS_OK: &str = "ok";
706
707#[cfg(test)]
708mod tests {
709 use rstest::rstest;
710 use rust_decimal_macros::dec;
711 use serde_json::json;
712
713 use super::*;
714
715 #[rstest]
716 fn test_meta_deserialization() {
717 let json = r#"{"universe": [{"name": "BTC", "szDecimals": 5}]}"#;
718
719 let meta: HyperliquidMeta = serde_json::from_str(json).unwrap();
720
721 assert_eq!(meta.universe.len(), 1);
722 assert_eq!(meta.universe[0].name, "BTC");
723 assert_eq!(meta.universe[0].sz_decimals, 5);
724 }
725
726 #[rstest]
727 fn test_funding_history_entry_with_premium() {
728 let json = r#"{
729 "coin": "BTC",
730 "fundingRate": "0.0000125",
731 "premium": "0.00029005",
732 "time": 1769908800000
733 }"#;
734
735 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
736
737 assert_eq!(entry.coin.as_str(), "BTC");
738 assert_eq!(entry.funding_rate, "0.0000125");
739 assert_eq!(entry.premium.as_deref(), Some("0.00029005"));
740 assert_eq!(entry.time, 1769908800000);
741 }
742
743 #[rstest]
744 fn test_funding_history_entry_without_premium() {
745 let json = r#"{
748 "coin": "BTC",
749 "fundingRate": "0.0000033",
750 "time": 1769916000000
751 }"#;
752
753 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
754
755 assert!(entry.premium.is_none());
756 assert_eq!(entry.funding_rate, "0.0000033");
757 }
758
759 #[rstest]
760 fn test_perp_asset_hip3_fields() {
761 let json = r#"{
762 "name": "xyz:TSLA",
763 "szDecimals": 3,
764 "maxLeverage": 10,
765 "onlyIsolated": true,
766 "growthMode": "enabled",
767 "marginMode": "strictIsolated"
768 }"#;
769
770 let asset: PerpAsset = serde_json::from_str(json).unwrap();
771
772 assert_eq!(asset.name, "xyz:TSLA");
773 assert_eq!(asset.sz_decimals, 3);
774 assert_eq!(asset.max_leverage, Some(10));
775 assert_eq!(asset.only_isolated, Some(true));
776 assert_eq!(asset.growth_mode.as_deref(), Some("enabled"));
777 assert_eq!(asset.margin_mode.as_deref(), Some("strictIsolated"));
778 }
779
780 #[rstest]
781 fn test_perp_asset_hip3_fields_absent() {
782 let json = r#"{"name": "BTC", "szDecimals": 5}"#;
783
784 let asset: PerpAsset = serde_json::from_str(json).unwrap();
785
786 assert_eq!(asset.growth_mode, None);
787 assert_eq!(asset.margin_mode, None);
788 }
789
790 #[rstest]
791 fn test_outcome_meta_defaults_missing_side_specs() {
792 let json = r#"{
793 "outcomes": [
794 {
795 "outcome": 123,
796 "name": "Recurring",
797 "description": "class:priceBinary|underlying:HYPE|expiry:20260310-1100|targetPrice:34.5|period:3m"
798 }
799 ]
800 }"#;
801
802 let meta: OutcomeMeta = serde_json::from_str(json).unwrap();
803
804 assert_eq!(meta.outcomes.len(), 1);
805 assert_eq!(meta.outcomes[0].outcome, 123);
806 assert!(meta.outcomes[0].side_specs.is_empty());
807 }
808
809 #[rstest]
810 fn test_l2_book_deserialization() {
811 let json = r#"{"coin": "BTC", "levels": [[{"px": "50000", "sz": "1.5"}], [{"px": "50100", "sz": "2.0"}]], "time": 1234567890}"#;
812
813 let book: HyperliquidL2Book = serde_json::from_str(json).unwrap();
814
815 assert_eq!(book.coin, "BTC");
816 assert_eq!(book.levels.len(), 2);
817 assert_eq!(book.time, 1234567890);
818 }
819
820 #[rstest]
821 fn test_exchange_response_deserialization() {
822 let json = r#"{"status": "ok", "response": {"type": "order"}}"#;
823
824 let response: HyperliquidExchangeResponse = serde_json::from_str(json).unwrap();
825 assert!(response.is_ok());
826 }
827
828 #[rstest]
829 fn test_spot_clearinghouse_state_deserialization() {
830 let json = r#"{
831 "balances": [
832 {"coin": "USDC", "token": 0, "total": "14.625485", "hold": "0.0", "entryNtl": "0.0"},
833 {"coin": "PURR", "token": 1, "total": "2000", "hold": "100", "entryNtl": "1234.56"}
834 ]
835 }"#;
836
837 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
838
839 assert_eq!(state.balances.len(), 2);
840 let usdc = &state.balances[0];
841 assert_eq!(usdc.coin.as_str(), "USDC");
842 assert_eq!(usdc.token, Some(0));
843 assert_eq!(usdc.total.to_string(), "14.625485");
844 assert_eq!(usdc.hold, rust_decimal::Decimal::ZERO);
845 assert_eq!(usdc.free().to_string(), "14.625485");
846 assert_eq!(usdc.avg_entry_px(), None);
847
848 let purr = &state.balances[1];
849 assert_eq!(purr.coin.as_str(), "PURR");
850 assert_eq!(purr.token, Some(1));
851 assert_eq!(purr.free().to_string(), "1900");
852 assert_eq!(
853 purr.avg_entry_px().unwrap(),
854 rust_decimal_macros::dec!(0.61728)
855 );
856 }
857
858 #[rstest]
859 fn test_spot_balance_outcome_side_token_lacks_token_field() {
860 let json = r#"{"coin": "+250", "total": "0.0", "hold": "0.0", "entryNtl": "0.0"}"#;
862 let balance: SpotBalance = serde_json::from_str(json).unwrap();
863 assert_eq!(balance.coin.as_str(), "+250");
864 assert_eq!(balance.token, None);
865 }
866
867 #[rstest]
868 fn test_spot_clearinghouse_state_empty() {
869 let json = r#"{"balances": []}"#;
870 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
871 assert!(state.balances.is_empty());
872 }
873
874 #[rstest]
875 fn test_spot_balance_handles_missing_entry_ntl() {
876 let json = r#"{"coin": "HYPE", "token": 150, "total": "5", "hold": "0"}"#;
877 let balance: SpotBalance = serde_json::from_str(json).unwrap();
878 assert_eq!(balance.entry_ntl, None);
879 assert_eq!(balance.avg_entry_px(), None);
880 }
881
882 #[rstest]
883 fn test_msgpack_serialization_matches_python() {
884 let action = HyperliquidExecAction::Order {
889 orders: vec![],
890 grouping: HyperliquidExecGrouping::Na,
891 builder: None,
892 };
893
894 let json = serde_json::to_string(&action).unwrap();
896 assert!(
897 json.contains(r#""type":"order""#),
898 "JSON should have type tag: {json}"
899 );
900
901 let msgpack_bytes = rmp_serde::to_vec_named(&action).unwrap();
903
904 let decoded: serde_json::Value = rmp_serde::from_slice(&msgpack_bytes).unwrap();
906
907 assert!(
909 decoded.get("type").is_some(),
910 "MsgPack should have type tag. Decoded: {decoded:?}"
911 );
912 assert_eq!(
913 decoded.get("type").unwrap().as_str().unwrap(),
914 "order",
915 "Type should be 'order'"
916 );
917 assert!(decoded.get("orders").is_some(), "Should have orders field");
918 assert!(
919 decoded.get("grouping").is_some(),
920 "Should have grouping field"
921 );
922 }
923
924 #[rstest]
925 fn test_user_outcome_split_serialization() {
926 let action = HyperliquidExecAction::UserOutcome {
927 op: HyperliquidExecUserOutcomeOp::SplitOutcome(HyperliquidExecSplitOutcomeParams {
928 outcome: 1,
929 amount: dec!(123.0),
930 }),
931 };
932
933 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
934 assert_eq!(
935 value,
936 json!({
937 "type": "userOutcome",
938 "splitOutcome": { "outcome": 1, "amount": "123.0" }
939 })
940 );
941 }
942
943 #[rstest]
944 fn test_user_outcome_split_msgpack_roundtrip() {
945 let action = HyperliquidExecAction::UserOutcome {
946 op: HyperliquidExecUserOutcomeOp::SplitOutcome(HyperliquidExecSplitOutcomeParams {
947 outcome: 4,
948 amount: dec!(10),
949 }),
950 };
951
952 let bytes = rmp_serde::to_vec_named(&action).unwrap();
953 let decoded: serde_json::Value = rmp_serde::from_slice(&bytes).unwrap();
954 assert_eq!(
955 decoded,
956 json!({
957 "type": "userOutcome",
958 "splitOutcome": { "outcome": 4, "amount": "10" }
959 })
960 );
961 }
962
963 #[rstest]
964 fn test_user_outcome_merge_outcome_serialization() {
965 let action = HyperliquidExecAction::UserOutcome {
966 op: HyperliquidExecUserOutcomeOp::MergeOutcome(HyperliquidExecMergeOutcomeParams {
967 outcome: 1,
968 amount: Some(dec!(5.0)),
969 }),
970 };
971 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
972 assert_eq!(
973 value,
974 json!({
975 "type": "userOutcome",
976 "mergeOutcome": { "outcome": 1, "amount": "5.0" }
977 })
978 );
979 }
980
981 #[rstest]
982 fn test_user_outcome_merge_outcome_null_amount_means_max() {
983 let action = HyperliquidExecAction::UserOutcome {
984 op: HyperliquidExecUserOutcomeOp::MergeOutcome(HyperliquidExecMergeOutcomeParams {
985 outcome: 7,
986 amount: None,
987 }),
988 };
989 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
990 assert_eq!(
991 value,
992 json!({
993 "type": "userOutcome",
994 "mergeOutcome": { "outcome": 7, "amount": null }
995 })
996 );
997 }
998
999 #[rstest]
1000 fn test_user_outcome_merge_question_serialization() {
1001 let action = HyperliquidExecAction::UserOutcome {
1002 op: HyperliquidExecUserOutcomeOp::MergeQuestion(HyperliquidExecMergeQuestionParams {
1003 question: 9,
1004 amount: Some(dec!(2.0)),
1005 }),
1006 };
1007 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1008 assert_eq!(
1009 value,
1010 json!({
1011 "type": "userOutcome",
1012 "mergeQuestion": { "question": 9, "amount": "2.0" }
1013 })
1014 );
1015 }
1016
1017 #[rstest]
1018 fn test_user_outcome_merge_question_null_amount_means_max() {
1019 let action = HyperliquidExecAction::UserOutcome {
1020 op: HyperliquidExecUserOutcomeOp::MergeQuestion(HyperliquidExecMergeQuestionParams {
1021 question: 9,
1022 amount: None,
1023 }),
1024 };
1025 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1026 assert_eq!(
1027 value,
1028 json!({
1029 "type": "userOutcome",
1030 "mergeQuestion": { "question": 9, "amount": null }
1031 })
1032 );
1033 }
1034
1035 #[rstest]
1036 fn test_user_outcome_negate_outcome_serialization() {
1037 let action = HyperliquidExecAction::UserOutcome {
1038 op: HyperliquidExecUserOutcomeOp::NegateOutcome(HyperliquidExecNegateOutcomeParams {
1039 question: 9,
1040 outcome: 52,
1041 amount: dec!(1.5),
1042 }),
1043 };
1044 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1045 assert_eq!(
1046 value,
1047 json!({
1048 "type": "userOutcome",
1049 "negateOutcome": { "question": 9, "outcome": 52, "amount": "1.5" }
1050 })
1051 );
1052 }
1053}
1054
1055#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1059pub enum HyperliquidExecTif {
1060 #[serde(rename = "Alo")]
1062 Alo,
1063 #[serde(rename = "Ioc")]
1065 Ioc,
1066 #[serde(rename = "Gtc")]
1068 Gtc,
1069}
1070
1071#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1073pub enum HyperliquidExecTpSl {
1074 #[serde(rename = "tp")]
1076 Tp,
1077 #[serde(rename = "sl")]
1079 Sl,
1080}
1081
1082#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1084pub enum HyperliquidExecGrouping {
1085 #[serde(rename = "na")]
1087 #[default]
1088 Na,
1089 #[serde(rename = "normalTpsl")]
1091 NormalTpsl,
1092 #[serde(rename = "positionTpsl")]
1094 PositionTpsl,
1095}
1096
1097#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1099#[serde(untagged)]
1100pub enum HyperliquidExecOrderKind {
1101 Limit {
1103 limit: HyperliquidExecLimitParams,
1105 },
1106 Trigger {
1108 trigger: HyperliquidExecTriggerParams,
1110 },
1111}
1112
1113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1115pub struct HyperliquidExecLimitParams {
1116 pub tif: HyperliquidExecTif,
1118}
1119
1120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1122#[serde(rename_all = "camelCase")]
1123pub struct HyperliquidExecTriggerParams {
1124 pub is_market: bool,
1126 #[serde(
1128 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1129 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1130 )]
1131 pub trigger_px: Decimal,
1132 pub tpsl: HyperliquidExecTpSl,
1134}
1135
1136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1141pub struct HyperliquidExecBuilderFee {
1142 #[serde(rename = "b")]
1144 pub address: String,
1145 #[serde(rename = "f")]
1147 pub fee_tenths_bp: u32,
1148}
1149
1150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1155pub struct HyperliquidExecPlaceOrderRequest {
1156 #[serde(rename = "a")]
1158 pub asset: AssetId,
1159 #[serde(rename = "b")]
1161 pub is_buy: bool,
1162 #[serde(
1164 rename = "p",
1165 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1166 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1167 )]
1168 pub price: Decimal,
1169 #[serde(
1171 rename = "s",
1172 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1173 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1174 )]
1175 pub size: Decimal,
1176 #[serde(rename = "r")]
1178 pub reduce_only: bool,
1179 #[serde(rename = "t")]
1181 pub kind: HyperliquidExecOrderKind,
1182 #[serde(rename = "c", skip_serializing_if = "Option::is_none")]
1184 pub cloid: Option<Cloid>,
1185}
1186
1187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1189pub struct HyperliquidExecCancelOrderRequest {
1190 #[serde(rename = "a")]
1192 pub asset: AssetId,
1193 #[serde(rename = "o")]
1195 pub oid: OrderId,
1196}
1197
1198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1203pub struct HyperliquidExecCancelByCloidRequest {
1204 pub asset: AssetId,
1206 pub cloid: Cloid,
1208}
1209
1210#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1215pub struct HyperliquidExecModifyOrderRequest {
1216 pub oid: OrderId,
1218 pub order: HyperliquidExecPlaceOrderRequest,
1220}
1221
1222#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1227pub struct HyperliquidExecSplitOutcomeParams {
1228 pub outcome: u32,
1230 #[serde(
1232 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1233 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1234 )]
1235 pub amount: Decimal,
1236}
1237
1238#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1244pub struct HyperliquidExecMergeOutcomeParams {
1245 pub outcome: u32,
1247 #[serde(
1249 default,
1250 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1251 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
1252 )]
1253 pub amount: Option<Decimal>,
1254}
1255
1256#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1262pub struct HyperliquidExecMergeQuestionParams {
1263 pub question: u32,
1265 #[serde(
1267 default,
1268 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1269 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
1270 )]
1271 pub amount: Option<Decimal>,
1272}
1273
1274#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1279pub struct HyperliquidExecNegateOutcomeParams {
1280 pub question: u32,
1282 pub outcome: u32,
1284 #[serde(
1286 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1287 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1288 )]
1289 pub amount: Decimal,
1290}
1291
1292#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1299pub enum HyperliquidExecUserOutcomeOp {
1300 #[serde(rename = "splitOutcome")]
1302 SplitOutcome(HyperliquidExecSplitOutcomeParams),
1303 #[serde(rename = "mergeOutcome")]
1306 MergeOutcome(HyperliquidExecMergeOutcomeParams),
1307 #[serde(rename = "mergeQuestion")]
1310 MergeQuestion(HyperliquidExecMergeQuestionParams),
1311 #[serde(rename = "negateOutcome")]
1314 NegateOutcome(HyperliquidExecNegateOutcomeParams),
1315}
1316
1317#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1319pub struct HyperliquidExecTwapRequest {
1320 #[serde(rename = "a")]
1322 pub asset: AssetId,
1323 #[serde(rename = "b")]
1325 pub is_buy: bool,
1326 #[serde(
1328 rename = "s",
1329 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1330 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1331 )]
1332 pub size: Decimal,
1333 #[serde(rename = "m")]
1335 pub duration_ms: u64,
1336}
1337
1338#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1344#[serde(tag = "type")]
1345pub enum HyperliquidExecAction {
1346 #[serde(rename = "order")]
1348 Order {
1349 orders: Vec<HyperliquidExecPlaceOrderRequest>,
1351 #[serde(default)]
1353 grouping: HyperliquidExecGrouping,
1354 #[serde(skip_serializing_if = "Option::is_none")]
1356 builder: Option<HyperliquidExecBuilderFee>,
1357 },
1358
1359 #[serde(rename = "cancel")]
1361 Cancel {
1362 cancels: Vec<HyperliquidExecCancelOrderRequest>,
1364 },
1365
1366 #[serde(rename = "cancelByCloid")]
1368 CancelByCloid {
1369 cancels: Vec<HyperliquidExecCancelByCloidRequest>,
1371 },
1372
1373 #[serde(rename = "modify")]
1375 Modify {
1376 #[serde(flatten)]
1378 modify: HyperliquidExecModifyOrderRequest,
1379 },
1380
1381 #[serde(rename = "batchModify")]
1383 BatchModify {
1384 modifies: Vec<HyperliquidExecModifyOrderRequest>,
1386 },
1387
1388 #[serde(rename = "scheduleCancel")]
1390 ScheduleCancel {
1391 #[serde(skip_serializing_if = "Option::is_none")]
1394 time: Option<u64>,
1395 },
1396
1397 #[serde(rename = "updateLeverage")]
1399 UpdateLeverage {
1400 #[serde(rename = "a")]
1402 asset: AssetId,
1403 #[serde(rename = "isCross")]
1405 is_cross: bool,
1406 #[serde(rename = "leverage")]
1408 leverage: u32,
1409 },
1410
1411 #[serde(rename = "updateIsolatedMargin")]
1413 UpdateIsolatedMargin {
1414 #[serde(rename = "a")]
1416 asset: AssetId,
1417 #[serde(
1419 rename = "delta",
1420 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1421 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1422 )]
1423 delta: Decimal,
1424 },
1425
1426 #[serde(rename = "usdClassTransfer")]
1428 UsdClassTransfer {
1429 from: String,
1431 to: String,
1433 #[serde(
1435 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1436 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1437 )]
1438 amount: Decimal,
1439 },
1440
1441 #[serde(rename = "userOutcome")]
1447 UserOutcome {
1448 #[serde(flatten)]
1450 op: HyperliquidExecUserOutcomeOp,
1451 },
1452
1453 #[serde(rename = "twapPlace")]
1455 TwapPlace {
1456 #[serde(flatten)]
1458 twap: HyperliquidExecTwapRequest,
1459 },
1460
1461 #[serde(rename = "twapCancel")]
1463 TwapCancel {
1464 #[serde(rename = "a")]
1466 asset: AssetId,
1467 #[serde(rename = "t")]
1469 twap_id: u64,
1470 },
1471
1472 #[serde(rename = "noop")]
1474 Noop,
1475}
1476
1477#[derive(Debug, Clone, Serialize)]
1482#[serde(rename_all = "camelCase")]
1483pub struct HyperliquidExecRequest {
1484 pub action: HyperliquidExecAction,
1486 pub nonce: u64,
1488 pub signature: String,
1490 #[serde(skip_serializing_if = "Option::is_none")]
1492 pub vault_address: Option<String>,
1493 #[serde(skip_serializing_if = "Option::is_none")]
1496 pub expires_after: Option<u64>,
1497}
1498
1499#[derive(Debug, Clone, Serialize, Deserialize)]
1501pub struct HyperliquidExecResponse {
1502 pub status: String,
1504 pub response: HyperliquidExecResponseData,
1506}
1507
1508#[derive(Debug, Clone, Serialize, Deserialize)]
1510#[serde(tag = "type")]
1511pub enum HyperliquidExecResponseData {
1512 #[serde(rename = "order")]
1514 Order {
1515 data: HyperliquidExecOrderResponseData,
1517 },
1518 #[serde(rename = "cancel")]
1520 Cancel {
1521 data: HyperliquidExecCancelResponseData,
1523 },
1524 #[serde(rename = "modify")]
1526 Modify {
1527 data: HyperliquidExecModifyResponseData,
1529 },
1530 #[serde(rename = "default")]
1532 Default,
1533 #[serde(other)]
1535 Unknown,
1536}
1537
1538#[derive(Debug, Clone, Serialize, Deserialize)]
1540pub struct HyperliquidExecOrderResponseData {
1541 pub statuses: Vec<HyperliquidExecOrderStatus>,
1543}
1544
1545#[derive(Debug, Clone, Serialize, Deserialize)]
1547pub struct HyperliquidExecCancelResponseData {
1548 pub statuses: Vec<HyperliquidExecCancelStatus>,
1550}
1551
1552#[derive(Debug, Clone, Serialize, Deserialize)]
1554pub struct HyperliquidExecModifyResponseData {
1555 pub statuses: Vec<HyperliquidExecModifyStatus>,
1557}
1558
1559#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1561#[serde(untagged)]
1562pub enum HyperliquidExecOrderStatus {
1563 Resting {
1565 resting: HyperliquidExecRestingInfo,
1567 },
1568 Filled {
1570 filled: HyperliquidExecFilledInfo,
1572 },
1573 Error {
1575 error: String,
1577 },
1578}
1579
1580#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1582pub struct HyperliquidExecRestingInfo {
1583 pub oid: OrderId,
1585}
1586
1587#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1589pub struct HyperliquidExecFilledInfo {
1590 #[serde(
1592 rename = "totalSz",
1593 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1594 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1595 )]
1596 pub total_sz: Decimal,
1597 #[serde(
1599 rename = "avgPx",
1600 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1601 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1602 )]
1603 pub avg_px: Decimal,
1604 pub oid: OrderId,
1606}
1607
1608#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1610#[serde(untagged)]
1611pub enum HyperliquidExecCancelStatus {
1612 Success(String), Error {
1616 error: String,
1618 },
1619}
1620
1621#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1623#[serde(untagged)]
1624pub enum HyperliquidExecModifyStatus {
1625 Success(String), Error {
1629 error: String,
1631 },
1632}
1633
1634#[derive(Debug, Clone, Serialize, Deserialize)]
1637#[serde(rename_all = "camelCase")]
1638pub struct ClearinghouseState {
1639 #[serde(default)]
1641 pub asset_positions: Vec<AssetPosition>,
1642 #[serde(default)]
1644 pub cross_margin_summary: Option<CrossMarginSummary>,
1645 #[serde(
1647 default,
1648 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1649 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
1650 )]
1651 pub withdrawable: Option<Decimal>,
1652 #[serde(default)]
1654 pub time: Option<u64>,
1655}
1656
1657#[derive(Debug, Clone, Serialize, Deserialize)]
1659#[serde(rename_all = "camelCase")]
1660pub struct AssetPosition {
1661 pub position: PositionData,
1663 #[serde(rename = "type")]
1665 pub position_type: HyperliquidPositionType,
1666}
1667
1668#[derive(Debug, Clone, Serialize, Deserialize)]
1670#[serde(rename_all = "camelCase")]
1671pub struct LeverageInfo {
1672 #[serde(rename = "type")]
1673 pub leverage_type: HyperliquidLeverageType,
1674 pub value: u32,
1676}
1677
1678#[derive(Debug, Clone, Serialize, Deserialize)]
1680#[serde(rename_all = "camelCase")]
1681pub struct CumFundingInfo {
1682 #[serde(
1684 rename = "allTime",
1685 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1686 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1687 )]
1688 pub all_time: Decimal,
1689 #[serde(
1691 rename = "sinceOpen",
1692 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1693 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1694 )]
1695 pub since_open: Decimal,
1696 #[serde(
1698 rename = "sinceChange",
1699 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1700 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1701 )]
1702 pub since_change: Decimal,
1703}
1704
1705#[derive(Debug, Clone, Serialize, Deserialize)]
1707#[serde(rename_all = "camelCase")]
1708pub struct PositionData {
1709 pub coin: Ustr,
1711 #[serde(rename = "cumFunding")]
1713 pub cum_funding: CumFundingInfo,
1714 #[serde(
1716 rename = "entryPx",
1717 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1718 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str",
1719 default
1720 )]
1721 pub entry_px: Option<Decimal>,
1722 pub leverage: LeverageInfo,
1724 #[serde(
1726 rename = "liquidationPx",
1727 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1728 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str",
1729 default
1730 )]
1731 pub liquidation_px: Option<Decimal>,
1732 #[serde(
1734 rename = "marginUsed",
1735 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1736 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1737 )]
1738 pub margin_used: Decimal,
1739 #[serde(rename = "maxLeverage", default)]
1741 pub max_leverage: Option<u32>,
1742 #[serde(
1744 rename = "positionValue",
1745 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1746 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1747 )]
1748 pub position_value: Decimal,
1749 #[serde(
1751 rename = "returnOnEquity",
1752 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1753 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1754 )]
1755 pub return_on_equity: Decimal,
1756 #[serde(
1758 rename = "szi",
1759 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1760 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1761 )]
1762 pub szi: Decimal,
1763 #[serde(
1765 rename = "unrealizedPnl",
1766 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1767 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1768 )]
1769 pub unrealized_pnl: Decimal,
1770}
1771
1772#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1778#[serde(rename_all = "camelCase")]
1779pub struct SpotClearinghouseState {
1780 #[serde(default)]
1782 pub balances: Vec<SpotBalance>,
1783}
1784
1785#[derive(Debug, Clone, Serialize, Deserialize)]
1787#[serde(rename_all = "camelCase")]
1788pub struct SpotBalance {
1789 pub coin: Ustr,
1791 #[serde(default)]
1794 pub token: Option<u32>,
1795 #[serde(
1797 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1798 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1799 )]
1800 pub total: Decimal,
1801 #[serde(
1803 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1804 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1805 )]
1806 pub hold: Decimal,
1807 #[serde(
1809 default,
1810 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1811 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
1812 )]
1813 pub entry_ntl: Option<Decimal>,
1814}
1815
1816impl SpotBalance {
1817 #[must_use]
1819 pub fn free(&self) -> Decimal {
1820 (self.total - self.hold).max(Decimal::ZERO)
1821 }
1822
1823 #[must_use]
1825 pub fn avg_entry_px(&self) -> Option<Decimal> {
1826 let entry_ntl = self.entry_ntl?;
1827
1828 if entry_ntl.is_zero() || self.total.is_zero() {
1829 return None;
1830 }
1831
1832 Some(entry_ntl / self.total)
1833 }
1834}
1835
1836#[derive(Debug, Clone, Serialize, Deserialize)]
1838#[serde(rename_all = "camelCase")]
1839pub struct CrossMarginSummary {
1840 #[serde(
1842 rename = "accountValue",
1843 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1844 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1845 )]
1846 pub account_value: Decimal,
1847 #[serde(
1849 rename = "totalNtlPos",
1850 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1851 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1852 )]
1853 pub total_ntl_pos: Decimal,
1854 #[serde(
1856 rename = "totalRawUsd",
1857 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1858 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1859 )]
1860 pub total_raw_usd: Decimal,
1861 #[serde(
1863 rename = "totalMarginUsed",
1864 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1865 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1866 )]
1867 pub total_margin_used: Decimal,
1868 #[serde(
1870 rename = "withdrawable",
1871 default,
1872 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1873 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
1874 )]
1875 pub withdrawable: Option<Decimal>,
1876}