Skip to main content

nautilus_hyperliquid/http/
models.rs

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