nautilus_hyperliquid/http/
models.rs

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