nautilus_hyperliquid/http/
models.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::fmt::Display;
17
18use alloy_primitives::Address;
19use rust_decimal::Decimal;
20use serde::{Deserialize, Deserializer, Serialize, Serializer};
21use ustr::Ustr;
22
23use crate::common::enums::{
24    HyperliquidFillDirection, HyperliquidOrderStatus as HyperliquidOrderStatusEnum,
25    HyperliquidPositionType, HyperliquidSide, HyperliquidTpSl, HyperliquidTrailingOffsetType,
26    HyperliquidTriggerPriceType,
27};
28
29/// Response from candleSnapshot endpoint (returns array directly).
30pub type HyperliquidCandleSnapshot = Vec<HyperliquidCandle>;
31
32/// A 128-bit client order ID represented as a hex string with `0x` prefix.
33#[derive(Clone, PartialEq, Eq, Hash, Debug)]
34pub struct Cloid(pub [u8; 16]);
35
36impl Cloid {
37    /// Creates a new `Cloid` from a hex string.
38    ///
39    /// # Errors
40    ///
41    /// Returns an error if the string is not a valid 128-bit hex with `0x` prefix.
42    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        for i in 0..16 {
54            let byte_str = &without_prefix[i * 2..i * 2 + 2];
55            bytes[i] = u8::from_str_radix(byte_str, 16)
56                .map_err(|_| "Invalid hex character in CLOID".to_string())?;
57        }
58
59        Ok(Self(bytes))
60    }
61
62    /// Converts the CLOID to a hex string with `0x` prefix.
63    pub fn to_hex(&self) -> String {
64        let mut result = String::with_capacity(34);
65        result.push_str("0x");
66        for byte in &self.0 {
67            result.push_str(&format!("{:02x}", byte));
68        }
69        result
70    }
71}
72
73impl Display for Cloid {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        write!(f, "{}", self.to_hex())
76    }
77}
78
79impl Serialize for Cloid {
80    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
81    where
82        S: Serializer,
83    {
84        serializer.serialize_str(&self.to_hex())
85    }
86}
87
88impl<'de> Deserialize<'de> for Cloid {
89    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
90    where
91        D: Deserializer<'de>,
92    {
93        let s = String::deserialize(deserializer)?;
94        Self::from_hex(&s).map_err(serde::de::Error::custom)
95    }
96}
97
98/// Asset ID type for Hyperliquid.
99///
100/// For perpetuals, this is the index in `meta.universe`.
101/// For spot trading, this is `10000 + index` from `spotMeta.universe`.
102pub type AssetId = u32;
103
104/// Order ID assigned by Hyperliquid.
105pub type OrderId = u64;
106
107/// Represents asset information from the meta endpoint.
108#[derive(Debug, Clone, Serialize, Deserialize)]
109#[serde(rename_all = "camelCase")]
110pub struct HyperliquidAssetInfo {
111    /// Asset name (e.g., "BTC").
112    pub name: Ustr,
113    /// Number of decimal places for size.
114    pub sz_decimals: u32,
115    /// Maximum leverage allowed for this asset.
116    #[serde(default)]
117    pub max_leverage: Option<u32>,
118    /// Whether this asset requires isolated margin only.
119    #[serde(default)]
120    pub only_isolated: Option<bool>,
121    /// Whether this asset is delisted/inactive.
122    #[serde(default)]
123    pub is_delisted: Option<bool>,
124}
125
126// -------------------------------------------------------------------------------------------------
127// === Extended Instrument Metadata Models ===
128// -------------------------------------------------------------------------------------------------
129
130/// Complete perpetuals metadata response from `POST /info` with `{ "type": "meta" }`.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct PerpMeta {
134    /// Perpetual assets universe.
135    pub universe: Vec<PerpAsset>,
136    /// Margin tables for leverage tiers.
137    #[serde(default)]
138    pub margin_tables: Vec<(u32, MarginTable)>,
139}
140
141/// A single perpetual asset from the universe.
142#[derive(Debug, Clone, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct PerpAsset {
145    /// Asset name (e.g., "BTC").
146    pub name: String,
147    /// Number of decimal places for size.
148    pub sz_decimals: u32,
149    /// Maximum leverage allowed for this asset.
150    #[serde(default)]
151    pub max_leverage: Option<u32>,
152    /// Whether this asset requires isolated margin only.
153    #[serde(default)]
154    pub only_isolated: Option<bool>,
155    /// Whether this asset is delisted/inactive.
156    #[serde(default)]
157    pub is_delisted: Option<bool>,
158}
159
160/// Margin table with leverage tiers.
161#[derive(Debug, Clone, Serialize, Deserialize)]
162#[serde(rename_all = "camelCase")]
163pub struct MarginTable {
164    /// Description of the margin table.
165    pub description: String,
166    /// Margin tiers for different position sizes.
167    #[serde(default)]
168    pub margin_tiers: Vec<MarginTier>,
169}
170
171/// Individual margin tier.
172#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(rename_all = "camelCase")]
174pub struct MarginTier {
175    /// Lower bound for this tier (as string to preserve precision).
176    pub lower_bound: String,
177    /// Maximum leverage for this tier.
178    pub max_leverage: u32,
179}
180
181/// Complete spot metadata response from `POST /info` with `{ "type": "spotMeta" }`.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub struct SpotMeta {
185    /// Spot tokens available.
186    pub tokens: Vec<SpotToken>,
187    /// Spot pairs universe.
188    pub universe: Vec<SpotPair>,
189}
190
191/// EVM contract information for a spot token.
192#[derive(Debug, Clone, Serialize, Deserialize)]
193#[serde(rename_all = "snake_case")]
194pub struct EvmContract {
195    /// EVM contract address (20 bytes).
196    pub address: Address,
197    /// Extra wei decimals for EVM precision (can be negative).
198    pub evm_extra_wei_decimals: i32,
199}
200
201/// A single spot token from the tokens list.
202#[derive(Debug, Clone, Serialize, Deserialize)]
203#[serde(rename_all = "camelCase")]
204pub struct SpotToken {
205    /// Token name (e.g., "USDC").
206    pub name: String,
207    /// Number of decimal places for size.
208    pub sz_decimals: u32,
209    /// Wei decimals (on-chain precision).
210    pub wei_decimals: u32,
211    /// Token index used for pair references.
212    pub index: u32,
213    /// Token contract ID/address.
214    pub token_id: String,
215    /// Whether this is the canonical token.
216    pub is_canonical: bool,
217    /// Optional EVM contract information.
218    #[serde(default)]
219    pub evm_contract: Option<EvmContract>,
220    /// Optional full name.
221    #[serde(default)]
222    pub full_name: Option<String>,
223    /// Optional deployer trading fee share.
224    #[serde(default)]
225    pub deployer_trading_fee_share: Option<String>,
226}
227
228/// A single spot pair from the universe.
229#[derive(Debug, Clone, Serialize, Deserialize)]
230#[serde(rename_all = "camelCase")]
231pub struct SpotPair {
232    /// Pair display name (e.g., "PURR/USDC").
233    pub name: String,
234    /// Token indices [base_token_index, quote_token_index].
235    pub tokens: [u32; 2],
236    /// Pair index.
237    pub index: u32,
238    /// Whether this is the canonical pair.
239    pub is_canonical: bool,
240}
241
242// -------------------------------------------------------------------------------------------------
243// === Optional Context Payloads (for price precision refinement) ===
244// -------------------------------------------------------------------------------------------------
245
246/// Optional perpetuals metadata with asset contexts from `{ "type": "metaAndAssetCtxs" }`.
247/// Returns a tuple: `[PerpMeta, Vec<PerpAssetCtx>]`
248#[derive(Debug, Clone, Serialize, Deserialize)]
249#[serde(untagged)]
250pub enum PerpMetaAndCtxs {
251    /// Tuple format: [meta, contexts]
252    Payload(Box<(PerpMeta, Vec<PerpAssetCtx>)>),
253}
254
255/// Runtime context for a perpetual asset (mark prices, funding, etc).
256#[derive(Debug, Clone, Serialize, Deserialize)]
257#[serde(rename_all = "camelCase")]
258pub struct PerpAssetCtx {
259    /// Mark price as string.
260    #[serde(default)]
261    pub mark_px: Option<String>,
262    /// Mid price as string.
263    #[serde(default)]
264    pub mid_px: Option<String>,
265    /// Funding rate as string.
266    #[serde(default)]
267    pub funding: Option<String>,
268    /// Open interest as string.
269    #[serde(default)]
270    pub open_interest: Option<String>,
271}
272
273/// Optional spot metadata with asset contexts from `{ "type": "spotMetaAndAssetCtxs" }`.
274/// Returns a tuple: `[SpotMeta, Vec<SpotAssetCtx>]`
275#[derive(Debug, Clone, Serialize, Deserialize)]
276#[serde(untagged)]
277pub enum SpotMetaAndCtxs {
278    /// Tuple format: [meta, contexts]
279    Payload(Box<(SpotMeta, Vec<SpotAssetCtx>)>),
280}
281
282/// Runtime context for a spot pair (prices, volumes, etc).
283#[derive(Debug, Clone, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285pub struct SpotAssetCtx {
286    /// Mark price as string.
287    #[serde(default)]
288    pub mark_px: Option<String>,
289    /// Mid price as string.
290    #[serde(default)]
291    pub mid_px: Option<String>,
292    /// 24h volume as string.
293    #[serde(default)]
294    pub day_volume: Option<String>,
295}
296
297/// Represents an L2 order book snapshot from `POST /info`.
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct HyperliquidL2Book {
300    /// Coin symbol.
301    pub coin: Ustr,
302    /// Order book levels: [bids, asks].
303    pub levels: Vec<Vec<HyperliquidLevel>>,
304    /// Timestamp in milliseconds.
305    pub time: u64,
306}
307
308/// Represents an order book level with price and size.
309#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct HyperliquidLevel {
311    /// Price level.
312    pub px: String,
313    /// Size at this level.
314    pub sz: String,
315}
316
317/// Represents user fills response from `POST /info`.
318///
319/// The Hyperliquid API returns fills directly as an array, not wrapped in an object.
320pub type HyperliquidFills = Vec<HyperliquidFill>;
321
322/// Represents metadata about available markets from `POST /info`.
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct HyperliquidMeta {
325    #[serde(default)]
326    pub universe: Vec<HyperliquidAssetInfo>,
327}
328
329/// Represents a single candle (OHLCV bar) from Hyperliquid.
330#[derive(Debug, Clone, Serialize, Deserialize)]
331#[serde(rename_all = "camelCase")]
332pub struct HyperliquidCandle {
333    /// Candle start timestamp in milliseconds.
334    #[serde(rename = "t")]
335    pub timestamp: u64,
336    /// Candle end timestamp in milliseconds.
337    #[serde(rename = "T")]
338    pub end_timestamp: u64,
339    /// Open price.
340    #[serde(rename = "o")]
341    pub open: String,
342    /// High price.
343    #[serde(rename = "h")]
344    pub high: String,
345    /// Low price.
346    #[serde(rename = "l")]
347    pub low: String,
348    /// Close price.
349    #[serde(rename = "c")]
350    pub close: String,
351    /// Volume.
352    #[serde(rename = "v")]
353    pub volume: String,
354    /// Number of trades (optional).
355    #[serde(rename = "n", default)]
356    pub num_trades: Option<u64>,
357}
358
359/// Represents an individual fill from user fills.
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct HyperliquidFill {
362    /// Coin symbol.
363    pub coin: Ustr,
364    /// Fill price.
365    pub px: String,
366    /// Fill size.
367    pub sz: String,
368    /// Order side (buy/sell).
369    pub side: HyperliquidSide,
370    /// Fill timestamp in milliseconds.
371    pub time: u64,
372    /// Position size before this fill.
373    #[serde(rename = "startPosition")]
374    pub start_position: String,
375    /// Fill direction (open/close).
376    pub dir: HyperliquidFillDirection,
377    /// Closed P&L from this fill.
378    #[serde(rename = "closedPnl")]
379    pub closed_pnl: String,
380    /// Hash reference.
381    pub hash: String,
382    /// Order ID that generated this fill.
383    pub oid: u64,
384    /// Crossed status.
385    pub crossed: bool,
386    /// Fee paid for this fill.
387    pub fee: String,
388}
389
390/// Represents order status response from `POST /info`.
391#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct HyperliquidOrderStatus {
393    #[serde(default)]
394    pub statuses: Vec<HyperliquidOrderStatusEntry>,
395}
396
397/// Represents an individual order status entry.
398#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct HyperliquidOrderStatusEntry {
400    /// Order information.
401    pub order: HyperliquidOrderInfo,
402    /// Current status.
403    pub status: HyperliquidOrderStatusEnum,
404    /// Status timestamp in milliseconds.
405    #[serde(rename = "statusTimestamp")]
406    pub status_timestamp: u64,
407}
408
409/// Represents order information within an order status entry.
410#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct HyperliquidOrderInfo {
412    /// Coin symbol.
413    pub coin: Ustr,
414    /// Order side (buy/sell).
415    pub side: HyperliquidSide,
416    /// Limit price.
417    #[serde(rename = "limitPx")]
418    pub limit_px: String,
419    /// Order size.
420    pub sz: String,
421    /// Order ID.
422    pub oid: u64,
423    /// Order timestamp in milliseconds.
424    pub timestamp: u64,
425    /// Original order size.
426    #[serde(rename = "origSz")]
427    pub orig_sz: String,
428}
429
430/// ECC signature components for Hyperliquid exchange requests.
431#[derive(Debug, Clone, Serialize)]
432pub struct HyperliquidSignature {
433    /// R component of the signature.
434    pub r: String,
435    /// S component of the signature.
436    pub s: String,
437    /// V component (recovery ID) of the signature.
438    pub v: u64,
439}
440
441impl HyperliquidSignature {
442    /// Parse a hex signature string (0x + 64 hex r + 64 hex s + 2 hex v) into components.
443    pub fn from_hex(sig_hex: &str) -> Result<Self, String> {
444        let sig_hex = sig_hex.strip_prefix("0x").unwrap_or(sig_hex);
445
446        if sig_hex.len() != 130 {
447            return Err(format!(
448                "Invalid signature length: expected 130 hex chars, was {}",
449                sig_hex.len()
450            ));
451        }
452
453        let r = format!("0x{}", &sig_hex[0..64]);
454        let s = format!("0x{}", &sig_hex[64..128]);
455        let v = u64::from_str_radix(&sig_hex[128..130], 16)
456            .map_err(|e| format!("Failed to parse v component: {e}"))?;
457
458        Ok(Self { r, s, v })
459    }
460}
461
462/// Represents an exchange action request wrapper for `POST /exchange`.
463#[derive(Debug, Clone, Serialize)]
464pub struct HyperliquidExchangeRequest<T> {
465    /// The action to perform.
466    #[serde(rename = "action")]
467    pub action: T,
468    /// Request nonce for replay protection.
469    #[serde(rename = "nonce")]
470    pub nonce: u64,
471    /// ECC signature over the action.
472    #[serde(rename = "signature")]
473    pub signature: HyperliquidSignature,
474    /// Optional vault address for sub-account trading.
475    #[serde(rename = "vaultAddress", skip_serializing_if = "Option::is_none")]
476    pub vault_address: Option<String>,
477    /// Optional expiration time in milliseconds.
478    #[serde(rename = "expiresAfter", skip_serializing_if = "Option::is_none")]
479    pub expires_after: Option<u64>,
480}
481
482impl<T> HyperliquidExchangeRequest<T>
483where
484    T: Serialize,
485{
486    /// Create a new exchange request with the given action.
487    pub fn new(action: T, nonce: u64, signature: String) -> Result<Self, String> {
488        Ok(Self {
489            action,
490            nonce,
491            signature: HyperliquidSignature::from_hex(&signature)?,
492            vault_address: None,
493            expires_after: None,
494        })
495    }
496
497    /// Create a new exchange request with vault address for sub-account trading.
498    pub fn with_vault(
499        action: T,
500        nonce: u64,
501        signature: String,
502        vault_address: String,
503    ) -> Result<Self, String> {
504        Ok(Self {
505            action,
506            nonce,
507            signature: HyperliquidSignature::from_hex(&signature)?,
508            vault_address: Some(vault_address),
509            expires_after: None,
510        })
511    }
512
513    /// Convert to JSON value for signing purposes.
514    pub fn to_sign_value(&self) -> serde_json::Result<serde_json::Value> {
515        serde_json::to_value(self)
516    }
517}
518
519/// Represents an exchange response wrapper from `POST /exchange`.
520#[derive(Debug, Clone, Serialize, Deserialize)]
521#[serde(untagged)]
522pub enum HyperliquidExchangeResponse {
523    /// Successful response with status.
524    Status {
525        /// Status message.
526        status: String,
527        /// Response payload.
528        response: serde_json::Value,
529    },
530    /// Error response.
531    Error {
532        /// Error message.
533        error: String,
534    },
535}
536
537////////////////////////////////////////////////////////////////////////////////
538// Conditional Order Models
539////////////////////////////////////////////////////////////////////////////////
540
541/// Extended trigger order parameters for advanced conditional orders.
542#[derive(Debug, Clone, Serialize, Deserialize)]
543#[serde(rename_all = "camelCase")]
544pub struct HyperliquidTriggerOrderParams {
545    /// Whether this is a market order when triggered (true) or limit order (false).
546    #[serde(rename = "isMarket")]
547    pub is_market: bool,
548    /// Trigger price.
549    #[serde(rename = "triggerPx")]
550    pub trigger_px: String,
551    /// Take profit or stop loss type.
552    pub tpsl: HyperliquidTpSl,
553    /// Optional trigger price type (last, mark, oracle). Defaults to mark price if not specified.
554    #[serde(rename = "triggerPxType", skip_serializing_if = "Option::is_none")]
555    pub trigger_px_type: Option<HyperliquidTriggerPriceType>,
556}
557
558/// Trailing stop order parameters.
559#[derive(Debug, Clone, Serialize, Deserialize)]
560#[serde(rename_all = "camelCase")]
561pub struct HyperliquidTrailingStopParams {
562    /// Trailing offset value.
563    #[serde(
564        rename = "trailingOffset",
565        serialize_with = "crate::common::parse::serialize_decimal_as_str",
566        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
567    )]
568    pub trailing_offset: Decimal,
569    /// Trailing offset type (price, percentage, basis_points).
570    #[serde(rename = "trailingOffsetType")]
571    pub trailing_offset_type: HyperliquidTrailingOffsetType,
572    /// Optional activation price - price at which the trailing stop becomes active.
573    #[serde(rename = "activationPx", skip_serializing_if = "Option::is_none")]
574    pub activation_px: Option<String>,
575    /// Take profit or stop loss type.
576    pub tpsl: HyperliquidTpSl,
577}
578
579/// Request to place a trigger order (stop or take profit).
580#[derive(Debug, Clone, Serialize, Deserialize)]
581#[serde(rename_all = "camelCase")]
582pub struct HyperliquidPlaceTriggerOrderRequest {
583    /// Asset ID.
584    #[serde(rename = "a")]
585    pub asset: AssetId,
586    /// Whether to buy or sell.
587    #[serde(rename = "b")]
588    pub is_buy: bool,
589    /// Order size.
590    #[serde(
591        rename = "s",
592        serialize_with = "crate::common::parse::serialize_decimal_as_str",
593        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
594    )]
595    pub sz: Decimal,
596    /// Limit price (required if is_market is false).
597    #[serde(rename = "limitPx", skip_serializing_if = "Option::is_none")]
598    pub limit_px: Option<String>,
599    /// Trigger order parameters.
600    #[serde(flatten)]
601    pub trigger_params: HyperliquidTriggerOrderParams,
602    /// Whether this is a reduce-only order.
603    #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
604    pub reduce_only: Option<bool>,
605    /// Optional client order ID for tracking.
606    #[serde(skip_serializing_if = "Option::is_none")]
607    pub cloid: Option<Cloid>,
608}
609
610/// Request to modify an existing trigger order.
611#[derive(Debug, Clone, Serialize, Deserialize)]
612#[serde(rename_all = "camelCase")]
613pub struct HyperliquidModifyTriggerOrderRequest {
614    /// Order ID to modify.
615    pub oid: OrderId,
616    /// Asset ID.
617    #[serde(rename = "a")]
618    pub asset: AssetId,
619    /// New trigger price.
620    #[serde(rename = "triggerPx")]
621    pub trigger_px: String,
622    /// New limit price (if applicable).
623    #[serde(rename = "limitPx", skip_serializing_if = "Option::is_none")]
624    pub limit_px: Option<String>,
625    /// New order size (if changing).
626    #[serde(
627        skip_serializing_if = "Option::is_none",
628        serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
629        deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
630    )]
631    pub sz: Option<Decimal>,
632}
633
634/// Request to cancel a trigger order.
635#[derive(Debug, Clone, Serialize, Deserialize)]
636#[serde(rename_all = "camelCase")]
637pub struct HyperliquidCancelTriggerOrderRequest {
638    /// Asset ID.
639    #[serde(rename = "a")]
640    pub asset: AssetId,
641    /// Order ID to cancel.
642    pub oid: OrderId,
643}
644
645/// Trigger order status response.
646#[derive(Debug, Clone, Serialize, Deserialize)]
647#[serde(rename_all = "camelCase")]
648pub struct HyperliquidTriggerOrderStatus {
649    /// Order ID.
650    pub oid: OrderId,
651    /// Order status.
652    pub status: HyperliquidOrderStatusEnum,
653    /// Timestamp when status was updated (milliseconds).
654    #[serde(rename = "statusTimestamp")]
655    pub status_timestamp: u64,
656    /// Trigger order information.
657    pub order: HyperliquidTriggerOrderInfo,
658}
659
660/// Information about a trigger order.
661#[derive(Debug, Clone, Serialize, Deserialize)]
662#[serde(rename_all = "camelCase")]
663pub struct HyperliquidTriggerOrderInfo {
664    /// Asset symbol.
665    pub coin: Ustr,
666    /// Order side.
667    pub side: HyperliquidSide,
668    /// Limit price (if limit order).
669    #[serde(rename = "limitPx", skip_serializing_if = "Option::is_none")]
670    pub limit_px: Option<String>,
671    /// Trigger price.
672    #[serde(rename = "triggerPx")]
673    pub trigger_px: String,
674    /// Order size.
675    pub sz: String,
676    /// Whether this is a market order when triggered.
677    #[serde(rename = "isMarket")]
678    pub is_market: bool,
679    /// Take profit or stop loss type.
680    pub tpsl: HyperliquidTpSl,
681    /// Order ID.
682    pub oid: OrderId,
683    /// Order creation timestamp (milliseconds).
684    pub timestamp: u64,
685    /// Whether the order has been triggered.
686    #[serde(default)]
687    pub triggered: bool,
688    /// Trigger timestamp (milliseconds, if triggered).
689    #[serde(rename = "triggerTime", skip_serializing_if = "Option::is_none")]
690    pub trigger_time: Option<u64>,
691}
692
693/// Bracket order request (entry + TP + SL).
694#[derive(Debug, Clone, Serialize, Deserialize)]
695#[serde(rename_all = "camelCase")]
696pub struct HyperliquidBracketOrderRequest {
697    /// Asset ID.
698    #[serde(rename = "a")]
699    pub asset: AssetId,
700    /// Whether to buy or sell.
701    #[serde(rename = "b")]
702    pub is_buy: bool,
703    /// Entry order size.
704    #[serde(
705        rename = "s",
706        serialize_with = "crate::common::parse::serialize_decimal_as_str",
707        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
708    )]
709    pub sz: Decimal,
710    /// Entry order limit price.
711    #[serde(rename = "limitPx")]
712    pub limit_px: String,
713    /// Take profit trigger price.
714    #[serde(rename = "tpTriggerPx")]
715    pub tp_trigger_px: String,
716    /// Take profit limit price (if limit order).
717    #[serde(rename = "tpLimitPx", skip_serializing_if = "Option::is_none")]
718    pub tp_limit_px: Option<String>,
719    /// Whether TP is market order.
720    #[serde(rename = "tpIsMarket", default)]
721    pub tp_is_market: bool,
722    /// Stop loss trigger price.
723    #[serde(rename = "slTriggerPx")]
724    pub sl_trigger_px: String,
725    /// Stop loss limit price (if limit order).
726    #[serde(rename = "slLimitPx", skip_serializing_if = "Option::is_none")]
727    pub sl_limit_px: Option<String>,
728    /// Whether SL is market order.
729    #[serde(rename = "slIsMarket", default)]
730    pub sl_is_market: bool,
731    /// Optional client order ID for entry order.
732    #[serde(skip_serializing_if = "Option::is_none")]
733    pub cloid: Option<Cloid>,
734}
735
736/// OCO (One-Cancels-Other) order request.
737#[derive(Debug, Clone, Serialize, Deserialize)]
738#[serde(rename_all = "camelCase")]
739pub struct HyperliquidOcoOrderRequest {
740    /// Asset ID.
741    #[serde(rename = "a")]
742    pub asset: AssetId,
743    /// Whether to buy or sell.
744    #[serde(rename = "b")]
745    pub is_buy: bool,
746    /// Order size.
747    #[serde(
748        rename = "s",
749        serialize_with = "crate::common::parse::serialize_decimal_as_str",
750        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
751    )]
752    pub sz: Decimal,
753    /// First order trigger price.
754    #[serde(rename = "triggerPx1")]
755    pub trigger_px_1: String,
756    /// First order limit price (if applicable).
757    #[serde(rename = "limitPx1", skip_serializing_if = "Option::is_none")]
758    pub limit_px_1: Option<String>,
759    /// Whether first order is market.
760    #[serde(rename = "isMarket1", default)]
761    pub is_market_1: bool,
762    /// First order TP/SL type.
763    #[serde(rename = "tpsl1")]
764    pub tpsl_1: HyperliquidTpSl,
765    /// Second order trigger price.
766    #[serde(rename = "triggerPx2")]
767    pub trigger_px_2: String,
768    /// Second order limit price (if applicable).
769    #[serde(rename = "limitPx2", skip_serializing_if = "Option::is_none")]
770    pub limit_px_2: Option<String>,
771    /// Whether second order is market.
772    #[serde(rename = "isMarket2", default)]
773    pub is_market_2: bool,
774    /// Second order TP/SL type.
775    #[serde(rename = "tpsl2")]
776    pub tpsl_2: HyperliquidTpSl,
777    /// Whether orders are reduce-only.
778    #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
779    pub reduce_only: Option<bool>,
780}
781
782////////////////////////////////////////////////////////////////////////////////
783// Tests
784////////////////////////////////////////////////////////////////////////////////
785
786#[cfg(test)]
787mod tests {
788    use rstest::rstest;
789
790    use super::*;
791
792    #[rstest]
793    fn test_meta_deserialization() {
794        let json = r#"{"universe": [{"name": "BTC", "szDecimals": 5}]}"#;
795
796        let meta: HyperliquidMeta = serde_json::from_str(json).unwrap();
797
798        assert_eq!(meta.universe.len(), 1);
799        assert_eq!(meta.universe[0].name, "BTC");
800        assert_eq!(meta.universe[0].sz_decimals, 5);
801    }
802
803    #[rstest]
804    fn test_l2_book_deserialization() {
805        let json = r#"{"coin": "BTC", "levels": [[{"px": "50000", "sz": "1.5"}], [{"px": "50100", "sz": "2.0"}]], "time": 1234567890}"#;
806
807        let book: HyperliquidL2Book = serde_json::from_str(json).unwrap();
808
809        assert_eq!(book.coin, "BTC");
810        assert_eq!(book.levels.len(), 2);
811        assert_eq!(book.time, 1234567890);
812    }
813
814    #[rstest]
815    fn test_exchange_response_deserialization() {
816        let json = r#"{"status": "ok", "response": {"type": "order"}}"#;
817
818        let response: HyperliquidExchangeResponse = serde_json::from_str(json).unwrap();
819
820        match response {
821            HyperliquidExchangeResponse::Status { status, .. } => assert_eq!(status, "ok"),
822            _ => panic!("Expected status response"),
823        }
824    }
825}
826
827/// Time-in-force for limit orders in exchange endpoint.
828///
829/// These values must match exactly what Hyperliquid expects for proper serialization.
830#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
831pub enum HyperliquidExecTif {
832    /// Add Liquidity Only (post-only order).
833    #[serde(rename = "Alo")]
834    Alo,
835    /// Immediate or Cancel.
836    #[serde(rename = "Ioc")]
837    Ioc,
838    /// Good Till Canceled.
839    #[serde(rename = "Gtc")]
840    Gtc,
841}
842
843/// Take profit or stop loss side for trigger orders in exchange endpoint.
844#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
845pub enum HyperliquidExecTpSl {
846    /// Take profit.
847    #[serde(rename = "tp")]
848    Tp,
849    /// Stop loss.
850    #[serde(rename = "sl")]
851    Sl,
852}
853
854/// Order grouping strategy for linked TP/SL orders in exchange endpoint.
855#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
856pub enum HyperliquidExecGrouping {
857    /// No grouping semantics.
858    #[serde(rename = "na")]
859    #[default]
860    Na,
861    /// Normal TP/SL grouping (linked orders).
862    #[serde(rename = "normalTpsl")]
863    NormalTpsl,
864    /// Position-level TP/SL grouping.
865    #[serde(rename = "positionTpsl")]
866    PositionTpsl,
867}
868
869/// Order kind specification for the `t` field in exchange endpoint order requests.
870#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
871#[serde(untagged)]
872pub enum HyperliquidExecOrderKind {
873    /// Limit order with time-in-force.
874    Limit {
875        /// Limit order parameters.
876        limit: HyperliquidExecLimitParams,
877    },
878    /// Trigger order (stop/take profit).
879    Trigger {
880        /// Trigger order parameters.
881        trigger: HyperliquidExecTriggerParams,
882    },
883}
884
885/// Parameters for limit orders in exchange endpoint.
886#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
887pub struct HyperliquidExecLimitParams {
888    /// Time-in-force for the limit order.
889    pub tif: HyperliquidExecTif,
890}
891
892/// Parameters for trigger orders (stop/take profit) in exchange endpoint.
893#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
894#[serde(rename_all = "camelCase")]
895pub struct HyperliquidExecTriggerParams {
896    /// Whether to use market price when triggered.
897    pub is_market: bool,
898    /// Trigger price as a string.
899    #[serde(
900        serialize_with = "crate::common::parse::serialize_decimal_as_str",
901        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
902    )]
903    pub trigger_px: Decimal,
904    /// Whether this is a take profit or stop loss.
905    pub tpsl: HyperliquidExecTpSl,
906}
907
908/// Optional builder fee for orders in exchange endpoint.
909///
910/// The builder fee is specified in tenths of a basis point.
911/// For example, `f: 10` represents 1 basis point (0.01%).
912#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
913pub struct HyperliquidExecBuilderFee {
914    /// Builder address to receive the fee.
915    #[serde(rename = "b")]
916    pub address: String,
917    /// Fee in tenths of a basis point.
918    #[serde(rename = "f")]
919    pub fee_tenths_bp: u32,
920}
921
922/// Order specification for placing orders via exchange endpoint.
923///
924/// This struct represents a single order in the exact format expected
925/// by the Hyperliquid exchange endpoint.
926#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
927pub struct HyperliquidExecPlaceOrderRequest {
928    /// Asset ID.
929    #[serde(rename = "a")]
930    pub asset: AssetId,
931    /// Is buy order (true for buy, false for sell).
932    #[serde(rename = "b")]
933    pub is_buy: bool,
934    /// Price as a string with no trailing zeros.
935    #[serde(
936        rename = "p",
937        serialize_with = "crate::common::parse::serialize_decimal_as_str",
938        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
939    )]
940    pub price: Decimal,
941    /// Size as a string with no trailing zeros.
942    #[serde(
943        rename = "s",
944        serialize_with = "crate::common::parse::serialize_decimal_as_str",
945        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
946    )]
947    pub size: Decimal,
948    /// Reduce-only flag.
949    #[serde(rename = "r")]
950    pub reduce_only: bool,
951    /// Order type (limit or trigger).
952    #[serde(rename = "t")]
953    pub kind: HyperliquidExecOrderKind,
954    /// Optional client order ID (128-bit hex).
955    #[serde(rename = "c", skip_serializing_if = "Option::is_none")]
956    pub cloid: Option<Cloid>,
957}
958
959/// Cancel specification for canceling orders by order ID via exchange endpoint.
960#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
961pub struct HyperliquidExecCancelOrderRequest {
962    /// Asset ID.
963    #[serde(rename = "a")]
964    pub asset: AssetId,
965    /// Order ID to cancel.
966    #[serde(rename = "o")]
967    pub oid: OrderId,
968}
969
970/// Cancel specification for canceling orders by client order ID via exchange endpoint.
971#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
972pub struct HyperliquidExecCancelByCloidRequest {
973    /// Asset ID.
974    #[serde(rename = "a")]
975    pub asset: AssetId,
976    /// Client order ID to cancel.
977    #[serde(rename = "c")]
978    pub cloid: Cloid,
979}
980
981/// Modify specification for modifying existing orders via exchange endpoint.
982#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
983pub struct HyperliquidExecModifyOrderRequest {
984    /// Asset ID.
985    #[serde(rename = "a")]
986    pub asset: AssetId,
987    /// Order ID to modify.
988    #[serde(rename = "o")]
989    pub oid: OrderId,
990    /// New price (optional).
991    #[serde(
992        rename = "p",
993        skip_serializing_if = "Option::is_none",
994        serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
995        deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
996    )]
997    pub price: Option<Decimal>,
998    /// New size (optional).
999    #[serde(
1000        rename = "s",
1001        skip_serializing_if = "Option::is_none",
1002        serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1003        deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
1004    )]
1005    pub size: Option<Decimal>,
1006    /// New reduce-only flag (optional).
1007    #[serde(rename = "r", skip_serializing_if = "Option::is_none")]
1008    pub reduce_only: Option<bool>,
1009    /// New order type (optional).
1010    #[serde(rename = "t", skip_serializing_if = "Option::is_none")]
1011    pub kind: Option<HyperliquidExecOrderKind>,
1012}
1013
1014/// TWAP (Time-Weighted Average Price) order specification for exchange endpoint.
1015#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1016pub struct HyperliquidExecTwapRequest {
1017    /// Asset ID.
1018    #[serde(rename = "a")]
1019    pub asset: AssetId,
1020    /// Is buy order.
1021    #[serde(rename = "b")]
1022    pub is_buy: bool,
1023    /// Total size to execute.
1024    #[serde(
1025        rename = "s",
1026        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1027        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1028    )]
1029    pub size: Decimal,
1030    /// Duration in milliseconds.
1031    #[serde(rename = "m")]
1032    pub duration_ms: u64,
1033}
1034
1035/// All possible exchange actions for the Hyperliquid `/exchange` endpoint.
1036///
1037/// Each variant corresponds to a specific action type that can be performed
1038/// through the exchange API. The serialization uses the exact action type
1039/// names expected by Hyperliquid.
1040#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1041#[serde(tag = "type")]
1042pub enum HyperliquidExecAction {
1043    /// Place one or more orders.
1044    #[serde(rename = "order")]
1045    Order {
1046        /// List of orders to place.
1047        orders: Vec<HyperliquidExecPlaceOrderRequest>,
1048        /// Grouping strategy for TP/SL orders.
1049        #[serde(default)]
1050        grouping: HyperliquidExecGrouping,
1051        /// Optional builder fee.
1052        #[serde(skip_serializing_if = "Option::is_none")]
1053        builder: Option<HyperliquidExecBuilderFee>,
1054    },
1055
1056    /// Cancel orders by order ID.
1057    #[serde(rename = "cancel")]
1058    Cancel {
1059        /// Orders to cancel.
1060        cancels: Vec<HyperliquidExecCancelOrderRequest>,
1061    },
1062
1063    /// Cancel orders by client order ID.
1064    #[serde(rename = "cancelByCloid")]
1065    CancelByCloid {
1066        /// Orders to cancel by CLOID.
1067        cancels: Vec<HyperliquidExecCancelByCloidRequest>,
1068    },
1069
1070    /// Modify a single order.
1071    #[serde(rename = "modify")]
1072    Modify {
1073        /// Order modification specification.
1074        #[serde(flatten)]
1075        modify: HyperliquidExecModifyOrderRequest,
1076    },
1077
1078    /// Modify multiple orders atomically.
1079    #[serde(rename = "batchModify")]
1080    BatchModify {
1081        /// Multiple order modifications.
1082        modifies: Vec<HyperliquidExecModifyOrderRequest>,
1083    },
1084
1085    /// Schedule automatic order cancellation (dead man's switch).
1086    #[serde(rename = "scheduleCancel")]
1087    ScheduleCancel {
1088        /// Time in milliseconds when orders should be cancelled.
1089        /// If None, clears the existing schedule.
1090        #[serde(skip_serializing_if = "Option::is_none")]
1091        time: Option<u64>,
1092    },
1093
1094    /// Update leverage for a position.
1095    #[serde(rename = "updateLeverage")]
1096    UpdateLeverage {
1097        /// Asset ID.
1098        #[serde(rename = "a")]
1099        asset: AssetId,
1100        /// Whether to use cross margin.
1101        #[serde(rename = "isCross")]
1102        is_cross: bool,
1103        /// Leverage value.
1104        #[serde(rename = "leverage")]
1105        leverage: u32,
1106    },
1107
1108    /// Update isolated margin for a position.
1109    #[serde(rename = "updateIsolatedMargin")]
1110    UpdateIsolatedMargin {
1111        /// Asset ID.
1112        #[serde(rename = "a")]
1113        asset: AssetId,
1114        /// Margin delta as a string.
1115        #[serde(
1116            rename = "delta",
1117            serialize_with = "crate::common::parse::serialize_decimal_as_str",
1118            deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1119        )]
1120        delta: Decimal,
1121    },
1122
1123    /// Transfer USD between spot and perp accounts.
1124    #[serde(rename = "usdClassTransfer")]
1125    UsdClassTransfer {
1126        /// Source account type.
1127        from: String,
1128        /// Destination account type.
1129        to: String,
1130        /// Amount to transfer.
1131        #[serde(
1132            serialize_with = "crate::common::parse::serialize_decimal_as_str",
1133            deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1134        )]
1135        amount: Decimal,
1136    },
1137
1138    /// Place a TWAP order.
1139    #[serde(rename = "twapPlace")]
1140    TwapPlace {
1141        /// TWAP order specification.
1142        #[serde(flatten)]
1143        twap: HyperliquidExecTwapRequest,
1144    },
1145
1146    /// Cancel a TWAP order.
1147    #[serde(rename = "twapCancel")]
1148    TwapCancel {
1149        /// Asset ID.
1150        #[serde(rename = "a")]
1151        asset: AssetId,
1152        /// TWAP ID.
1153        #[serde(rename = "t")]
1154        twap_id: u64,
1155    },
1156
1157    /// No-operation to invalidate pending nonces.
1158    #[serde(rename = "noop")]
1159    Noop,
1160}
1161
1162/// Exchange request envelope for the `/exchange` endpoint.
1163///
1164/// This is the top-level structure sent to Hyperliquid's exchange endpoint.
1165/// It includes the action to perform along with authentication and metadata.
1166#[derive(Debug, Clone, Serialize)]
1167#[serde(rename_all = "camelCase")]
1168pub struct HyperliquidExecRequest {
1169    /// The exchange action to perform.
1170    pub action: HyperliquidExecAction,
1171    /// Request nonce for replay protection (milliseconds timestamp recommended).
1172    pub nonce: u64,
1173    /// ECC signature over the action and nonce.
1174    pub signature: String,
1175    /// Optional vault address for sub-account trading.
1176    #[serde(skip_serializing_if = "Option::is_none")]
1177    pub vault_address: Option<String>,
1178    /// Optional expiration time in milliseconds.
1179    /// Note: Using this field increases rate limit weight by 5x if the request expires.
1180    #[serde(skip_serializing_if = "Option::is_none")]
1181    pub expires_after: Option<u64>,
1182}
1183
1184/// Exchange response envelope from the `/exchange` endpoint.
1185#[derive(Debug, Clone, Serialize, Deserialize)]
1186pub struct HyperliquidExecResponse {
1187    /// Response status ("ok" for success).
1188    pub status: String,
1189    /// Response payload.
1190    pub response: HyperliquidExecResponseData,
1191}
1192
1193/// Response data containing the actual response payload from exchange endpoint.
1194#[derive(Debug, Clone, Serialize, Deserialize)]
1195#[serde(tag = "type")]
1196pub enum HyperliquidExecResponseData {
1197    /// Response for order actions.
1198    #[serde(rename = "order")]
1199    Order {
1200        /// Order response data.
1201        data: HyperliquidExecOrderResponseData,
1202    },
1203    /// Response for cancel actions.
1204    #[serde(rename = "cancel")]
1205    Cancel {
1206        /// Cancel response data.
1207        data: HyperliquidExecCancelResponseData,
1208    },
1209    /// Response for modify actions.
1210    #[serde(rename = "modify")]
1211    Modify {
1212        /// Modify response data.
1213        data: HyperliquidExecModifyResponseData,
1214    },
1215    /// Generic response for other actions.
1216    #[serde(rename = "default")]
1217    Default,
1218    /// Catch-all for unknown response types.
1219    #[serde(other)]
1220    Unknown,
1221}
1222
1223/// Order response data containing status for each order from exchange endpoint.
1224#[derive(Debug, Clone, Serialize, Deserialize)]
1225pub struct HyperliquidExecOrderResponseData {
1226    /// Status for each order in the request.
1227    pub statuses: Vec<HyperliquidExecOrderStatus>,
1228}
1229
1230/// Cancel response data containing status for each cancellation from exchange endpoint.
1231#[derive(Debug, Clone, Serialize, Deserialize)]
1232pub struct HyperliquidExecCancelResponseData {
1233    /// Status for each cancellation in the request.
1234    pub statuses: Vec<HyperliquidExecCancelStatus>,
1235}
1236
1237/// Modify response data containing status for each modification from exchange endpoint.
1238#[derive(Debug, Clone, Serialize, Deserialize)]
1239pub struct HyperliquidExecModifyResponseData {
1240    /// Status for each modification in the request.
1241    pub statuses: Vec<HyperliquidExecModifyStatus>,
1242}
1243
1244/// Status of an individual order submission via exchange endpoint.
1245#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1246#[serde(untagged)]
1247pub enum HyperliquidExecOrderStatus {
1248    /// Order is resting on the order book.
1249    Resting {
1250        /// Resting order information.
1251        resting: HyperliquidExecRestingInfo,
1252    },
1253    /// Order was filled immediately.
1254    Filled {
1255        /// Fill information.
1256        filled: HyperliquidExecFilledInfo,
1257    },
1258    /// Order submission failed.
1259    Error {
1260        /// Error message.
1261        error: String,
1262    },
1263}
1264
1265/// Information about a resting order via exchange endpoint.
1266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1267pub struct HyperliquidExecRestingInfo {
1268    /// Order ID assigned by Hyperliquid.
1269    pub oid: OrderId,
1270}
1271
1272/// Information about a filled order via exchange endpoint.
1273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1274pub struct HyperliquidExecFilledInfo {
1275    /// Total filled size.
1276    #[serde(
1277        rename = "totalSz",
1278        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1279        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1280    )]
1281    pub total_sz: Decimal,
1282    /// Average fill price.
1283    #[serde(
1284        rename = "avgPx",
1285        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1286        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1287    )]
1288    pub avg_px: Decimal,
1289    /// Order ID.
1290    pub oid: OrderId,
1291}
1292
1293/// Status of an individual order cancellation via exchange endpoint.
1294#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1295#[serde(untagged)]
1296pub enum HyperliquidExecCancelStatus {
1297    /// Cancellation succeeded.
1298    Success(String), // Usually "success"
1299    /// Cancellation failed.
1300    Error {
1301        /// Error message.
1302        error: String,
1303    },
1304}
1305
1306/// Status of an individual order modification via exchange endpoint.
1307#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1308#[serde(untagged)]
1309pub enum HyperliquidExecModifyStatus {
1310    /// Modification succeeded.
1311    Success(String), // Usually "success"
1312    /// Modification failed.
1313    Error {
1314        /// Error message.
1315        error: String,
1316    },
1317}
1318
1319/// Complete clearinghouse state response from `POST /info` with `{ "type": "clearinghouseState", "user": "address" }`.
1320/// This provides account positions, margin information, and balances.
1321#[derive(Debug, Clone, Serialize, Deserialize)]
1322#[serde(rename_all = "camelCase")]
1323pub struct ClearinghouseState {
1324    /// List of asset positions (perpetual contracts).
1325    #[serde(default)]
1326    pub asset_positions: Vec<AssetPosition>,
1327    /// Cross margin summary information.
1328    #[serde(default)]
1329    pub cross_margin_summary: Option<CrossMarginSummary>,
1330    /// Time of the state snapshot (milliseconds since epoch).
1331    #[serde(default)]
1332    pub time: Option<u64>,
1333}
1334
1335/// A single asset position in the clearinghouse state.
1336#[derive(Debug, Clone, Serialize, Deserialize)]
1337#[serde(rename_all = "camelCase")]
1338pub struct AssetPosition {
1339    /// Position information.
1340    pub position: PositionData,
1341    /// Type of position.
1342    #[serde(rename = "type")]
1343    pub position_type: HyperliquidPositionType,
1344}
1345
1346/// Detailed position data for an asset.
1347#[derive(Debug, Clone, Serialize, Deserialize)]
1348#[serde(rename_all = "camelCase")]
1349pub struct PositionData {
1350    /// Asset symbol/coin (e.g., "BTC").
1351    pub coin: String,
1352    /// Cumulative funding (entry price weighted by position size changes).
1353    #[serde(
1354        rename = "cumFunding",
1355        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1356        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1357    )]
1358    pub cum_funding: Decimal,
1359    /// Entry price for the position.
1360    #[serde(
1361        rename = "entryPx",
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 entry_px: Option<Decimal>,
1367    /// Leverage used for the position.
1368    #[serde(
1369        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1370        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1371    )]
1372    pub leverage: Decimal,
1373    /// Liquidation price.
1374    #[serde(
1375        rename = "liquidationPx",
1376        serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1377        deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str",
1378        default
1379    )]
1380    pub liquidation_px: Option<Decimal>,
1381    /// Margin used for this position.
1382    #[serde(
1383        rename = "marginUsed",
1384        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1385        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1386    )]
1387    pub margin_used: Decimal,
1388    /// Maximum trade sizes allowed.
1389    #[serde(
1390        rename = "maxTradeSzs",
1391        serialize_with = "crate::common::parse::serialize_vec_decimal_as_str",
1392        deserialize_with = "crate::common::parse::deserialize_vec_decimal_from_str"
1393    )]
1394    pub max_trade_szs: Vec<Decimal>,
1395    /// Position value.
1396    #[serde(
1397        rename = "positionValue",
1398        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1399        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1400    )]
1401    pub position_value: Decimal,
1402    /// Return on equity percentage.
1403    #[serde(
1404        rename = "returnOnEquity",
1405        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1406        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1407    )]
1408    pub return_on_equity: Decimal,
1409    /// Position size (positive for long, negative for short).
1410    #[serde(
1411        rename = "szi",
1412        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1413        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1414    )]
1415    pub szi: Decimal,
1416    /// Unrealized PnL.
1417    #[serde(
1418        rename = "unrealizedPnl",
1419        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1420        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1421    )]
1422    pub unrealized_pnl: Decimal,
1423}
1424
1425/// Cross margin summary information.
1426#[derive(Debug, Clone, Serialize, Deserialize)]
1427#[serde(rename_all = "camelCase")]
1428pub struct CrossMarginSummary {
1429    /// Account value in USD.
1430    #[serde(
1431        rename = "accountValue",
1432        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1433        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1434    )]
1435    pub account_value: Decimal,
1436    /// Total notional position value.
1437    #[serde(
1438        rename = "totalNtlPos",
1439        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1440        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1441    )]
1442    pub total_ntl_pos: Decimal,
1443    /// Total raw USD value (collateral).
1444    #[serde(
1445        rename = "totalRawUsd",
1446        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1447        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1448    )]
1449    pub total_raw_usd: Decimal,
1450    /// Total margin used across all positions.
1451    #[serde(
1452        rename = "totalMarginUsed",
1453        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1454        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1455    )]
1456    pub total_margin_used: Decimal,
1457    /// Withdrawable balance.
1458    #[serde(
1459        rename = "withdrawable",
1460        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1461        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1462    )]
1463    pub withdrawable: Decimal,
1464}