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, HyperliquidOrderStatus as HyperliquidOrderStatusEnum,
26    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}
393
394/// Represents order status response from `POST /info`.
395#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct HyperliquidOrderStatus {
397    #[serde(default)]
398    pub statuses: Vec<HyperliquidOrderStatusEntry>,
399}
400
401/// Represents an individual order status entry.
402#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct HyperliquidOrderStatusEntry {
404    /// Order information.
405    pub order: HyperliquidOrderInfo,
406    /// Current status.
407    pub status: HyperliquidOrderStatusEnum,
408    /// Status timestamp in milliseconds.
409    #[serde(rename = "statusTimestamp")]
410    pub status_timestamp: u64,
411}
412
413/// Represents order information within an order status entry.
414#[derive(Debug, Clone, Serialize, Deserialize)]
415pub struct HyperliquidOrderInfo {
416    /// Coin symbol.
417    pub coin: Ustr,
418    /// Order side (buy/sell).
419    pub side: HyperliquidSide,
420    /// Limit price.
421    #[serde(rename = "limitPx")]
422    pub limit_px: String,
423    /// Order size.
424    pub sz: String,
425    /// Order ID.
426    pub oid: u64,
427    /// Order timestamp in milliseconds.
428    pub timestamp: u64,
429    /// Original order size.
430    #[serde(rename = "origSz")]
431    pub orig_sz: String,
432}
433
434/// ECC signature components for Hyperliquid exchange requests.
435#[derive(Debug, Clone, Serialize)]
436pub struct HyperliquidSignature {
437    /// R component of the signature.
438    pub r: String,
439    /// S component of the signature.
440    pub s: String,
441    /// V component (recovery ID) of the signature.
442    pub v: u64,
443}
444
445impl HyperliquidSignature {
446    /// Parse a hex signature string (0x + 64 hex r + 64 hex s + 2 hex v) into components.
447    pub fn from_hex(sig_hex: &str) -> Result<Self, String> {
448        let sig_hex = sig_hex.strip_prefix("0x").unwrap_or(sig_hex);
449
450        if sig_hex.len() != 130 {
451            return Err(format!(
452                "Invalid signature length: expected 130 hex chars, was {}",
453                sig_hex.len()
454            ));
455        }
456
457        let r = format!("0x{}", &sig_hex[0..64]);
458        let s = format!("0x{}", &sig_hex[64..128]);
459        let v = u64::from_str_radix(&sig_hex[128..130], 16)
460            .map_err(|e| format!("Failed to parse v component: {e}"))?;
461
462        Ok(Self { r, s, v })
463    }
464}
465
466/// Represents an exchange action request wrapper for `POST /exchange`.
467#[derive(Debug, Clone, Serialize)]
468pub struct HyperliquidExchangeRequest<T> {
469    /// The action to perform.
470    #[serde(rename = "action")]
471    pub action: T,
472    /// Request nonce for replay protection.
473    #[serde(rename = "nonce")]
474    pub nonce: u64,
475    /// ECC signature over the action.
476    #[serde(rename = "signature")]
477    pub signature: HyperliquidSignature,
478    /// Optional vault address for sub-account trading.
479    #[serde(rename = "vaultAddress", skip_serializing_if = "Option::is_none")]
480    pub vault_address: Option<String>,
481    /// Optional expiration time in milliseconds.
482    #[serde(rename = "expiresAfter", skip_serializing_if = "Option::is_none")]
483    pub expires_after: Option<u64>,
484}
485
486impl<T> HyperliquidExchangeRequest<T>
487where
488    T: Serialize,
489{
490    /// Create a new exchange request with the given action.
491    pub fn new(action: T, nonce: u64, signature: String) -> Result<Self, String> {
492        Ok(Self {
493            action,
494            nonce,
495            signature: HyperliquidSignature::from_hex(&signature)?,
496            vault_address: None,
497            expires_after: None,
498        })
499    }
500
501    /// Create a new exchange request with vault address for sub-account trading.
502    pub fn with_vault(
503        action: T,
504        nonce: u64,
505        signature: String,
506        vault_address: String,
507    ) -> Result<Self, String> {
508        Ok(Self {
509            action,
510            nonce,
511            signature: HyperliquidSignature::from_hex(&signature)?,
512            vault_address: Some(vault_address),
513            expires_after: None,
514        })
515    }
516
517    /// Convert to JSON value for signing purposes.
518    pub fn to_sign_value(&self) -> serde_json::Result<serde_json::Value> {
519        serde_json::to_value(self)
520    }
521}
522
523/// Represents an exchange response wrapper from `POST /exchange`.
524#[derive(Debug, Clone, Serialize, Deserialize)]
525#[serde(untagged)]
526pub enum HyperliquidExchangeResponse {
527    /// Successful response with status.
528    Status {
529        /// Status message.
530        status: String,
531        /// Response payload.
532        response: serde_json::Value,
533    },
534    /// Error response.
535    Error {
536        /// Error message.
537        error: String,
538    },
539}
540
541#[cfg(test)]
542mod tests {
543    use rstest::rstest;
544
545    use super::*;
546
547    #[rstest]
548    fn test_meta_deserialization() {
549        let json = r#"{"universe": [{"name": "BTC", "szDecimals": 5}]}"#;
550
551        let meta: HyperliquidMeta = serde_json::from_str(json).unwrap();
552
553        assert_eq!(meta.universe.len(), 1);
554        assert_eq!(meta.universe[0].name, "BTC");
555        assert_eq!(meta.universe[0].sz_decimals, 5);
556    }
557
558    #[rstest]
559    fn test_l2_book_deserialization() {
560        let json = r#"{"coin": "BTC", "levels": [[{"px": "50000", "sz": "1.5"}], [{"px": "50100", "sz": "2.0"}]], "time": 1234567890}"#;
561
562        let book: HyperliquidL2Book = serde_json::from_str(json).unwrap();
563
564        assert_eq!(book.coin, "BTC");
565        assert_eq!(book.levels.len(), 2);
566        assert_eq!(book.time, 1234567890);
567    }
568
569    #[rstest]
570    fn test_exchange_response_deserialization() {
571        let json = r#"{"status": "ok", "response": {"type": "order"}}"#;
572
573        let response: HyperliquidExchangeResponse = serde_json::from_str(json).unwrap();
574
575        match response {
576            HyperliquidExchangeResponse::Status { status, .. } => assert_eq!(status, "ok"),
577            _ => panic!("Expected status response"),
578        }
579    }
580
581    #[rstest]
582    fn test_msgpack_serialization_matches_python() {
583        // Test that msgpack serialization includes the "type" tag properly.
584        // Python SDK serializes: {"type": "order", "orders": [...], "grouping": "na"}
585        // We need to verify rmp_serde::to_vec_named produces the same format.
586
587        let action = HyperliquidExecAction::Order {
588            orders: vec![],
589            grouping: HyperliquidExecGrouping::Na,
590            builder: None,
591        };
592
593        // First verify JSON is correct
594        let json = serde_json::to_string(&action).unwrap();
595        assert!(
596            json.contains(r#""type":"order""#),
597            "JSON should have type tag: {json}"
598        );
599
600        // Serialize with msgpack
601        let msgpack_bytes = rmp_serde::to_vec_named(&action).unwrap();
602
603        // Decode back to a generic Value to inspect the structure
604        let decoded: serde_json::Value = rmp_serde::from_slice(&msgpack_bytes).unwrap();
605
606        // The decoded value should have a "type" field
607        assert!(
608            decoded.get("type").is_some(),
609            "MsgPack should have type tag. Decoded: {decoded:?}"
610        );
611        assert_eq!(
612            decoded.get("type").unwrap().as_str().unwrap(),
613            "order",
614            "Type should be 'order'"
615        );
616        assert!(decoded.get("orders").is_some(), "Should have orders field");
617        assert!(
618            decoded.get("grouping").is_some(),
619            "Should have grouping field"
620        );
621    }
622}
623
624/// Time-in-force for limit orders in exchange endpoint.
625///
626/// These values must match exactly what Hyperliquid expects for proper serialization.
627#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
628pub enum HyperliquidExecTif {
629    /// Add Liquidity Only (post-only order).
630    #[serde(rename = "Alo")]
631    Alo,
632    /// Immediate or Cancel.
633    #[serde(rename = "Ioc")]
634    Ioc,
635    /// Good Till Canceled.
636    #[serde(rename = "Gtc")]
637    Gtc,
638}
639
640/// Take profit or stop loss side for trigger orders in exchange endpoint.
641#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
642pub enum HyperliquidExecTpSl {
643    /// Take profit.
644    #[serde(rename = "tp")]
645    Tp,
646    /// Stop loss.
647    #[serde(rename = "sl")]
648    Sl,
649}
650
651/// Order grouping strategy for linked TP/SL orders in exchange endpoint.
652#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
653pub enum HyperliquidExecGrouping {
654    /// No grouping semantics.
655    #[serde(rename = "na")]
656    #[default]
657    Na,
658    /// Normal TP/SL grouping (linked orders).
659    #[serde(rename = "normalTpsl")]
660    NormalTpsl,
661    /// Position-level TP/SL grouping.
662    #[serde(rename = "positionTpsl")]
663    PositionTpsl,
664}
665
666/// Order kind specification for the `t` field in exchange endpoint order requests.
667#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
668#[serde(untagged)]
669pub enum HyperliquidExecOrderKind {
670    /// Limit order with time-in-force.
671    Limit {
672        /// Limit order parameters.
673        limit: HyperliquidExecLimitParams,
674    },
675    /// Trigger order (stop/take profit).
676    Trigger {
677        /// Trigger order parameters.
678        trigger: HyperliquidExecTriggerParams,
679    },
680}
681
682/// Parameters for limit orders in exchange endpoint.
683#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
684pub struct HyperliquidExecLimitParams {
685    /// Time-in-force for the limit order.
686    pub tif: HyperliquidExecTif,
687}
688
689/// Parameters for trigger orders (stop/take profit) in exchange endpoint.
690#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
691#[serde(rename_all = "camelCase")]
692pub struct HyperliquidExecTriggerParams {
693    /// Whether to use market price when triggered.
694    pub is_market: bool,
695    /// Trigger price as a string.
696    #[serde(
697        serialize_with = "crate::common::parse::serialize_decimal_as_str",
698        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
699    )]
700    pub trigger_px: Decimal,
701    /// Whether this is a take profit or stop loss.
702    pub tpsl: HyperliquidExecTpSl,
703}
704
705/// Optional builder fee for orders in exchange endpoint.
706///
707/// The builder fee is specified in tenths of a basis point.
708/// For example, `f: 10` represents 1 basis point (0.01%).
709#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
710pub struct HyperliquidExecBuilderFee {
711    /// Builder address to receive the fee.
712    #[serde(rename = "b")]
713    pub address: String,
714    /// Fee in tenths of a basis point.
715    #[serde(rename = "f")]
716    pub fee_tenths_bp: u32,
717}
718
719/// Order specification for placing orders via exchange endpoint.
720///
721/// This struct represents a single order in the exact format expected
722/// by the Hyperliquid exchange endpoint.
723#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
724pub struct HyperliquidExecPlaceOrderRequest {
725    /// Asset ID.
726    #[serde(rename = "a")]
727    pub asset: AssetId,
728    /// Is buy order (true for buy, false for sell).
729    #[serde(rename = "b")]
730    pub is_buy: bool,
731    /// Price as a string with no trailing zeros.
732    #[serde(
733        rename = "p",
734        serialize_with = "crate::common::parse::serialize_decimal_as_str",
735        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
736    )]
737    pub price: Decimal,
738    /// Size as a string with no trailing zeros.
739    #[serde(
740        rename = "s",
741        serialize_with = "crate::common::parse::serialize_decimal_as_str",
742        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
743    )]
744    pub size: Decimal,
745    /// Reduce-only flag.
746    #[serde(rename = "r")]
747    pub reduce_only: bool,
748    /// Order type (limit or trigger).
749    #[serde(rename = "t")]
750    pub kind: HyperliquidExecOrderKind,
751    /// Optional client order ID (128-bit hex).
752    #[serde(rename = "c", skip_serializing_if = "Option::is_none")]
753    pub cloid: Option<Cloid>,
754}
755
756/// Cancel specification for canceling orders by order ID via exchange endpoint.
757#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
758pub struct HyperliquidExecCancelOrderRequest {
759    /// Asset ID.
760    #[serde(rename = "a")]
761    pub asset: AssetId,
762    /// Order ID to cancel.
763    #[serde(rename = "o")]
764    pub oid: OrderId,
765}
766
767/// Cancel specification for canceling orders by client order ID via exchange endpoint.
768///
769/// Note: Unlike order placement which uses abbreviated field names ("a", "c"),
770/// cancel-by-cloid uses full field names ("asset", "cloid") per the Hyperliquid API.
771#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
772pub struct HyperliquidExecCancelByCloidRequest {
773    /// Asset ID.
774    pub asset: AssetId,
775    /// Client order ID to cancel.
776    pub cloid: Cloid,
777}
778
779/// Modify specification for modifying existing orders via exchange endpoint.
780#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
781pub struct HyperliquidExecModifyOrderRequest {
782    /// Asset ID.
783    #[serde(rename = "a")]
784    pub asset: AssetId,
785    /// Order ID to modify.
786    #[serde(rename = "o")]
787    pub oid: OrderId,
788    /// New price (optional).
789    #[serde(
790        rename = "p",
791        skip_serializing_if = "Option::is_none",
792        serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
793        deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
794    )]
795    pub price: Option<Decimal>,
796    /// New size (optional).
797    #[serde(
798        rename = "s",
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 size: Option<Decimal>,
804    /// New reduce-only flag (optional).
805    #[serde(rename = "r", skip_serializing_if = "Option::is_none")]
806    pub reduce_only: Option<bool>,
807    /// New order type (optional).
808    #[serde(rename = "t", skip_serializing_if = "Option::is_none")]
809    pub kind: Option<HyperliquidExecOrderKind>,
810}
811
812/// TWAP (Time-Weighted Average Price) order specification for exchange endpoint.
813#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
814pub struct HyperliquidExecTwapRequest {
815    /// Asset ID.
816    #[serde(rename = "a")]
817    pub asset: AssetId,
818    /// Is buy order.
819    #[serde(rename = "b")]
820    pub is_buy: bool,
821    /// Total size to execute.
822    #[serde(
823        rename = "s",
824        serialize_with = "crate::common::parse::serialize_decimal_as_str",
825        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
826    )]
827    pub size: Decimal,
828    /// Duration in milliseconds.
829    #[serde(rename = "m")]
830    pub duration_ms: u64,
831}
832
833/// All possible exchange actions for the Hyperliquid `/exchange` endpoint.
834///
835/// Each variant corresponds to a specific action type that can be performed
836/// through the exchange API. The serialization uses the exact action type
837/// names expected by Hyperliquid.
838#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
839#[serde(tag = "type")]
840pub enum HyperliquidExecAction {
841    /// Place one or more orders.
842    #[serde(rename = "order")]
843    Order {
844        /// List of orders to place.
845        orders: Vec<HyperliquidExecPlaceOrderRequest>,
846        /// Grouping strategy for TP/SL orders.
847        #[serde(default)]
848        grouping: HyperliquidExecGrouping,
849        /// Optional builder fee.
850        #[serde(skip_serializing_if = "Option::is_none")]
851        builder: Option<HyperliquidExecBuilderFee>,
852    },
853
854    /// Cancel orders by order ID.
855    #[serde(rename = "cancel")]
856    Cancel {
857        /// Orders to cancel.
858        cancels: Vec<HyperliquidExecCancelOrderRequest>,
859    },
860
861    /// Cancel orders by client order ID.
862    #[serde(rename = "cancelByCloid")]
863    CancelByCloid {
864        /// Orders to cancel by CLOID.
865        cancels: Vec<HyperliquidExecCancelByCloidRequest>,
866    },
867
868    /// Modify a single order.
869    #[serde(rename = "modify")]
870    Modify {
871        /// Order modification specification.
872        #[serde(flatten)]
873        modify: HyperliquidExecModifyOrderRequest,
874    },
875
876    /// Modify multiple orders atomically.
877    #[serde(rename = "batchModify")]
878    BatchModify {
879        /// Multiple order modifications.
880        modifies: Vec<HyperliquidExecModifyOrderRequest>,
881    },
882
883    /// Schedule automatic order cancellation (dead man's switch).
884    #[serde(rename = "scheduleCancel")]
885    ScheduleCancel {
886        /// Time in milliseconds when orders should be cancelled.
887        /// If None, clears the existing schedule.
888        #[serde(skip_serializing_if = "Option::is_none")]
889        time: Option<u64>,
890    },
891
892    /// Update leverage for a position.
893    #[serde(rename = "updateLeverage")]
894    UpdateLeverage {
895        /// Asset ID.
896        #[serde(rename = "a")]
897        asset: AssetId,
898        /// Whether to use cross margin.
899        #[serde(rename = "isCross")]
900        is_cross: bool,
901        /// Leverage value.
902        #[serde(rename = "leverage")]
903        leverage: u32,
904    },
905
906    /// Update isolated margin for a position.
907    #[serde(rename = "updateIsolatedMargin")]
908    UpdateIsolatedMargin {
909        /// Asset ID.
910        #[serde(rename = "a")]
911        asset: AssetId,
912        /// Margin delta as a string.
913        #[serde(
914            rename = "delta",
915            serialize_with = "crate::common::parse::serialize_decimal_as_str",
916            deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
917        )]
918        delta: Decimal,
919    },
920
921    /// Transfer USD between spot and perp accounts.
922    #[serde(rename = "usdClassTransfer")]
923    UsdClassTransfer {
924        /// Source account type.
925        from: String,
926        /// Destination account type.
927        to: String,
928        /// Amount to transfer.
929        #[serde(
930            serialize_with = "crate::common::parse::serialize_decimal_as_str",
931            deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
932        )]
933        amount: Decimal,
934    },
935
936    /// Place a TWAP order.
937    #[serde(rename = "twapPlace")]
938    TwapPlace {
939        /// TWAP order specification.
940        #[serde(flatten)]
941        twap: HyperliquidExecTwapRequest,
942    },
943
944    /// Cancel a TWAP order.
945    #[serde(rename = "twapCancel")]
946    TwapCancel {
947        /// Asset ID.
948        #[serde(rename = "a")]
949        asset: AssetId,
950        /// TWAP ID.
951        #[serde(rename = "t")]
952        twap_id: u64,
953    },
954
955    /// No-operation to invalidate pending nonces.
956    #[serde(rename = "noop")]
957    Noop,
958}
959
960/// Exchange request envelope for the `/exchange` endpoint.
961///
962/// This is the top-level structure sent to Hyperliquid's exchange endpoint.
963/// It includes the action to perform along with authentication and metadata.
964#[derive(Debug, Clone, Serialize)]
965#[serde(rename_all = "camelCase")]
966pub struct HyperliquidExecRequest {
967    /// The exchange action to perform.
968    pub action: HyperliquidExecAction,
969    /// Request nonce for replay protection (milliseconds timestamp recommended).
970    pub nonce: u64,
971    /// ECC signature over the action and nonce.
972    pub signature: String,
973    /// Optional vault address for sub-account trading.
974    #[serde(skip_serializing_if = "Option::is_none")]
975    pub vault_address: Option<String>,
976    /// Optional expiration time in milliseconds.
977    /// Note: Using this field increases rate limit weight by 5x if the request expires.
978    #[serde(skip_serializing_if = "Option::is_none")]
979    pub expires_after: Option<u64>,
980}
981
982/// Exchange response envelope from the `/exchange` endpoint.
983#[derive(Debug, Clone, Serialize, Deserialize)]
984pub struct HyperliquidExecResponse {
985    /// Response status ("ok" for success).
986    pub status: String,
987    /// Response payload.
988    pub response: HyperliquidExecResponseData,
989}
990
991/// Response data containing the actual response payload from exchange endpoint.
992#[derive(Debug, Clone, Serialize, Deserialize)]
993#[serde(tag = "type")]
994pub enum HyperliquidExecResponseData {
995    /// Response for order actions.
996    #[serde(rename = "order")]
997    Order {
998        /// Order response data.
999        data: HyperliquidExecOrderResponseData,
1000    },
1001    /// Response for cancel actions.
1002    #[serde(rename = "cancel")]
1003    Cancel {
1004        /// Cancel response data.
1005        data: HyperliquidExecCancelResponseData,
1006    },
1007    /// Response for modify actions.
1008    #[serde(rename = "modify")]
1009    Modify {
1010        /// Modify response data.
1011        data: HyperliquidExecModifyResponseData,
1012    },
1013    /// Generic response for other actions.
1014    #[serde(rename = "default")]
1015    Default,
1016    /// Catch-all for unknown response types.
1017    #[serde(other)]
1018    Unknown,
1019}
1020
1021/// Order response data containing status for each order from exchange endpoint.
1022#[derive(Debug, Clone, Serialize, Deserialize)]
1023pub struct HyperliquidExecOrderResponseData {
1024    /// Status for each order in the request.
1025    pub statuses: Vec<HyperliquidExecOrderStatus>,
1026}
1027
1028/// Cancel response data containing status for each cancellation from exchange endpoint.
1029#[derive(Debug, Clone, Serialize, Deserialize)]
1030pub struct HyperliquidExecCancelResponseData {
1031    /// Status for each cancellation in the request.
1032    pub statuses: Vec<HyperliquidExecCancelStatus>,
1033}
1034
1035/// Modify response data containing status for each modification from exchange endpoint.
1036#[derive(Debug, Clone, Serialize, Deserialize)]
1037pub struct HyperliquidExecModifyResponseData {
1038    /// Status for each modification in the request.
1039    pub statuses: Vec<HyperliquidExecModifyStatus>,
1040}
1041
1042/// Status of an individual order submission via exchange endpoint.
1043#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1044#[serde(untagged)]
1045pub enum HyperliquidExecOrderStatus {
1046    /// Order is resting on the order book.
1047    Resting {
1048        /// Resting order information.
1049        resting: HyperliquidExecRestingInfo,
1050    },
1051    /// Order was filled immediately.
1052    Filled {
1053        /// Fill information.
1054        filled: HyperliquidExecFilledInfo,
1055    },
1056    /// Order submission failed.
1057    Error {
1058        /// Error message.
1059        error: String,
1060    },
1061}
1062
1063/// Information about a resting order via exchange endpoint.
1064#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1065pub struct HyperliquidExecRestingInfo {
1066    /// Order ID assigned by Hyperliquid.
1067    pub oid: OrderId,
1068}
1069
1070/// Information about a filled order via exchange endpoint.
1071#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1072pub struct HyperliquidExecFilledInfo {
1073    /// Total filled size.
1074    #[serde(
1075        rename = "totalSz",
1076        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1077        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1078    )]
1079    pub total_sz: Decimal,
1080    /// Average fill price.
1081    #[serde(
1082        rename = "avgPx",
1083        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1084        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1085    )]
1086    pub avg_px: Decimal,
1087    /// Order ID.
1088    pub oid: OrderId,
1089}
1090
1091/// Status of an individual order cancellation via exchange endpoint.
1092#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1093#[serde(untagged)]
1094pub enum HyperliquidExecCancelStatus {
1095    /// Cancellation succeeded.
1096    Success(String), // Usually "success"
1097    /// Cancellation failed.
1098    Error {
1099        /// Error message.
1100        error: String,
1101    },
1102}
1103
1104/// Status of an individual order modification via exchange endpoint.
1105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1106#[serde(untagged)]
1107pub enum HyperliquidExecModifyStatus {
1108    /// Modification succeeded.
1109    Success(String), // Usually "success"
1110    /// Modification failed.
1111    Error {
1112        /// Error message.
1113        error: String,
1114    },
1115}
1116
1117/// Complete clearinghouse state response from `POST /info` with `{ "type": "clearinghouseState", "user": "address" }`.
1118/// This provides account positions, margin information, and balances.
1119#[derive(Debug, Clone, Serialize, Deserialize)]
1120#[serde(rename_all = "camelCase")]
1121pub struct ClearinghouseState {
1122    /// List of asset positions (perpetual contracts).
1123    #[serde(default)]
1124    pub asset_positions: Vec<AssetPosition>,
1125    /// Cross margin summary information.
1126    #[serde(default)]
1127    pub cross_margin_summary: Option<CrossMarginSummary>,
1128    /// Withdrawable balance (top-level field).
1129    #[serde(
1130        default,
1131        serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1132        deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
1133    )]
1134    pub withdrawable: Option<Decimal>,
1135    /// Time of the state snapshot (milliseconds since epoch).
1136    #[serde(default)]
1137    pub time: Option<u64>,
1138}
1139
1140/// A single asset position in the clearinghouse state.
1141#[derive(Debug, Clone, Serialize, Deserialize)]
1142#[serde(rename_all = "camelCase")]
1143pub struct AssetPosition {
1144    /// Position information.
1145    pub position: PositionData,
1146    /// Type of position.
1147    #[serde(rename = "type")]
1148    pub position_type: HyperliquidPositionType,
1149}
1150
1151/// Leverage information for a position.
1152#[derive(Debug, Clone, Serialize, Deserialize)]
1153#[serde(rename_all = "camelCase")]
1154pub struct LeverageInfo {
1155    /// Leverage type (e.g., "cross", "isolated").
1156    #[serde(rename = "type")]
1157    pub leverage_type: String,
1158    /// Leverage value.
1159    pub value: u32,
1160}
1161
1162/// Cumulative funding breakdown for a position.
1163#[derive(Debug, Clone, Serialize, Deserialize)]
1164#[serde(rename_all = "camelCase")]
1165pub struct CumFundingInfo {
1166    /// All-time cumulative funding.
1167    #[serde(
1168        rename = "allTime",
1169        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1170        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1171    )]
1172    pub all_time: Decimal,
1173    /// Funding since position opened.
1174    #[serde(
1175        rename = "sinceOpen",
1176        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1177        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1178    )]
1179    pub since_open: Decimal,
1180    /// Funding since last position change.
1181    #[serde(
1182        rename = "sinceChange",
1183        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1184        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1185    )]
1186    pub since_change: Decimal,
1187}
1188
1189/// Detailed position data for an asset.
1190#[derive(Debug, Clone, Serialize, Deserialize)]
1191#[serde(rename_all = "camelCase")]
1192pub struct PositionData {
1193    /// Asset symbol/coin (e.g., "BTC").
1194    pub coin: String,
1195    /// Cumulative funding breakdown.
1196    #[serde(rename = "cumFunding")]
1197    pub cum_funding: CumFundingInfo,
1198    /// Entry price for the position.
1199    #[serde(
1200        rename = "entryPx",
1201        serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1202        deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str",
1203        default
1204    )]
1205    pub entry_px: Option<Decimal>,
1206    /// Leverage information for the position.
1207    pub leverage: LeverageInfo,
1208    /// Liquidation price.
1209    #[serde(
1210        rename = "liquidationPx",
1211        serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1212        deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str",
1213        default
1214    )]
1215    pub liquidation_px: Option<Decimal>,
1216    /// Margin used for this position.
1217    #[serde(
1218        rename = "marginUsed",
1219        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1220        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1221    )]
1222    pub margin_used: Decimal,
1223    /// Maximum leverage allowed for this asset.
1224    #[serde(rename = "maxLeverage", default)]
1225    pub max_leverage: Option<u32>,
1226    /// Position value.
1227    #[serde(
1228        rename = "positionValue",
1229        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1230        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1231    )]
1232    pub position_value: Decimal,
1233    /// Return on equity percentage.
1234    #[serde(
1235        rename = "returnOnEquity",
1236        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1237        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1238    )]
1239    pub return_on_equity: Decimal,
1240    /// Position size (positive for long, negative for short).
1241    #[serde(
1242        rename = "szi",
1243        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1244        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1245    )]
1246    pub szi: Decimal,
1247    /// Unrealized PnL.
1248    #[serde(
1249        rename = "unrealizedPnl",
1250        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1251        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1252    )]
1253    pub unrealized_pnl: Decimal,
1254}
1255
1256/// Cross margin summary information.
1257#[derive(Debug, Clone, Serialize, Deserialize)]
1258#[serde(rename_all = "camelCase")]
1259pub struct CrossMarginSummary {
1260    /// Account value in USD.
1261    #[serde(
1262        rename = "accountValue",
1263        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1264        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1265    )]
1266    pub account_value: Decimal,
1267    /// Total notional position value.
1268    #[serde(
1269        rename = "totalNtlPos",
1270        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1271        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1272    )]
1273    pub total_ntl_pos: Decimal,
1274    /// Total raw USD value (collateral).
1275    #[serde(
1276        rename = "totalRawUsd",
1277        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1278        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1279    )]
1280    pub total_raw_usd: Decimal,
1281    /// Total margin used across all positions.
1282    #[serde(
1283        rename = "totalMarginUsed",
1284        serialize_with = "crate::common::parse::serialize_decimal_as_str",
1285        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
1286    )]
1287    pub total_margin_used: Decimal,
1288    /// Withdrawable balance.
1289    #[serde(
1290        rename = "withdrawable",
1291        default,
1292        serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
1293        deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
1294    )]
1295    pub withdrawable: Option<Decimal>,
1296}