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