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