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)]
259#[serde(rename_all = "camelCase")]
260pub struct OutcomeMeta {
261 pub outcomes: Vec<OutcomeMarket>,
263 #[serde(default)]
267 pub questions: Vec<OutcomeQuestion>,
268}
269
270impl OutcomeMeta {
271 #[must_use]
274 pub fn parent_question(&self, outcome_index: u32) -> Option<&OutcomeQuestion> {
275 self.questions.iter().find(|q| {
276 q.fallback_outcome == Some(outcome_index) || q.named_outcomes.contains(&outcome_index)
277 })
278 }
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283#[serde(rename_all = "camelCase")]
284pub struct OutcomeMarket {
285 pub outcome: u32,
287 pub name: String,
289 pub description: String,
291 #[serde(default)]
293 pub side_specs: Vec<OutcomeSideSpec>,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
298#[serde(rename_all = "camelCase")]
299pub struct OutcomeSideSpec {
300 pub name: String,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
310#[serde(rename_all = "camelCase")]
311pub struct OutcomeQuestion {
312 pub question: u32,
314 pub name: String,
316 pub description: String,
318 #[serde(default)]
320 pub fallback_outcome: Option<u32>,
321 #[serde(default)]
323 pub named_outcomes: Vec<u32>,
324 #[serde(default)]
326 pub settled_named_outcomes: Vec<u32>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
332#[serde(untagged)]
333pub enum PerpMetaAndCtxs {
334 Payload(Box<(PerpMeta, Vec<PerpAssetCtx>)>),
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
340#[serde(rename_all = "camelCase")]
341pub struct PerpAssetCtx {
342 #[serde(default)]
344 pub mark_px: Option<String>,
345 #[serde(default)]
347 pub mid_px: Option<String>,
348 #[serde(default)]
350 pub funding: Option<String>,
351 #[serde(default)]
353 pub open_interest: Option<String>,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
359#[serde(untagged)]
360pub enum SpotMetaAndCtxs {
361 Payload(Box<(SpotMeta, Vec<SpotAssetCtx>)>),
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
367#[serde(rename_all = "camelCase")]
368pub struct SpotAssetCtx {
369 #[serde(default)]
371 pub mark_px: Option<String>,
372 #[serde(default)]
374 pub mid_px: Option<String>,
375 #[serde(default)]
377 pub day_volume: Option<String>,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct HyperliquidL2Book {
383 pub coin: Ustr,
385 pub levels: Vec<Vec<HyperliquidLevel>>,
387 pub time: u64,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct HyperliquidLevel {
394 pub px: String,
396 pub sz: String,
398}
399
400pub type HyperliquidFills = Vec<HyperliquidFill>;
404
405#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct HyperliquidMeta {
408 #[serde(default)]
409 pub universe: Vec<HyperliquidAssetInfo>,
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize)]
414#[serde(rename_all = "camelCase")]
415pub struct HyperliquidCandle {
416 #[serde(rename = "t")]
418 pub timestamp: u64,
419 #[serde(rename = "T")]
421 pub end_timestamp: u64,
422 #[serde(rename = "o")]
424 pub open: String,
425 #[serde(rename = "h")]
427 pub high: String,
428 #[serde(rename = "l")]
430 pub low: String,
431 #[serde(rename = "c")]
433 pub close: String,
434 #[serde(rename = "v")]
436 pub volume: String,
437 #[serde(rename = "n", default)]
439 pub num_trades: Option<u64>,
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct HyperliquidFundingHistoryEntry {
445 pub coin: Ustr,
447 #[serde(rename = "fundingRate")]
449 pub funding_rate: String,
450 #[serde(default)]
452 pub premium: Option<String>,
453 pub time: u64,
455}
456
457#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct HyperliquidFill {
460 pub coin: Ustr,
462 pub px: String,
464 pub sz: String,
466 pub side: HyperliquidSide,
468 pub time: u64,
470 #[serde(rename = "startPosition")]
472 pub start_position: String,
473 pub dir: HyperliquidFillDirection,
475 #[serde(rename = "closedPnl")]
477 pub closed_pnl: String,
478 pub hash: String,
480 pub oid: u64,
482 pub crossed: bool,
484 pub fee: String,
486 #[serde(rename = "feeToken")]
488 pub fee_token: Ustr,
489}
490
491#[derive(Debug, Clone, Serialize, Deserialize)]
496#[serde(tag = "status", rename_all = "camelCase")]
497pub enum HyperliquidOrderStatus {
498 Order { order: HyperliquidOrderStatusEntry },
499 UnknownOid,
500}
501
502impl HyperliquidOrderStatus {
503 #[must_use]
505 pub fn into_order(self) -> Option<HyperliquidOrderStatusEntry> {
506 match self {
507 Self::Order { order } => Some(order),
508 Self::UnknownOid => None,
509 }
510 }
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct HyperliquidOrderStatusEntry {
516 pub order: HyperliquidOrderInfo,
518 pub status: HyperliquidOrderStatusEnum,
520 #[serde(rename = "statusTimestamp")]
522 pub status_timestamp: u64,
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
527pub struct HyperliquidOrderInfo {
528 pub coin: Ustr,
530 pub side: HyperliquidSide,
532 #[serde(rename = "limitPx")]
534 pub limit_px: String,
535 pub sz: String,
537 pub oid: u64,
539 pub timestamp: u64,
541 #[serde(rename = "origSz")]
543 pub orig_sz: String,
544 #[serde(default)]
546 pub cloid: Option<String>,
547}
548
549#[derive(Debug, Clone, Serialize)]
551pub struct HyperliquidSignature {
552 pub r: String,
554 pub s: String,
556 pub v: u64,
558}
559
560impl HyperliquidSignature {
561 #[must_use]
563 pub fn new(r: String, s: String, v: u64) -> Self {
564 Self { r, s, v }
565 }
566
567 #[must_use]
569 pub fn to_hex(&self) -> String {
570 let r = self.r.strip_prefix("0x").unwrap_or(&self.r);
571 let s = self.s.strip_prefix("0x").unwrap_or(&self.s);
572 format!("0x{r}{s}{:02x}", self.v)
573 }
574
575 pub fn from_hex(sig_hex: &str) -> Result<Self, String> {
577 let sig_hex = sig_hex.strip_prefix("0x").unwrap_or(sig_hex);
578
579 if sig_hex.len() != 130 {
580 return Err(format!(
581 "Invalid signature length: expected 130 hex chars, was {}",
582 sig_hex.len()
583 ));
584 }
585
586 let r = format!("0x{}", &sig_hex[0..64]);
587 let s = format!("0x{}", &sig_hex[64..128]);
588 let v = u64::from_str_radix(&sig_hex[128..130], 16)
589 .map_err(|e| format!("Failed to parse v component: {e}"))?;
590
591 Ok(Self { r, s, v })
592 }
593}
594
595#[derive(Debug, Clone, Serialize)]
597pub struct HyperliquidExchangeRequest<T> {
598 #[serde(rename = "action")]
600 pub action: T,
601 #[serde(rename = "nonce")]
603 pub nonce: u64,
604 #[serde(rename = "signature")]
606 pub signature: HyperliquidSignature,
607 #[serde(rename = "vaultAddress", skip_serializing_if = "Option::is_none")]
609 pub vault_address: Option<String>,
610 #[serde(rename = "expiresAfter", skip_serializing_if = "Option::is_none")]
612 pub expires_after: Option<u64>,
613}
614
615impl<T> HyperliquidExchangeRequest<T>
616where
617 T: Serialize,
618{
619 #[must_use]
621 pub fn new(action: T, nonce: u64, signature: HyperliquidSignature) -> Self {
622 Self {
623 action,
624 nonce,
625 signature,
626 vault_address: None,
627 expires_after: None,
628 }
629 }
630
631 #[must_use]
633 pub fn with_vault(
634 action: T,
635 nonce: u64,
636 signature: HyperliquidSignature,
637 vault_address: String,
638 ) -> Self {
639 Self {
640 action,
641 nonce,
642 signature,
643 vault_address: Some(vault_address),
644 expires_after: None,
645 }
646 }
647
648 pub fn to_sign_value(&self) -> serde_json::Result<serde_json::Value> {
650 serde_json::to_value(self)
651 }
652}
653
654#[derive(Debug, Clone, Serialize, Deserialize)]
656#[serde(untagged)]
657pub enum HyperliquidExchangeResponse {
658 Status {
660 status: String,
662 response: serde_json::Value,
664 },
665 Error {
667 error: String,
669 },
670}
671
672impl HyperliquidExchangeResponse {
673 pub fn is_ok(&self) -> bool {
674 matches!(self, Self::Status { status, .. } if status == RESPONSE_STATUS_OK)
675 }
676}
677
678pub const RESPONSE_STATUS_OK: &str = "ok";
680
681#[cfg(test)]
682mod tests {
683 use rstest::rstest;
684 use rust_decimal_macros::dec;
685 use serde_json::json;
686
687 use super::*;
688
689 #[rstest]
690 fn test_meta_deserialization() {
691 let json = r#"{"universe": [{"name": "BTC", "szDecimals": 5}]}"#;
692
693 let meta: HyperliquidMeta = serde_json::from_str(json).unwrap();
694
695 assert_eq!(meta.universe.len(), 1);
696 assert_eq!(meta.universe[0].name, "BTC");
697 assert_eq!(meta.universe[0].sz_decimals, 5);
698 }
699
700 #[rstest]
701 fn test_funding_history_entry_with_premium() {
702 let json = r#"{
703 "coin": "BTC",
704 "fundingRate": "0.0000125",
705 "premium": "0.00029005",
706 "time": 1769908800000
707 }"#;
708
709 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
710
711 assert_eq!(entry.coin.as_str(), "BTC");
712 assert_eq!(entry.funding_rate, "0.0000125");
713 assert_eq!(entry.premium.as_deref(), Some("0.00029005"));
714 assert_eq!(entry.time, 1769908800000);
715 }
716
717 #[rstest]
718 fn test_funding_history_entry_without_premium() {
719 let json = r#"{
722 "coin": "BTC",
723 "fundingRate": "0.0000033",
724 "time": 1769916000000
725 }"#;
726
727 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
728
729 assert!(entry.premium.is_none());
730 assert_eq!(entry.funding_rate, "0.0000033");
731 }
732
733 #[rstest]
734 fn test_perp_asset_hip3_fields() {
735 let json = r#"{
736 "name": "xyz:TSLA",
737 "szDecimals": 3,
738 "maxLeverage": 10,
739 "onlyIsolated": true,
740 "growthMode": "enabled",
741 "marginMode": "strictIsolated"
742 }"#;
743
744 let asset: PerpAsset = serde_json::from_str(json).unwrap();
745
746 assert_eq!(asset.name, "xyz:TSLA");
747 assert_eq!(asset.sz_decimals, 3);
748 assert_eq!(asset.max_leverage, Some(10));
749 assert_eq!(asset.only_isolated, Some(true));
750 assert_eq!(asset.growth_mode.as_deref(), Some("enabled"));
751 assert_eq!(asset.margin_mode.as_deref(), Some("strictIsolated"));
752 }
753
754 #[rstest]
755 fn test_perp_asset_hip3_fields_absent() {
756 let json = r#"{"name": "BTC", "szDecimals": 5}"#;
757
758 let asset: PerpAsset = serde_json::from_str(json).unwrap();
759
760 assert_eq!(asset.growth_mode, None);
761 assert_eq!(asset.margin_mode, None);
762 }
763
764 #[rstest]
765 fn test_outcome_meta_defaults_missing_side_specs() {
766 let json = r#"{
767 "outcomes": [
768 {
769 "outcome": 123,
770 "name": "Recurring",
771 "description": "class:priceBinary|underlying:HYPE|expiry:20260310-1100|targetPrice:34.5|period:3m"
772 }
773 ]
774 }"#;
775
776 let meta: OutcomeMeta = serde_json::from_str(json).unwrap();
777
778 assert_eq!(meta.outcomes.len(), 1);
779 assert_eq!(meta.outcomes[0].outcome, 123);
780 assert!(meta.outcomes[0].side_specs.is_empty());
781 }
782
783 #[rstest]
784 fn test_l2_book_deserialization() {
785 let json = r#"{"coin": "BTC", "levels": [[{"px": "50000", "sz": "1.5"}], [{"px": "50100", "sz": "2.0"}]], "time": 1234567890}"#;
786
787 let book: HyperliquidL2Book = serde_json::from_str(json).unwrap();
788
789 assert_eq!(book.coin, "BTC");
790 assert_eq!(book.levels.len(), 2);
791 assert_eq!(book.time, 1234567890);
792 }
793
794 #[rstest]
795 fn test_exchange_response_deserialization() {
796 let json = r#"{"status": "ok", "response": {"type": "order"}}"#;
797
798 let response: HyperliquidExchangeResponse = serde_json::from_str(json).unwrap();
799 assert!(response.is_ok());
800 }
801
802 #[rstest]
803 fn test_spot_clearinghouse_state_deserialization() {
804 let json = r#"{
805 "balances": [
806 {"coin": "USDC", "token": 0, "total": "14.625485", "hold": "0.0", "entryNtl": "0.0"},
807 {"coin": "PURR", "token": 1, "total": "2000", "hold": "100", "entryNtl": "1234.56"}
808 ]
809 }"#;
810
811 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
812
813 assert_eq!(state.balances.len(), 2);
814 let usdc = &state.balances[0];
815 assert_eq!(usdc.coin.as_str(), "USDC");
816 assert_eq!(usdc.token, Some(0));
817 assert_eq!(usdc.total.to_string(), "14.625485");
818 assert_eq!(usdc.hold, rust_decimal::Decimal::ZERO);
819 assert_eq!(usdc.free().to_string(), "14.625485");
820 assert_eq!(usdc.avg_entry_px(), None);
821
822 let purr = &state.balances[1];
823 assert_eq!(purr.coin.as_str(), "PURR");
824 assert_eq!(purr.token, Some(1));
825 assert_eq!(purr.free().to_string(), "1900");
826 assert_eq!(
827 purr.avg_entry_px().unwrap(),
828 rust_decimal_macros::dec!(0.61728)
829 );
830 }
831
832 #[rstest]
833 fn test_spot_balance_outcome_side_token_lacks_token_field() {
834 let json = r#"{"coin": "+250", "total": "0.0", "hold": "0.0", "entryNtl": "0.0"}"#;
836 let balance: SpotBalance = serde_json::from_str(json).unwrap();
837 assert_eq!(balance.coin.as_str(), "+250");
838 assert_eq!(balance.token, None);
839 }
840
841 #[rstest]
842 fn test_spot_clearinghouse_state_empty() {
843 let json = r#"{"balances": []}"#;
844 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
845 assert!(state.balances.is_empty());
846 }
847
848 #[rstest]
849 fn test_spot_balance_handles_missing_entry_ntl() {
850 let json = r#"{"coin": "HYPE", "token": 150, "total": "5", "hold": "0"}"#;
851 let balance: SpotBalance = serde_json::from_str(json).unwrap();
852 assert_eq!(balance.entry_ntl, None);
853 assert_eq!(balance.avg_entry_px(), None);
854 }
855
856 #[rstest]
857 fn test_msgpack_serialization_matches_python() {
858 let action = HyperliquidExecAction::Order {
863 orders: vec![],
864 grouping: HyperliquidExecGrouping::Na,
865 builder: None,
866 };
867
868 let json = serde_json::to_string(&action).unwrap();
870 assert!(
871 json.contains(r#""type":"order""#),
872 "JSON should have type tag: {json}"
873 );
874
875 let msgpack_bytes = rmp_serde::to_vec_named(&action).unwrap();
877
878 let decoded: serde_json::Value = rmp_serde::from_slice(&msgpack_bytes).unwrap();
880
881 assert!(
883 decoded.get("type").is_some(),
884 "MsgPack should have type tag. Decoded: {decoded:?}"
885 );
886 assert_eq!(
887 decoded.get("type").unwrap().as_str().unwrap(),
888 "order",
889 "Type should be 'order'"
890 );
891 assert!(decoded.get("orders").is_some(), "Should have orders field");
892 assert!(
893 decoded.get("grouping").is_some(),
894 "Should have grouping field"
895 );
896 }
897
898 #[rstest]
899 fn test_user_outcome_split_serialization() {
900 let action = HyperliquidExecAction::UserOutcome {
901 op: HyperliquidExecUserOutcomeOp::SplitOutcome(HyperliquidExecSplitOutcomeParams {
902 outcome: 1,
903 amount: dec!(123.0),
904 }),
905 };
906
907 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
908 assert_eq!(
909 value,
910 json!({
911 "type": "userOutcome",
912 "splitOutcome": { "outcome": 1, "amount": "123.0" }
913 })
914 );
915 }
916
917 #[rstest]
918 fn test_user_outcome_split_msgpack_roundtrip() {
919 let action = HyperliquidExecAction::UserOutcome {
920 op: HyperliquidExecUserOutcomeOp::SplitOutcome(HyperliquidExecSplitOutcomeParams {
921 outcome: 4,
922 amount: dec!(10),
923 }),
924 };
925
926 let bytes = rmp_serde::to_vec_named(&action).unwrap();
927 let decoded: serde_json::Value = rmp_serde::from_slice(&bytes).unwrap();
928 assert_eq!(
929 decoded,
930 json!({
931 "type": "userOutcome",
932 "splitOutcome": { "outcome": 4, "amount": "10" }
933 })
934 );
935 }
936
937 #[rstest]
938 fn test_user_outcome_merge_outcome_serialization() {
939 let action = HyperliquidExecAction::UserOutcome {
940 op: HyperliquidExecUserOutcomeOp::MergeOutcome(HyperliquidExecMergeOutcomeParams {
941 outcome: 1,
942 amount: Some(dec!(5.0)),
943 }),
944 };
945 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
946 assert_eq!(
947 value,
948 json!({
949 "type": "userOutcome",
950 "mergeOutcome": { "outcome": 1, "amount": "5.0" }
951 })
952 );
953 }
954
955 #[rstest]
956 fn test_user_outcome_merge_outcome_null_amount_means_max() {
957 let action = HyperliquidExecAction::UserOutcome {
958 op: HyperliquidExecUserOutcomeOp::MergeOutcome(HyperliquidExecMergeOutcomeParams {
959 outcome: 7,
960 amount: None,
961 }),
962 };
963 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
964 assert_eq!(
965 value,
966 json!({
967 "type": "userOutcome",
968 "mergeOutcome": { "outcome": 7, "amount": null }
969 })
970 );
971 }
972
973 #[rstest]
974 fn test_user_outcome_merge_question_serialization() {
975 let action = HyperliquidExecAction::UserOutcome {
976 op: HyperliquidExecUserOutcomeOp::MergeQuestion(HyperliquidExecMergeQuestionParams {
977 question: 9,
978 amount: Some(dec!(2.0)),
979 }),
980 };
981 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
982 assert_eq!(
983 value,
984 json!({
985 "type": "userOutcome",
986 "mergeQuestion": { "question": 9, "amount": "2.0" }
987 })
988 );
989 }
990
991 #[rstest]
992 fn test_user_outcome_merge_question_null_amount_means_max() {
993 let action = HyperliquidExecAction::UserOutcome {
994 op: HyperliquidExecUserOutcomeOp::MergeQuestion(HyperliquidExecMergeQuestionParams {
995 question: 9,
996 amount: None,
997 }),
998 };
999 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1000 assert_eq!(
1001 value,
1002 json!({
1003 "type": "userOutcome",
1004 "mergeQuestion": { "question": 9, "amount": null }
1005 })
1006 );
1007 }
1008
1009 #[rstest]
1010 fn test_user_outcome_negate_outcome_serialization() {
1011 let action = HyperliquidExecAction::UserOutcome {
1012 op: HyperliquidExecUserOutcomeOp::NegateOutcome(HyperliquidExecNegateOutcomeParams {
1013 question: 9,
1014 outcome: 52,
1015 amount: dec!(1.5),
1016 }),
1017 };
1018 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1019 assert_eq!(
1020 value,
1021 json!({
1022 "type": "userOutcome",
1023 "negateOutcome": { "question": 9, "outcome": 52, "amount": "1.5" }
1024 })
1025 );
1026 }
1027}
1028
1029#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1033pub enum HyperliquidExecTif {
1034 #[serde(rename = "Alo")]
1036 Alo,
1037 #[serde(rename = "Ioc")]
1039 Ioc,
1040 #[serde(rename = "Gtc")]
1042 Gtc,
1043}
1044
1045#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1047pub enum HyperliquidExecTpSl {
1048 #[serde(rename = "tp")]
1050 Tp,
1051 #[serde(rename = "sl")]
1053 Sl,
1054}
1055
1056#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1058pub enum HyperliquidExecGrouping {
1059 #[serde(rename = "na")]
1061 #[default]
1062 Na,
1063 #[serde(rename = "normalTpsl")]
1065 NormalTpsl,
1066 #[serde(rename = "positionTpsl")]
1068 PositionTpsl,
1069}
1070
1071#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1073#[serde(untagged)]
1074pub enum HyperliquidExecOrderKind {
1075 Limit {
1077 limit: HyperliquidExecLimitParams,
1079 },
1080 Trigger {
1082 trigger: HyperliquidExecTriggerParams,
1084 },
1085}
1086
1087#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1089pub struct HyperliquidExecLimitParams {
1090 pub tif: HyperliquidExecTif,
1092}
1093
1094#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1096#[serde(rename_all = "camelCase")]
1097pub struct HyperliquidExecTriggerParams {
1098 pub is_market: bool,
1100 #[serde(
1102 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1103 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1104 )]
1105 pub trigger_px: Decimal,
1106 pub tpsl: HyperliquidExecTpSl,
1108}
1109
1110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1115pub struct HyperliquidExecBuilderFee {
1116 #[serde(rename = "b")]
1118 pub address: String,
1119 #[serde(rename = "f")]
1121 pub fee_tenths_bp: u32,
1122}
1123
1124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1129pub struct HyperliquidExecPlaceOrderRequest {
1130 #[serde(rename = "a")]
1132 pub asset: AssetId,
1133 #[serde(rename = "b")]
1135 pub is_buy: bool,
1136 #[serde(
1138 rename = "p",
1139 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1140 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1141 )]
1142 pub price: Decimal,
1143 #[serde(
1145 rename = "s",
1146 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1147 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1148 )]
1149 pub size: Decimal,
1150 #[serde(rename = "r")]
1152 pub reduce_only: bool,
1153 #[serde(rename = "t")]
1155 pub kind: HyperliquidExecOrderKind,
1156 #[serde(rename = "c", skip_serializing_if = "Option::is_none")]
1158 pub cloid: Option<Cloid>,
1159}
1160
1161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1163pub struct HyperliquidExecCancelOrderRequest {
1164 #[serde(rename = "a")]
1166 pub asset: AssetId,
1167 #[serde(rename = "o")]
1169 pub oid: OrderId,
1170}
1171
1172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1177pub struct HyperliquidExecCancelByCloidRequest {
1178 pub asset: AssetId,
1180 pub cloid: Cloid,
1182}
1183
1184#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1189pub struct HyperliquidExecModifyOrderRequest {
1190 pub oid: OrderId,
1192 pub order: HyperliquidExecPlaceOrderRequest,
1194}
1195
1196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1201pub struct HyperliquidExecSplitOutcomeParams {
1202 pub outcome: u32,
1204 #[serde(
1206 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1207 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1208 )]
1209 pub amount: Decimal,
1210}
1211
1212#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1218pub struct HyperliquidExecMergeOutcomeParams {
1219 pub outcome: u32,
1221 #[serde(
1223 default,
1224 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1225 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
1226 )]
1227 pub amount: Option<Decimal>,
1228}
1229
1230#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1236pub struct HyperliquidExecMergeQuestionParams {
1237 pub question: u32,
1239 #[serde(
1241 default,
1242 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1243 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
1244 )]
1245 pub amount: Option<Decimal>,
1246}
1247
1248#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1253pub struct HyperliquidExecNegateOutcomeParams {
1254 pub question: u32,
1256 pub outcome: u32,
1258 #[serde(
1260 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1261 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1262 )]
1263 pub amount: Decimal,
1264}
1265
1266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1273pub enum HyperliquidExecUserOutcomeOp {
1274 #[serde(rename = "splitOutcome")]
1276 SplitOutcome(HyperliquidExecSplitOutcomeParams),
1277 #[serde(rename = "mergeOutcome")]
1280 MergeOutcome(HyperliquidExecMergeOutcomeParams),
1281 #[serde(rename = "mergeQuestion")]
1284 MergeQuestion(HyperliquidExecMergeQuestionParams),
1285 #[serde(rename = "negateOutcome")]
1288 NegateOutcome(HyperliquidExecNegateOutcomeParams),
1289}
1290
1291#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1293pub struct HyperliquidExecTwapRequest {
1294 #[serde(rename = "a")]
1296 pub asset: AssetId,
1297 #[serde(rename = "b")]
1299 pub is_buy: bool,
1300 #[serde(
1302 rename = "s",
1303 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1304 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1305 )]
1306 pub size: Decimal,
1307 #[serde(rename = "m")]
1309 pub duration_ms: u64,
1310}
1311
1312#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1318#[serde(tag = "type")]
1319pub enum HyperliquidExecAction {
1320 #[serde(rename = "order")]
1322 Order {
1323 orders: Vec<HyperliquidExecPlaceOrderRequest>,
1325 #[serde(default)]
1327 grouping: HyperliquidExecGrouping,
1328 #[serde(skip_serializing_if = "Option::is_none")]
1330 builder: Option<HyperliquidExecBuilderFee>,
1331 },
1332
1333 #[serde(rename = "cancel")]
1335 Cancel {
1336 cancels: Vec<HyperliquidExecCancelOrderRequest>,
1338 },
1339
1340 #[serde(rename = "cancelByCloid")]
1342 CancelByCloid {
1343 cancels: Vec<HyperliquidExecCancelByCloidRequest>,
1345 },
1346
1347 #[serde(rename = "modify")]
1349 Modify {
1350 #[serde(flatten)]
1352 modify: HyperliquidExecModifyOrderRequest,
1353 },
1354
1355 #[serde(rename = "batchModify")]
1357 BatchModify {
1358 modifies: Vec<HyperliquidExecModifyOrderRequest>,
1360 },
1361
1362 #[serde(rename = "scheduleCancel")]
1364 ScheduleCancel {
1365 #[serde(skip_serializing_if = "Option::is_none")]
1368 time: Option<u64>,
1369 },
1370
1371 #[serde(rename = "updateLeverage")]
1373 UpdateLeverage {
1374 #[serde(rename = "a")]
1376 asset: AssetId,
1377 #[serde(rename = "isCross")]
1379 is_cross: bool,
1380 #[serde(rename = "leverage")]
1382 leverage: u32,
1383 },
1384
1385 #[serde(rename = "updateIsolatedMargin")]
1387 UpdateIsolatedMargin {
1388 #[serde(rename = "a")]
1390 asset: AssetId,
1391 #[serde(
1393 rename = "delta",
1394 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1395 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1396 )]
1397 delta: Decimal,
1398 },
1399
1400 #[serde(rename = "usdClassTransfer")]
1402 UsdClassTransfer {
1403 from: String,
1405 to: String,
1407 #[serde(
1409 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1410 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1411 )]
1412 amount: Decimal,
1413 },
1414
1415 #[serde(rename = "userOutcome")]
1421 UserOutcome {
1422 #[serde(flatten)]
1424 op: HyperliquidExecUserOutcomeOp,
1425 },
1426
1427 #[serde(rename = "twapPlace")]
1429 TwapPlace {
1430 #[serde(flatten)]
1432 twap: HyperliquidExecTwapRequest,
1433 },
1434
1435 #[serde(rename = "twapCancel")]
1437 TwapCancel {
1438 #[serde(rename = "a")]
1440 asset: AssetId,
1441 #[serde(rename = "t")]
1443 twap_id: u64,
1444 },
1445
1446 #[serde(rename = "noop")]
1448 Noop,
1449}
1450
1451#[derive(Debug, Clone, Serialize)]
1456#[serde(rename_all = "camelCase")]
1457pub struct HyperliquidExecRequest {
1458 pub action: HyperliquidExecAction,
1460 pub nonce: u64,
1462 pub signature: String,
1464 #[serde(skip_serializing_if = "Option::is_none")]
1466 pub vault_address: Option<String>,
1467 #[serde(skip_serializing_if = "Option::is_none")]
1470 pub expires_after: Option<u64>,
1471}
1472
1473#[derive(Debug, Clone, Serialize, Deserialize)]
1475pub struct HyperliquidExecResponse {
1476 pub status: String,
1478 pub response: HyperliquidExecResponseData,
1480}
1481
1482#[derive(Debug, Clone, Serialize, Deserialize)]
1484#[serde(tag = "type")]
1485pub enum HyperliquidExecResponseData {
1486 #[serde(rename = "order")]
1488 Order {
1489 data: HyperliquidExecOrderResponseData,
1491 },
1492 #[serde(rename = "cancel")]
1494 Cancel {
1495 data: HyperliquidExecCancelResponseData,
1497 },
1498 #[serde(rename = "modify")]
1500 Modify {
1501 data: HyperliquidExecModifyResponseData,
1503 },
1504 #[serde(rename = "default")]
1506 Default,
1507 #[serde(other)]
1509 Unknown,
1510}
1511
1512#[derive(Debug, Clone, Serialize, Deserialize)]
1514pub struct HyperliquidExecOrderResponseData {
1515 pub statuses: Vec<HyperliquidExecOrderStatus>,
1517}
1518
1519#[derive(Debug, Clone, Serialize, Deserialize)]
1521pub struct HyperliquidExecCancelResponseData {
1522 pub statuses: Vec<HyperliquidExecCancelStatus>,
1524}
1525
1526#[derive(Debug, Clone, Serialize, Deserialize)]
1528pub struct HyperliquidExecModifyResponseData {
1529 pub statuses: Vec<HyperliquidExecModifyStatus>,
1531}
1532
1533#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1535#[serde(untagged)]
1536pub enum HyperliquidExecOrderStatus {
1537 Resting {
1539 resting: HyperliquidExecRestingInfo,
1541 },
1542 Filled {
1544 filled: HyperliquidExecFilledInfo,
1546 },
1547 Error {
1549 error: String,
1551 },
1552}
1553
1554#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1556pub struct HyperliquidExecRestingInfo {
1557 pub oid: OrderId,
1559}
1560
1561#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1563pub struct HyperliquidExecFilledInfo {
1564 #[serde(
1566 rename = "totalSz",
1567 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1568 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1569 )]
1570 pub total_sz: Decimal,
1571 #[serde(
1573 rename = "avgPx",
1574 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1575 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1576 )]
1577 pub avg_px: Decimal,
1578 pub oid: OrderId,
1580}
1581
1582#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1584#[serde(untagged)]
1585pub enum HyperliquidExecCancelStatus {
1586 Success(String), Error {
1590 error: String,
1592 },
1593}
1594
1595#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1597#[serde(untagged)]
1598pub enum HyperliquidExecModifyStatus {
1599 Success(String), Error {
1603 error: String,
1605 },
1606}
1607
1608#[derive(Debug, Clone, Serialize, Deserialize)]
1611#[serde(rename_all = "camelCase")]
1612pub struct ClearinghouseState {
1613 #[serde(default)]
1615 pub asset_positions: Vec<AssetPosition>,
1616 #[serde(default)]
1618 pub cross_margin_summary: Option<CrossMarginSummary>,
1619 #[serde(
1621 default,
1622 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1623 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
1624 )]
1625 pub withdrawable: Option<Decimal>,
1626 #[serde(default)]
1628 pub time: Option<u64>,
1629}
1630
1631#[derive(Debug, Clone, Serialize, Deserialize)]
1633#[serde(rename_all = "camelCase")]
1634pub struct AssetPosition {
1635 pub position: PositionData,
1637 #[serde(rename = "type")]
1639 pub position_type: HyperliquidPositionType,
1640}
1641
1642#[derive(Debug, Clone, Serialize, Deserialize)]
1644#[serde(rename_all = "camelCase")]
1645pub struct LeverageInfo {
1646 #[serde(rename = "type")]
1647 pub leverage_type: HyperliquidLeverageType,
1648 pub value: u32,
1650}
1651
1652#[derive(Debug, Clone, Serialize, Deserialize)]
1654#[serde(rename_all = "camelCase")]
1655pub struct CumFundingInfo {
1656 #[serde(
1658 rename = "allTime",
1659 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1660 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1661 )]
1662 pub all_time: Decimal,
1663 #[serde(
1665 rename = "sinceOpen",
1666 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1667 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1668 )]
1669 pub since_open: Decimal,
1670 #[serde(
1672 rename = "sinceChange",
1673 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1674 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1675 )]
1676 pub since_change: Decimal,
1677}
1678
1679#[derive(Debug, Clone, Serialize, Deserialize)]
1681#[serde(rename_all = "camelCase")]
1682pub struct PositionData {
1683 pub coin: Ustr,
1685 #[serde(rename = "cumFunding")]
1687 pub cum_funding: CumFundingInfo,
1688 #[serde(
1690 rename = "entryPx",
1691 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1692 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str",
1693 default
1694 )]
1695 pub entry_px: Option<Decimal>,
1696 pub leverage: LeverageInfo,
1698 #[serde(
1700 rename = "liquidationPx",
1701 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1702 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str",
1703 default
1704 )]
1705 pub liquidation_px: Option<Decimal>,
1706 #[serde(
1708 rename = "marginUsed",
1709 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1710 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1711 )]
1712 pub margin_used: Decimal,
1713 #[serde(rename = "maxLeverage", default)]
1715 pub max_leverage: Option<u32>,
1716 #[serde(
1718 rename = "positionValue",
1719 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1720 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1721 )]
1722 pub position_value: Decimal,
1723 #[serde(
1725 rename = "returnOnEquity",
1726 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1727 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1728 )]
1729 pub return_on_equity: Decimal,
1730 #[serde(
1732 rename = "szi",
1733 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1734 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1735 )]
1736 pub szi: Decimal,
1737 #[serde(
1739 rename = "unrealizedPnl",
1740 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1741 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1742 )]
1743 pub unrealized_pnl: Decimal,
1744}
1745
1746#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1752#[serde(rename_all = "camelCase")]
1753pub struct SpotClearinghouseState {
1754 #[serde(default)]
1756 pub balances: Vec<SpotBalance>,
1757}
1758
1759#[derive(Debug, Clone, Serialize, Deserialize)]
1761#[serde(rename_all = "camelCase")]
1762pub struct SpotBalance {
1763 pub coin: Ustr,
1765 #[serde(default)]
1768 pub token: Option<u32>,
1769 #[serde(
1771 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1772 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1773 )]
1774 pub total: Decimal,
1775 #[serde(
1777 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1778 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1779 )]
1780 pub hold: Decimal,
1781 #[serde(
1783 default,
1784 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1785 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
1786 )]
1787 pub entry_ntl: Option<Decimal>,
1788}
1789
1790impl SpotBalance {
1791 #[must_use]
1793 pub fn free(&self) -> Decimal {
1794 (self.total - self.hold).max(Decimal::ZERO)
1795 }
1796
1797 #[must_use]
1799 pub fn avg_entry_px(&self) -> Option<Decimal> {
1800 let entry_ntl = self.entry_ntl?;
1801
1802 if entry_ntl.is_zero() || self.total.is_zero() {
1803 return None;
1804 }
1805
1806 Some(entry_ntl / self.total)
1807 }
1808}
1809
1810#[derive(Debug, Clone, Serialize, Deserialize)]
1812#[serde(rename_all = "camelCase")]
1813pub struct CrossMarginSummary {
1814 #[serde(
1816 rename = "accountValue",
1817 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1818 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1819 )]
1820 pub account_value: Decimal,
1821 #[serde(
1823 rename = "totalNtlPos",
1824 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1825 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1826 )]
1827 pub total_ntl_pos: Decimal,
1828 #[serde(
1830 rename = "totalRawUsd",
1831 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1832 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1833 )]
1834 pub total_raw_usd: Decimal,
1835 #[serde(
1837 rename = "totalMarginUsed",
1838 serialize_with = "crate::common::parse::serialize_decimal_as_str",
1839 deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1840 )]
1841 pub total_margin_used: Decimal,
1842 #[serde(
1844 rename = "withdrawable",
1845 default,
1846 serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1847 deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
1848 )]
1849 pub withdrawable: Option<Decimal>,
1850}