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!("{byte:02x}"));
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#[cfg(test)]
783mod tests {
784    use rstest::rstest;
785
786    use super::*;
787
788    #[rstest]
789    fn test_meta_deserialization() {
790        let json = r#"{"universe": [{"name": "BTC", "szDecimals": 5}]}"#;
791
792        let meta: HyperliquidMeta = serde_json::from_str(json).unwrap();
793
794        assert_eq!(meta.universe.len(), 1);
795        assert_eq!(meta.universe[0].name, "BTC");
796        assert_eq!(meta.universe[0].sz_decimals, 5);
797    }
798
799    #[rstest]
800    fn test_l2_book_deserialization() {
801        let json = r#"{"coin": "BTC", "levels": [[{"px": "50000", "sz": "1.5"}], [{"px": "50100", "sz": "2.0"}]], "time": 1234567890}"#;
802
803        let book: HyperliquidL2Book = serde_json::from_str(json).unwrap();
804
805        assert_eq!(book.coin, "BTC");
806        assert_eq!(book.levels.len(), 2);
807        assert_eq!(book.time, 1234567890);
808    }
809
810    #[rstest]
811    fn test_exchange_response_deserialization() {
812        let json = r#"{"status": "ok", "response": {"type": "order"}}"#;
813
814        let response: HyperliquidExchangeResponse = serde_json::from_str(json).unwrap();
815
816        match response {
817            HyperliquidExchangeResponse::Status { status, .. } => assert_eq!(status, "ok"),
818            _ => panic!("Expected status response"),
819        }
820    }
821}
822
823/// Time-in-force for limit orders in exchange endpoint.
824///
825/// These values must match exactly what Hyperliquid expects for proper serialization.
826#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
827pub enum HyperliquidExecTif {
828    /// Add Liquidity Only (post-only order).
829    #[serde(rename = "Alo")]
830    Alo,
831    /// Immediate or Cancel.
832    #[serde(rename = "Ioc")]
833    Ioc,
834    /// Good Till Canceled.
835    #[serde(rename = "Gtc")]
836    Gtc,
837}
838
839/// Take profit or stop loss side for trigger orders in exchange endpoint.
840#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
841pub enum HyperliquidExecTpSl {
842    /// Take profit.
843    #[serde(rename = "tp")]
844    Tp,
845    /// Stop loss.
846    #[serde(rename = "sl")]
847    Sl,
848}
849
850/// Order grouping strategy for linked TP/SL orders in exchange endpoint.
851#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
852pub enum HyperliquidExecGrouping {
853    /// No grouping semantics.
854    #[serde(rename = "na")]
855    #[default]
856    Na,
857    /// Normal TP/SL grouping (linked orders).
858    #[serde(rename = "normalTpsl")]
859    NormalTpsl,
860    /// Position-level TP/SL grouping.
861    #[serde(rename = "positionTpsl")]
862    PositionTpsl,
863}
864
865/// Order kind specification for the `t` field in exchange endpoint order requests.
866#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
867#[serde(untagged)]
868pub enum HyperliquidExecOrderKind {
869    /// Limit order with time-in-force.
870    Limit {
871        /// Limit order parameters.
872        limit: HyperliquidExecLimitParams,
873    },
874    /// Trigger order (stop/take profit).
875    Trigger {
876        /// Trigger order parameters.
877        trigger: HyperliquidExecTriggerParams,
878    },
879}
880
881/// Parameters for limit orders in exchange endpoint.
882#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
883pub struct HyperliquidExecLimitParams {
884    /// Time-in-force for the limit order.
885    pub tif: HyperliquidExecTif,
886}
887
888/// Parameters for trigger orders (stop/take profit) in exchange endpoint.
889#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
890#[serde(rename_all = "camelCase")]
891pub struct HyperliquidExecTriggerParams {
892    /// Whether to use market price when triggered.
893    pub is_market: bool,
894    /// Trigger price as a string.
895    #[serde(
896        serialize_with = "crate::common::parse::serialize_decimal_as_str",
897        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
898    )]
899    pub trigger_px: Decimal,
900    /// Whether this is a take profit or stop loss.
901    pub tpsl: HyperliquidExecTpSl,
902}
903
904/// Optional builder fee for orders in exchange endpoint.
905///
906/// The builder fee is specified in tenths of a basis point.
907/// For example, `f: 10` represents 1 basis point (0.01%).
908#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
909pub struct HyperliquidExecBuilderFee {
910    /// Builder address to receive the fee.
911    #[serde(rename = "b")]
912    pub address: String,
913    /// Fee in tenths of a basis point.
914    #[serde(rename = "f")]
915    pub fee_tenths_bp: u32,
916}
917
918/// Order specification for placing orders via exchange endpoint.
919///
920/// This struct represents a single order in the exact format expected
921/// by the Hyperliquid exchange endpoint.
922#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
923pub struct HyperliquidExecPlaceOrderRequest {
924    /// Asset ID.
925    #[serde(rename = "a")]
926    pub asset: AssetId,
927    /// Is buy order (true for buy, false for sell).
928    #[serde(rename = "b")]
929    pub is_buy: bool,
930    /// Price as a string with no trailing zeros.
931    #[serde(
932        rename = "p",
933        serialize_with = "crate::common::parse::serialize_decimal_as_str",
934        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
935    )]
936    pub price: Decimal,
937    /// Size as a string with no trailing zeros.
938    #[serde(
939        rename = "s",
940        serialize_with = "crate::common::parse::serialize_decimal_as_str",
941        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
942    )]
943    pub size: Decimal,
944    /// Reduce-only flag.
945    #[serde(rename = "r")]
946    pub reduce_only: bool,
947    /// Order type (limit or trigger).
948    #[serde(rename = "t")]
949    pub kind: HyperliquidExecOrderKind,
950    /// Optional client order ID (128-bit hex).
951    #[serde(rename = "c", skip_serializing_if = "Option::is_none")]
952    pub cloid: Option<Cloid>,
953}
954
955/// Cancel specification for canceling orders by order ID via exchange endpoint.
956#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
957pub struct HyperliquidExecCancelOrderRequest {
958    /// Asset ID.
959    #[serde(rename = "a")]
960    pub asset: AssetId,
961    /// Order ID to cancel.
962    #[serde(rename = "o")]
963    pub oid: OrderId,
964}
965
966/// Cancel specification for canceling orders by client order ID via exchange endpoint.
967#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
968pub struct HyperliquidExecCancelByCloidRequest {
969    /// Asset ID.
970    #[serde(rename = "a")]
971    pub asset: AssetId,
972    /// Client order ID to cancel.
973    #[serde(rename = "c")]
974    pub cloid: Cloid,
975}
976
977/// Modify specification for modifying existing orders via exchange endpoint.
978#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
979pub struct HyperliquidExecModifyOrderRequest {
980    /// Asset ID.
981    #[serde(rename = "a")]
982    pub asset: AssetId,
983    /// Order ID to modify.
984    #[serde(rename = "o")]
985    pub oid: OrderId,
986    /// New price (optional).
987    #[serde(
988        rename = "p",
989        skip_serializing_if = "Option::is_none",
990        serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
991        deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
992    )]
993    pub price: Option<Decimal>,
994    /// New size (optional).
995    #[serde(
996        rename = "s",
997        skip_serializing_if = "Option::is_none",
998        serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
999        deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
1000    )]
1001    pub size: Option<Decimal>,
1002    /// New reduce-only flag (optional).
1003    #[serde(rename = "r", skip_serializing_if = "Option::is_none")]
1004    pub reduce_only: Option<bool>,
1005    /// New order type (optional).
1006    #[serde(rename = "t", skip_serializing_if = "Option::is_none")]
1007    pub kind: Option<HyperliquidExecOrderKind>,
1008}
1009
1010/// TWAP (Time-Weighted Average Price) order specification for exchange endpoint.
1011#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1012pub struct HyperliquidExecTwapRequest {
1013    /// Asset ID.
1014    #[serde(rename = "a")]
1015    pub asset: AssetId,
1016    /// Is buy order.
1017    #[serde(rename = "b")]
1018    pub is_buy: bool,
1019    /// Total size to execute.
1020    #[serde(
1021        rename = "s",
1022        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1023        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1024    )]
1025    pub size: Decimal,
1026    /// Duration in milliseconds.
1027    #[serde(rename = "m")]
1028    pub duration_ms: u64,
1029}
1030
1031/// All possible exchange actions for the Hyperliquid `/exchange` endpoint.
1032///
1033/// Each variant corresponds to a specific action type that can be performed
1034/// through the exchange API. The serialization uses the exact action type
1035/// names expected by Hyperliquid.
1036#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1037#[serde(tag = "type")]
1038pub enum HyperliquidExecAction {
1039    /// Place one or more orders.
1040    #[serde(rename = "order")]
1041    Order {
1042        /// List of orders to place.
1043        orders: Vec<HyperliquidExecPlaceOrderRequest>,
1044        /// Grouping strategy for TP/SL orders.
1045        #[serde(default)]
1046        grouping: HyperliquidExecGrouping,
1047        /// Optional builder fee.
1048        #[serde(skip_serializing_if = "Option::is_none")]
1049        builder: Option<HyperliquidExecBuilderFee>,
1050    },
1051
1052    /// Cancel orders by order ID.
1053    #[serde(rename = "cancel")]
1054    Cancel {
1055        /// Orders to cancel.
1056        cancels: Vec<HyperliquidExecCancelOrderRequest>,
1057    },
1058
1059    /// Cancel orders by client order ID.
1060    #[serde(rename = "cancelByCloid")]
1061    CancelByCloid {
1062        /// Orders to cancel by CLOID.
1063        cancels: Vec<HyperliquidExecCancelByCloidRequest>,
1064    },
1065
1066    /// Modify a single order.
1067    #[serde(rename = "modify")]
1068    Modify {
1069        /// Order modification specification.
1070        #[serde(flatten)]
1071        modify: HyperliquidExecModifyOrderRequest,
1072    },
1073
1074    /// Modify multiple orders atomically.
1075    #[serde(rename = "batchModify")]
1076    BatchModify {
1077        /// Multiple order modifications.
1078        modifies: Vec<HyperliquidExecModifyOrderRequest>,
1079    },
1080
1081    /// Schedule automatic order cancellation (dead man's switch).
1082    #[serde(rename = "scheduleCancel")]
1083    ScheduleCancel {
1084        /// Time in milliseconds when orders should be cancelled.
1085        /// If None, clears the existing schedule.
1086        #[serde(skip_serializing_if = "Option::is_none")]
1087        time: Option<u64>,
1088    },
1089
1090    /// Update leverage for a position.
1091    #[serde(rename = "updateLeverage")]
1092    UpdateLeverage {
1093        /// Asset ID.
1094        #[serde(rename = "a")]
1095        asset: AssetId,
1096        /// Whether to use cross margin.
1097        #[serde(rename = "isCross")]
1098        is_cross: bool,
1099        /// Leverage value.
1100        #[serde(rename = "leverage")]
1101        leverage: u32,
1102    },
1103
1104    /// Update isolated margin for a position.
1105    #[serde(rename = "updateIsolatedMargin")]
1106    UpdateIsolatedMargin {
1107        /// Asset ID.
1108        #[serde(rename = "a")]
1109        asset: AssetId,
1110        /// Margin delta as a string.
1111        #[serde(
1112            rename = "delta",
1113            serialize_with = "crate::common::parse::serialize_decimal_as_str",
1114            deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1115        )]
1116        delta: Decimal,
1117    },
1118
1119    /// Transfer USD between spot and perp accounts.
1120    #[serde(rename = "usdClassTransfer")]
1121    UsdClassTransfer {
1122        /// Source account type.
1123        from: String,
1124        /// Destination account type.
1125        to: String,
1126        /// Amount to transfer.
1127        #[serde(
1128            serialize_with = "crate::common::parse::serialize_decimal_as_str",
1129            deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1130        )]
1131        amount: Decimal,
1132    },
1133
1134    /// Place a TWAP order.
1135    #[serde(rename = "twapPlace")]
1136    TwapPlace {
1137        /// TWAP order specification.
1138        #[serde(flatten)]
1139        twap: HyperliquidExecTwapRequest,
1140    },
1141
1142    /// Cancel a TWAP order.
1143    #[serde(rename = "twapCancel")]
1144    TwapCancel {
1145        /// Asset ID.
1146        #[serde(rename = "a")]
1147        asset: AssetId,
1148        /// TWAP ID.
1149        #[serde(rename = "t")]
1150        twap_id: u64,
1151    },
1152
1153    /// No-operation to invalidate pending nonces.
1154    #[serde(rename = "noop")]
1155    Noop,
1156}
1157
1158/// Exchange request envelope for the `/exchange` endpoint.
1159///
1160/// This is the top-level structure sent to Hyperliquid's exchange endpoint.
1161/// It includes the action to perform along with authentication and metadata.
1162#[derive(Debug, Clone, Serialize)]
1163#[serde(rename_all = "camelCase")]
1164pub struct HyperliquidExecRequest {
1165    /// The exchange action to perform.
1166    pub action: HyperliquidExecAction,
1167    /// Request nonce for replay protection (milliseconds timestamp recommended).
1168    pub nonce: u64,
1169    /// ECC signature over the action and nonce.
1170    pub signature: String,
1171    /// Optional vault address for sub-account trading.
1172    #[serde(skip_serializing_if = "Option::is_none")]
1173    pub vault_address: Option<String>,
1174    /// Optional expiration time in milliseconds.
1175    /// Note: Using this field increases rate limit weight by 5x if the request expires.
1176    #[serde(skip_serializing_if = "Option::is_none")]
1177    pub expires_after: Option<u64>,
1178}
1179
1180/// Exchange response envelope from the `/exchange` endpoint.
1181#[derive(Debug, Clone, Serialize, Deserialize)]
1182pub struct HyperliquidExecResponse {
1183    /// Response status ("ok" for success).
1184    pub status: String,
1185    /// Response payload.
1186    pub response: HyperliquidExecResponseData,
1187}
1188
1189/// Response data containing the actual response payload from exchange endpoint.
1190#[derive(Debug, Clone, Serialize, Deserialize)]
1191#[serde(tag = "type")]
1192pub enum HyperliquidExecResponseData {
1193    /// Response for order actions.
1194    #[serde(rename = "order")]
1195    Order {
1196        /// Order response data.
1197        data: HyperliquidExecOrderResponseData,
1198    },
1199    /// Response for cancel actions.
1200    #[serde(rename = "cancel")]
1201    Cancel {
1202        /// Cancel response data.
1203        data: HyperliquidExecCancelResponseData,
1204    },
1205    /// Response for modify actions.
1206    #[serde(rename = "modify")]
1207    Modify {
1208        /// Modify response data.
1209        data: HyperliquidExecModifyResponseData,
1210    },
1211    /// Generic response for other actions.
1212    #[serde(rename = "default")]
1213    Default,
1214    /// Catch-all for unknown response types.
1215    #[serde(other)]
1216    Unknown,
1217}
1218
1219/// Order response data containing status for each order from exchange endpoint.
1220#[derive(Debug, Clone, Serialize, Deserialize)]
1221pub struct HyperliquidExecOrderResponseData {
1222    /// Status for each order in the request.
1223    pub statuses: Vec<HyperliquidExecOrderStatus>,
1224}
1225
1226/// Cancel response data containing status for each cancellation from exchange endpoint.
1227#[derive(Debug, Clone, Serialize, Deserialize)]
1228pub struct HyperliquidExecCancelResponseData {
1229    /// Status for each cancellation in the request.
1230    pub statuses: Vec<HyperliquidExecCancelStatus>,
1231}
1232
1233/// Modify response data containing status for each modification from exchange endpoint.
1234#[derive(Debug, Clone, Serialize, Deserialize)]
1235pub struct HyperliquidExecModifyResponseData {
1236    /// Status for each modification in the request.
1237    pub statuses: Vec<HyperliquidExecModifyStatus>,
1238}
1239
1240/// Status of an individual order submission via exchange endpoint.
1241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1242#[serde(untagged)]
1243pub enum HyperliquidExecOrderStatus {
1244    /// Order is resting on the order book.
1245    Resting {
1246        /// Resting order information.
1247        resting: HyperliquidExecRestingInfo,
1248    },
1249    /// Order was filled immediately.
1250    Filled {
1251        /// Fill information.
1252        filled: HyperliquidExecFilledInfo,
1253    },
1254    /// Order submission failed.
1255    Error {
1256        /// Error message.
1257        error: String,
1258    },
1259}
1260
1261/// Information about a resting order via exchange endpoint.
1262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1263pub struct HyperliquidExecRestingInfo {
1264    /// Order ID assigned by Hyperliquid.
1265    pub oid: OrderId,
1266}
1267
1268/// Information about a filled order via exchange endpoint.
1269#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1270pub struct HyperliquidExecFilledInfo {
1271    /// Total filled size.
1272    #[serde(
1273        rename = "totalSz",
1274        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1275        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1276    )]
1277    pub total_sz: Decimal,
1278    /// Average fill price.
1279    #[serde(
1280        rename = "avgPx",
1281        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1282        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1283    )]
1284    pub avg_px: Decimal,
1285    /// Order ID.
1286    pub oid: OrderId,
1287}
1288
1289/// Status of an individual order cancellation via exchange endpoint.
1290#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1291#[serde(untagged)]
1292pub enum HyperliquidExecCancelStatus {
1293    /// Cancellation succeeded.
1294    Success(String), // Usually "success"
1295    /// Cancellation failed.
1296    Error {
1297        /// Error message.
1298        error: String,
1299    },
1300}
1301
1302/// Status of an individual order modification via exchange endpoint.
1303#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1304#[serde(untagged)]
1305pub enum HyperliquidExecModifyStatus {
1306    /// Modification succeeded.
1307    Success(String), // Usually "success"
1308    /// Modification failed.
1309    Error {
1310        /// Error message.
1311        error: String,
1312    },
1313}
1314
1315/// Complete clearinghouse state response from `POST /info` with `{ "type": "clearinghouseState", "user": "address" }`.
1316/// This provides account positions, margin information, and balances.
1317#[derive(Debug, Clone, Serialize, Deserialize)]
1318#[serde(rename_all = "camelCase")]
1319pub struct ClearinghouseState {
1320    /// List of asset positions (perpetual contracts).
1321    #[serde(default)]
1322    pub asset_positions: Vec<AssetPosition>,
1323    /// Cross margin summary information.
1324    #[serde(default)]
1325    pub cross_margin_summary: Option<CrossMarginSummary>,
1326    /// Time of the state snapshot (milliseconds since epoch).
1327    #[serde(default)]
1328    pub time: Option<u64>,
1329}
1330
1331/// A single asset position in the clearinghouse state.
1332#[derive(Debug, Clone, Serialize, Deserialize)]
1333#[serde(rename_all = "camelCase")]
1334pub struct AssetPosition {
1335    /// Position information.
1336    pub position: PositionData,
1337    /// Type of position.
1338    #[serde(rename = "type")]
1339    pub position_type: HyperliquidPositionType,
1340}
1341
1342/// Detailed position data for an asset.
1343#[derive(Debug, Clone, Serialize, Deserialize)]
1344#[serde(rename_all = "camelCase")]
1345pub struct PositionData {
1346    /// Asset symbol/coin (e.g., "BTC").
1347    pub coin: String,
1348    /// Cumulative funding (entry price weighted by position size changes).
1349    #[serde(
1350        rename = "cumFunding",
1351        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1352        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1353    )]
1354    pub cum_funding: Decimal,
1355    /// Entry price for the position.
1356    #[serde(
1357        rename = "entryPx",
1358        serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1359        deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str",
1360        default
1361    )]
1362    pub entry_px: Option<Decimal>,
1363    /// Leverage used for the position.
1364    #[serde(
1365        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1366        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1367    )]
1368    pub leverage: Decimal,
1369    /// Liquidation price.
1370    #[serde(
1371        rename = "liquidationPx",
1372        serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1373        deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str",
1374        default
1375    )]
1376    pub liquidation_px: Option<Decimal>,
1377    /// Margin used for this position.
1378    #[serde(
1379        rename = "marginUsed",
1380        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1381        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1382    )]
1383    pub margin_used: Decimal,
1384    /// Maximum trade sizes allowed.
1385    #[serde(
1386        rename = "maxTradeSzs",
1387        serialize_with = "crate::common::parse::serialize_vec_decimal_as_str",
1388        deserialize_with = "crate::common::parse::deserialize_vec_decimal_from_str"
1389    )]
1390    pub max_trade_szs: Vec<Decimal>,
1391    /// Position value.
1392    #[serde(
1393        rename = "positionValue",
1394        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1395        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1396    )]
1397    pub position_value: Decimal,
1398    /// Return on equity percentage.
1399    #[serde(
1400        rename = "returnOnEquity",
1401        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1402        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1403    )]
1404    pub return_on_equity: Decimal,
1405    /// Position size (positive for long, negative for short).
1406    #[serde(
1407        rename = "szi",
1408        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1409        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1410    )]
1411    pub szi: Decimal,
1412    /// Unrealized PnL.
1413    #[serde(
1414        rename = "unrealizedPnl",
1415        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1416        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1417    )]
1418    pub unrealized_pnl: Decimal,
1419}
1420
1421/// Cross margin summary information.
1422#[derive(Debug, Clone, Serialize, Deserialize)]
1423#[serde(rename_all = "camelCase")]
1424pub struct CrossMarginSummary {
1425    /// Account value in USD.
1426    #[serde(
1427        rename = "accountValue",
1428        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1429        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1430    )]
1431    pub account_value: Decimal,
1432    /// Total notional position value.
1433    #[serde(
1434        rename = "totalNtlPos",
1435        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1436        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1437    )]
1438    pub total_ntl_pos: Decimal,
1439    /// Total raw USD value (collateral).
1440    #[serde(
1441        rename = "totalRawUsd",
1442        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1443        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1444    )]
1445    pub total_raw_usd: Decimal,
1446    /// Total margin used across all positions.
1447    #[serde(
1448        rename = "totalMarginUsed",
1449        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1450        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1451    )]
1452    pub total_margin_used: Decimal,
1453    /// Withdrawable balance.
1454    #[serde(
1455        rename = "withdrawable",
1456        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1457        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1458    )]
1459    pub withdrawable: Decimal,
1460}