nautilus-hyperliquid 0.55.0

Hyperliquid integration adapter for the Nautilus trading engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
// -------------------------------------------------------------------------------------------------
//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
//  https://nautechsystems.io
//
//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
//  You may not use this file except in compliance with the License.
//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// -------------------------------------------------------------------------------------------------

use std::fmt::Display;

use alloy_primitives::{Address, keccak256};
use nautilus_model::identifiers::ClientOrderId;
use rust_decimal::Decimal;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use ustr::Ustr;

use crate::common::enums::{
    HyperliquidFillDirection, HyperliquidLeverageType,
    HyperliquidOrderStatus as HyperliquidOrderStatusEnum, HyperliquidPositionType, HyperliquidSide,
};

/// Response from candleSnapshot endpoint (returns array directly).
pub type HyperliquidCandleSnapshot = Vec<HyperliquidCandle>;

/// A 128-bit client order ID represented as a hex string with `0x` prefix.
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct Cloid(pub [u8; 16]);

impl Cloid {
    /// Creates a new `Cloid` from a hex string.
    ///
    /// # Errors
    ///
    /// Returns an error if the string is not a valid 128-bit hex with `0x` prefix.
    pub fn from_hex<S: AsRef<str>>(s: S) -> Result<Self, String> {
        let hex_str = s.as_ref();
        let without_prefix = hex_str
            .strip_prefix("0x")
            .ok_or("CLOID must start with '0x'")?;

        if without_prefix.len() != 32 {
            return Err("CLOID must be exactly 32 hex characters (128 bits)".to_string());
        }

        let mut bytes = [0u8; 16];
        for i in 0..16 {
            let byte_str = &without_prefix[i * 2..i * 2 + 2];
            bytes[i] = u8::from_str_radix(byte_str, 16)
                .map_err(|_| "Invalid hex character in CLOID".to_string())?;
        }

        Ok(Self(bytes))
    }

    /// Creates a `Cloid` from a Nautilus `ClientOrderId` by hashing it.
    ///
    /// Uses keccak256 hash and takes the first 16 bytes to create a deterministic
    /// 128-bit CLOID from any client order ID format.
    #[must_use]
    pub fn from_client_order_id(client_order_id: ClientOrderId) -> Self {
        let hash = keccak256(client_order_id.as_str().as_bytes());
        let mut bytes = [0u8; 16];
        bytes.copy_from_slice(&hash[..16]);
        Self(bytes)
    }

    /// Converts the CLOID to a hex string with `0x` prefix.
    pub fn to_hex(&self) -> String {
        let mut result = String::with_capacity(34);
        result.push_str("0x");
        for byte in &self.0 {
            result.push_str(&format!("{byte:02x}"));
        }
        result
    }
}

impl Display for Cloid {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.to_hex())
    }
}

impl Serialize for Cloid {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&self.to_hex())
    }
}

impl<'de> Deserialize<'de> for Cloid {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        Self::from_hex(&s).map_err(serde::de::Error::custom)
    }
}

/// Asset ID type for Hyperliquid.
///
/// For perpetuals, this is the index in `meta.universe`.
/// For spot trading, this is `10000 + index` from `spotMeta.universe`.
pub type AssetId = u32;

/// Order ID assigned by Hyperliquid.
pub type OrderId = u64;

/// Represents asset information from the meta endpoint.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HyperliquidAssetInfo {
    /// Asset name (e.g., "BTC").
    pub name: Ustr,
    /// Number of decimal places for size.
    pub sz_decimals: u32,
    /// Maximum leverage allowed for this asset.
    #[serde(default)]
    pub max_leverage: Option<u32>,
    /// Whether this asset requires isolated margin only.
    #[serde(default)]
    pub only_isolated: Option<bool>,
    /// Whether this asset is delisted/inactive.
    #[serde(default)]
    pub is_delisted: Option<bool>,
}

/// Complete perpetuals metadata response from `POST /info` with `{ "type": "meta" }`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PerpMeta {
    /// Perpetual assets universe.
    pub universe: Vec<PerpAsset>,
    /// Margin tables for leverage tiers.
    #[serde(default)]
    pub margin_tables: Vec<(u32, MarginTable)>,
}

/// A single perpetual asset from the universe.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PerpAsset {
    /// Asset name (e.g., "BTC", "xyz:TSLA" for HIP-3).
    pub name: String,
    /// Number of decimal places for size.
    pub sz_decimals: u32,
    /// Maximum leverage allowed for this asset.
    #[serde(default)]
    pub max_leverage: Option<u32>,
    /// Whether this asset requires isolated margin only.
    #[serde(default)]
    pub only_isolated: Option<bool>,
    /// Whether this asset is delisted/inactive.
    #[serde(default)]
    pub is_delisted: Option<bool>,
    /// HIP-3 growth mode status (e.g., "enabled").
    #[serde(default)]
    pub growth_mode: Option<String>,
    /// Margin mode (e.g., "strictIsolated").
    #[serde(default)]
    pub margin_mode: Option<String>,
}

/// Margin table with leverage tiers.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarginTable {
    /// Description of the margin table.
    pub description: String,
    /// Margin tiers for different position sizes.
    #[serde(default)]
    pub margin_tiers: Vec<MarginTier>,
}

/// Individual margin tier.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarginTier {
    /// Lower bound for this tier (as string to preserve precision).
    pub lower_bound: String,
    /// Maximum leverage for this tier.
    pub max_leverage: u32,
}

/// Complete spot metadata response from `POST /info` with `{ "type": "spotMeta" }`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SpotMeta {
    /// Spot tokens available.
    pub tokens: Vec<SpotToken>,
    /// Spot pairs universe.
    pub universe: Vec<SpotPair>,
}

/// EVM contract information for a spot token.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct EvmContract {
    /// EVM contract address (20 bytes).
    pub address: Address,
    /// Extra wei decimals for EVM precision (can be negative).
    pub evm_extra_wei_decimals: i32,
}

/// A single spot token from the tokens list.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SpotToken {
    /// Token name (e.g., "USDC").
    pub name: String,
    /// Number of decimal places for size.
    pub sz_decimals: u32,
    /// Wei decimals (on-chain precision).
    pub wei_decimals: u32,
    /// Token index used for pair references.
    pub index: u32,
    /// Token contract ID/address.
    pub token_id: String,
    /// Whether this is the canonical token.
    pub is_canonical: bool,
    /// Optional EVM contract information.
    #[serde(default)]
    pub evm_contract: Option<EvmContract>,
    /// Optional full name.
    #[serde(default)]
    pub full_name: Option<String>,
    /// Optional deployer trading fee share.
    #[serde(default)]
    pub deployer_trading_fee_share: Option<String>,
}

/// A single spot pair from the universe.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SpotPair {
    /// Pair display name (e.g., "PURR/USDC").
    pub name: String,
    /// Token indices [base_token_index, quote_token_index].
    pub tokens: [u32; 2],
    /// Pair index.
    pub index: u32,
    /// Whether this is the canonical pair.
    pub is_canonical: bool,
}

/// Optional perpetuals metadata with asset contexts from `{ "type": "metaAndAssetCtxs" }`.
/// Returns a tuple: `[PerpMeta, Vec<PerpAssetCtx>]`
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PerpMetaAndCtxs {
    /// Tuple format: [meta, contexts]
    Payload(Box<(PerpMeta, Vec<PerpAssetCtx>)>),
}

/// Runtime context for a perpetual asset (mark prices, funding, etc).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PerpAssetCtx {
    /// Mark price as string.
    #[serde(default)]
    pub mark_px: Option<String>,
    /// Mid price as string.
    #[serde(default)]
    pub mid_px: Option<String>,
    /// Funding rate as string.
    #[serde(default)]
    pub funding: Option<String>,
    /// Open interest as string.
    #[serde(default)]
    pub open_interest: Option<String>,
}

/// Optional spot metadata with asset contexts from `{ "type": "spotMetaAndAssetCtxs" }`.
/// Returns a tuple: `[SpotMeta, Vec<SpotAssetCtx>]`
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SpotMetaAndCtxs {
    /// Tuple format: [meta, contexts]
    Payload(Box<(SpotMeta, Vec<SpotAssetCtx>)>),
}

/// Runtime context for a spot pair (prices, volumes, etc).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SpotAssetCtx {
    /// Mark price as string.
    #[serde(default)]
    pub mark_px: Option<String>,
    /// Mid price as string.
    #[serde(default)]
    pub mid_px: Option<String>,
    /// 24h volume as string.
    #[serde(default)]
    pub day_volume: Option<String>,
}

/// Represents an L2 order book snapshot from `POST /info`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HyperliquidL2Book {
    /// Coin symbol.
    pub coin: Ustr,
    /// Order book levels: [bids, asks].
    pub levels: Vec<Vec<HyperliquidLevel>>,
    /// Timestamp in milliseconds.
    pub time: u64,
}

/// Represents an order book level with price and size.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HyperliquidLevel {
    /// Price level.
    pub px: String,
    /// Size at this level.
    pub sz: String,
}

/// Represents user fills response from `POST /info`.
///
/// The Hyperliquid API returns fills directly as an array, not wrapped in an object.
pub type HyperliquidFills = Vec<HyperliquidFill>;

/// Represents metadata about available markets from `POST /info`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HyperliquidMeta {
    #[serde(default)]
    pub universe: Vec<HyperliquidAssetInfo>,
}

/// Represents a single candle (OHLCV bar) from Hyperliquid.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HyperliquidCandle {
    /// Candle start timestamp in milliseconds.
    #[serde(rename = "t")]
    pub timestamp: u64,
    /// Candle end timestamp in milliseconds.
    #[serde(rename = "T")]
    pub end_timestamp: u64,
    /// Open price.
    #[serde(rename = "o")]
    pub open: String,
    /// High price.
    #[serde(rename = "h")]
    pub high: String,
    /// Low price.
    #[serde(rename = "l")]
    pub low: String,
    /// Close price.
    #[serde(rename = "c")]
    pub close: String,
    /// Volume.
    #[serde(rename = "v")]
    pub volume: String,
    /// Number of trades (optional).
    #[serde(rename = "n", default)]
    pub num_trades: Option<u64>,
}

/// Represents an individual fill from user fills.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HyperliquidFill {
    /// Coin symbol.
    pub coin: Ustr,
    /// Fill price.
    pub px: String,
    /// Fill size.
    pub sz: String,
    /// Order side (buy/sell).
    pub side: HyperliquidSide,
    /// Fill timestamp in milliseconds.
    pub time: u64,
    /// Position size before this fill.
    #[serde(rename = "startPosition")]
    pub start_position: String,
    /// Fill direction (open/close).
    pub dir: HyperliquidFillDirection,
    /// Closed P&L from this fill.
    #[serde(rename = "closedPnl")]
    pub closed_pnl: String,
    /// Hash reference.
    pub hash: String,
    /// Order ID that generated this fill.
    pub oid: u64,
    /// Crossed status.
    pub crossed: bool,
    /// Fee paid for this fill.
    pub fee: String,
    /// Token the fee was paid in (e.g. "USDC", "HYPE").
    #[serde(rename = "feeToken")]
    pub fee_token: Ustr,
}

/// Represents order status response from `POST /info`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HyperliquidOrderStatus {
    #[serde(default)]
    pub statuses: Vec<HyperliquidOrderStatusEntry>,
}

/// Represents an individual order status entry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HyperliquidOrderStatusEntry {
    /// Order information.
    pub order: HyperliquidOrderInfo,
    /// Current status.
    pub status: HyperliquidOrderStatusEnum,
    /// Status timestamp in milliseconds.
    #[serde(rename = "statusTimestamp")]
    pub status_timestamp: u64,
}

/// Represents order information within an order status entry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HyperliquidOrderInfo {
    /// Coin symbol.
    pub coin: Ustr,
    /// Order side (buy/sell).
    pub side: HyperliquidSide,
    /// Limit price.
    #[serde(rename = "limitPx")]
    pub limit_px: String,
    /// Order size.
    pub sz: String,
    /// Order ID.
    pub oid: u64,
    /// Order timestamp in milliseconds.
    pub timestamp: u64,
    /// Original order size.
    #[serde(rename = "origSz")]
    pub orig_sz: String,
}

/// ECC signature components for Hyperliquid exchange requests.
#[derive(Debug, Clone, Serialize)]
pub struct HyperliquidSignature {
    /// R component of the signature.
    pub r: String,
    /// S component of the signature.
    pub s: String,
    /// V component (recovery ID) of the signature.
    pub v: u64,
}

impl HyperliquidSignature {
    /// Parse a hex signature string (0x + 64 hex r + 64 hex s + 2 hex v) into components.
    pub fn from_hex(sig_hex: &str) -> Result<Self, String> {
        let sig_hex = sig_hex.strip_prefix("0x").unwrap_or(sig_hex);

        if sig_hex.len() != 130 {
            return Err(format!(
                "Invalid signature length: expected 130 hex chars, was {}",
                sig_hex.len()
            ));
        }

        let r = format!("0x{}", &sig_hex[0..64]);
        let s = format!("0x{}", &sig_hex[64..128]);
        let v = u64::from_str_radix(&sig_hex[128..130], 16)
            .map_err(|e| format!("Failed to parse v component: {e}"))?;

        Ok(Self { r, s, v })
    }
}

/// Represents an exchange action request wrapper for `POST /exchange`.
#[derive(Debug, Clone, Serialize)]
pub struct HyperliquidExchangeRequest<T> {
    /// The action to perform.
    #[serde(rename = "action")]
    pub action: T,
    /// Request nonce for replay protection.
    #[serde(rename = "nonce")]
    pub nonce: u64,
    /// ECC signature over the action.
    #[serde(rename = "signature")]
    pub signature: HyperliquidSignature,
    /// Optional vault address for sub-account trading.
    #[serde(rename = "vaultAddress", skip_serializing_if = "Option::is_none")]
    pub vault_address: Option<String>,
    /// Optional expiration time in milliseconds.
    #[serde(rename = "expiresAfter", skip_serializing_if = "Option::is_none")]
    pub expires_after: Option<u64>,
}

impl<T> HyperliquidExchangeRequest<T>
where
    T: Serialize,
{
    /// Create a new exchange request with the given action.
    pub fn new(action: T, nonce: u64, signature: &str) -> Result<Self, String> {
        Ok(Self {
            action,
            nonce,
            signature: HyperliquidSignature::from_hex(signature)?,
            vault_address: None,
            expires_after: None,
        })
    }

    /// Create a new exchange request with vault address for sub-account trading.
    pub fn with_vault(
        action: T,
        nonce: u64,
        signature: &str,
        vault_address: String,
    ) -> Result<Self, String> {
        Ok(Self {
            action,
            nonce,
            signature: HyperliquidSignature::from_hex(signature)?,
            vault_address: Some(vault_address),
            expires_after: None,
        })
    }

    /// Convert to JSON value for signing purposes.
    pub fn to_sign_value(&self) -> serde_json::Result<serde_json::Value> {
        serde_json::to_value(self)
    }
}

/// Represents an exchange response wrapper from `POST /exchange`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum HyperliquidExchangeResponse {
    /// Successful response with status.
    Status {
        /// Status message.
        status: String,
        /// Response payload.
        response: serde_json::Value,
    },
    /// Error response.
    Error {
        /// Error message.
        error: String,
    },
}

impl HyperliquidExchangeResponse {
    pub fn is_ok(&self) -> bool {
        matches!(self, Self::Status { status, .. } if status == RESPONSE_STATUS_OK)
    }
}

/// The success status string returned by the Hyperliquid exchange API.
pub const RESPONSE_STATUS_OK: &str = "ok";

#[cfg(test)]
mod tests {
    use rstest::rstest;

    use super::*;

    #[rstest]
    fn test_meta_deserialization() {
        let json = r#"{"universe": [{"name": "BTC", "szDecimals": 5}]}"#;

        let meta: HyperliquidMeta = serde_json::from_str(json).unwrap();

        assert_eq!(meta.universe.len(), 1);
        assert_eq!(meta.universe[0].name, "BTC");
        assert_eq!(meta.universe[0].sz_decimals, 5);
    }

    #[rstest]
    fn test_perp_asset_hip3_fields() {
        let json = r#"{
            "name": "xyz:TSLA",
            "szDecimals": 3,
            "maxLeverage": 10,
            "onlyIsolated": true,
            "growthMode": "enabled",
            "marginMode": "strictIsolated"
        }"#;

        let asset: PerpAsset = serde_json::from_str(json).unwrap();

        assert_eq!(asset.name, "xyz:TSLA");
        assert_eq!(asset.sz_decimals, 3);
        assert_eq!(asset.max_leverage, Some(10));
        assert_eq!(asset.only_isolated, Some(true));
        assert_eq!(asset.growth_mode.as_deref(), Some("enabled"));
        assert_eq!(asset.margin_mode.as_deref(), Some("strictIsolated"));
    }

    #[rstest]
    fn test_perp_asset_hip3_fields_absent() {
        let json = r#"{"name": "BTC", "szDecimals": 5}"#;

        let asset: PerpAsset = serde_json::from_str(json).unwrap();

        assert_eq!(asset.growth_mode, None);
        assert_eq!(asset.margin_mode, None);
    }

    #[rstest]
    fn test_l2_book_deserialization() {
        let json = r#"{"coin": "BTC", "levels": [[{"px": "50000", "sz": "1.5"}], [{"px": "50100", "sz": "2.0"}]], "time": 1234567890}"#;

        let book: HyperliquidL2Book = serde_json::from_str(json).unwrap();

        assert_eq!(book.coin, "BTC");
        assert_eq!(book.levels.len(), 2);
        assert_eq!(book.time, 1234567890);
    }

    #[rstest]
    fn test_exchange_response_deserialization() {
        let json = r#"{"status": "ok", "response": {"type": "order"}}"#;

        let response: HyperliquidExchangeResponse = serde_json::from_str(json).unwrap();
        assert!(response.is_ok());
    }

    #[rstest]
    fn test_msgpack_serialization_matches_python() {
        // Test that msgpack serialization includes the "type" tag properly.
        // Python SDK serializes: {"type": "order", "orders": [...], "grouping": "na"}
        // We need to verify rmp_serde::to_vec_named produces the same format.

        let action = HyperliquidExecAction::Order {
            orders: vec![],
            grouping: HyperliquidExecGrouping::Na,
            builder: None,
        };

        // First verify JSON is correct
        let json = serde_json::to_string(&action).unwrap();
        assert!(
            json.contains(r#""type":"order""#),
            "JSON should have type tag: {json}"
        );

        // Serialize with msgpack
        let msgpack_bytes = rmp_serde::to_vec_named(&action).unwrap();

        // Decode back to a generic Value to inspect the structure
        let decoded: serde_json::Value = rmp_serde::from_slice(&msgpack_bytes).unwrap();

        // The decoded value should have a "type" field
        assert!(
            decoded.get("type").is_some(),
            "MsgPack should have type tag. Decoded: {decoded:?}"
        );
        assert_eq!(
            decoded.get("type").unwrap().as_str().unwrap(),
            "order",
            "Type should be 'order'"
        );
        assert!(decoded.get("orders").is_some(), "Should have orders field");
        assert!(
            decoded.get("grouping").is_some(),
            "Should have grouping field"
        );
    }
}

/// Time-in-force for limit orders in exchange endpoint.
///
/// These values must match exactly what Hyperliquid expects for proper serialization.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HyperliquidExecTif {
    /// Add Liquidity Only (post-only order).
    #[serde(rename = "Alo")]
    Alo,
    /// Immediate or Cancel.
    #[serde(rename = "Ioc")]
    Ioc,
    /// Good Till Canceled.
    #[serde(rename = "Gtc")]
    Gtc,
}

/// Take profit or stop loss side for trigger orders in exchange endpoint.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HyperliquidExecTpSl {
    /// Take profit.
    #[serde(rename = "tp")]
    Tp,
    /// Stop loss.
    #[serde(rename = "sl")]
    Sl,
}

/// Order grouping strategy for linked TP/SL orders in exchange endpoint.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum HyperliquidExecGrouping {
    /// No grouping semantics.
    #[serde(rename = "na")]
    #[default]
    Na,
    /// Normal TP/SL grouping (linked orders).
    #[serde(rename = "normalTpsl")]
    NormalTpsl,
    /// Position-level TP/SL grouping.
    #[serde(rename = "positionTpsl")]
    PositionTpsl,
}

/// Order kind specification for the `t` field in exchange endpoint order requests.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum HyperliquidExecOrderKind {
    /// Limit order with time-in-force.
    Limit {
        /// Limit order parameters.
        limit: HyperliquidExecLimitParams,
    },
    /// Trigger order (stop/take profit).
    Trigger {
        /// Trigger order parameters.
        trigger: HyperliquidExecTriggerParams,
    },
}

/// Parameters for limit orders in exchange endpoint.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HyperliquidExecLimitParams {
    /// Time-in-force for the limit order.
    pub tif: HyperliquidExecTif,
}

/// Parameters for trigger orders (stop/take profit) in exchange endpoint.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HyperliquidExecTriggerParams {
    /// Whether to use market price when triggered.
    pub is_market: bool,
    /// Trigger price as a string.
    #[serde(
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub trigger_px: Decimal,
    /// Whether this is a take profit or stop loss.
    pub tpsl: HyperliquidExecTpSl,
}

/// Builder code for order attribution in the exchange endpoint.
///
/// The fee is specified in tenths of a basis point.
/// For example, `f: 10` represents 1 basis point (0.01%).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HyperliquidExecBuilderFee {
    /// Builder address for attribution.
    #[serde(rename = "b")]
    pub address: String,
    /// Fee in tenths of a basis point.
    #[serde(rename = "f")]
    pub fee_tenths_bp: u32,
}

/// Order specification for placing orders via exchange endpoint.
///
/// This struct represents a single order in the exact format expected
/// by the Hyperliquid exchange endpoint.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HyperliquidExecPlaceOrderRequest {
    /// Asset ID.
    #[serde(rename = "a")]
    pub asset: AssetId,
    /// Is buy order (true for buy, false for sell).
    #[serde(rename = "b")]
    pub is_buy: bool,
    /// Price as a string with no trailing zeros.
    #[serde(
        rename = "p",
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub price: Decimal,
    /// Size as a string with no trailing zeros.
    #[serde(
        rename = "s",
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub size: Decimal,
    /// Reduce-only flag.
    #[serde(rename = "r")]
    pub reduce_only: bool,
    /// Order type (limit or trigger).
    #[serde(rename = "t")]
    pub kind: HyperliquidExecOrderKind,
    /// Optional client order ID (128-bit hex).
    #[serde(rename = "c", skip_serializing_if = "Option::is_none")]
    pub cloid: Option<Cloid>,
}

/// Cancel specification for canceling orders by order ID via exchange endpoint.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HyperliquidExecCancelOrderRequest {
    /// Asset ID.
    #[serde(rename = "a")]
    pub asset: AssetId,
    /// Order ID to cancel.
    #[serde(rename = "o")]
    pub oid: OrderId,
}

/// Cancel specification for canceling orders by client order ID via exchange endpoint.
///
/// Note: Unlike order placement which uses abbreviated field names ("a", "c"),
/// cancel-by-cloid uses full field names ("asset", "cloid") per the Hyperliquid API.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HyperliquidExecCancelByCloidRequest {
    /// Asset ID.
    pub asset: AssetId,
    /// Client order ID to cancel.
    pub cloid: Cloid,
}

/// Modify specification for modifying existing orders via exchange endpoint.
///
/// The HL API requires the full order spec (same as a place order) plus
/// the venue order ID to modify.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HyperliquidExecModifyOrderRequest {
    /// Venue order ID to modify.
    pub oid: OrderId,
    /// Full replacement order specification.
    pub order: HyperliquidExecPlaceOrderRequest,
}

/// TWAP (Time-Weighted Average Price) order specification for exchange endpoint.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HyperliquidExecTwapRequest {
    /// Asset ID.
    #[serde(rename = "a")]
    pub asset: AssetId,
    /// Is buy order.
    #[serde(rename = "b")]
    pub is_buy: bool,
    /// Total size to execute.
    #[serde(
        rename = "s",
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub size: Decimal,
    /// Duration in milliseconds.
    #[serde(rename = "m")]
    pub duration_ms: u64,
}

/// All possible exchange actions for the Hyperliquid `/exchange` endpoint.
///
/// Each variant corresponds to a specific action type that can be performed
/// through the exchange API. The serialization uses the exact action type
/// names expected by Hyperliquid.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum HyperliquidExecAction {
    /// Place one or more orders.
    #[serde(rename = "order")]
    Order {
        /// List of orders to place.
        orders: Vec<HyperliquidExecPlaceOrderRequest>,
        /// Grouping strategy for TP/SL orders.
        #[serde(default)]
        grouping: HyperliquidExecGrouping,
        /// Optional builder code for attribution.
        #[serde(skip_serializing_if = "Option::is_none")]
        builder: Option<HyperliquidExecBuilderFee>,
    },

    /// Cancel orders by order ID.
    #[serde(rename = "cancel")]
    Cancel {
        /// Orders to cancel.
        cancels: Vec<HyperliquidExecCancelOrderRequest>,
    },

    /// Cancel orders by client order ID.
    #[serde(rename = "cancelByCloid")]
    CancelByCloid {
        /// Orders to cancel by CLOID.
        cancels: Vec<HyperliquidExecCancelByCloidRequest>,
    },

    /// Modify a single order.
    #[serde(rename = "modify")]
    Modify {
        /// Order modification specification.
        #[serde(flatten)]
        modify: HyperliquidExecModifyOrderRequest,
    },

    /// Modify multiple orders atomically.
    #[serde(rename = "batchModify")]
    BatchModify {
        /// Multiple order modifications.
        modifies: Vec<HyperliquidExecModifyOrderRequest>,
    },

    /// Schedule automatic order cancellation (dead man's switch).
    #[serde(rename = "scheduleCancel")]
    ScheduleCancel {
        /// Time in milliseconds when orders should be cancelled.
        /// If None, clears the existing schedule.
        #[serde(skip_serializing_if = "Option::is_none")]
        time: Option<u64>,
    },

    /// Update leverage for a position.
    #[serde(rename = "updateLeverage")]
    UpdateLeverage {
        /// Asset ID.
        #[serde(rename = "a")]
        asset: AssetId,
        /// Whether to use cross margin.
        #[serde(rename = "isCross")]
        is_cross: bool,
        /// Leverage value.
        #[serde(rename = "leverage")]
        leverage: u32,
    },

    /// Update isolated margin for a position.
    #[serde(rename = "updateIsolatedMargin")]
    UpdateIsolatedMargin {
        /// Asset ID.
        #[serde(rename = "a")]
        asset: AssetId,
        /// Margin delta as a string.
        #[serde(
            rename = "delta",
            serialize_with = "crate::common::parse::serialize_decimal_as_str",
            deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
        )]
        delta: Decimal,
    },

    /// Transfer USD between spot and perp accounts.
    #[serde(rename = "usdClassTransfer")]
    UsdClassTransfer {
        /// Source account type.
        from: String,
        /// Destination account type.
        to: String,
        /// Amount to transfer.
        #[serde(
            serialize_with = "crate::common::parse::serialize_decimal_as_str",
            deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
        )]
        amount: Decimal,
    },

    /// Place a TWAP order.
    #[serde(rename = "twapPlace")]
    TwapPlace {
        /// TWAP order specification.
        #[serde(flatten)]
        twap: HyperliquidExecTwapRequest,
    },

    /// Cancel a TWAP order.
    #[serde(rename = "twapCancel")]
    TwapCancel {
        /// Asset ID.
        #[serde(rename = "a")]
        asset: AssetId,
        /// TWAP ID.
        #[serde(rename = "t")]
        twap_id: u64,
    },

    /// No-operation to invalidate pending nonces.
    #[serde(rename = "noop")]
    Noop,
}

/// Exchange request envelope for the `/exchange` endpoint.
///
/// This is the top-level structure sent to Hyperliquid's exchange endpoint.
/// It includes the action to perform along with authentication and metadata.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HyperliquidExecRequest {
    /// The exchange action to perform.
    pub action: HyperliquidExecAction,
    /// Request nonce for replay protection (milliseconds timestamp recommended).
    pub nonce: u64,
    /// ECC signature over the action and nonce.
    pub signature: String,
    /// Optional vault address for sub-account trading.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vault_address: Option<String>,
    /// Optional expiration time in milliseconds.
    /// Note: Using this field increases rate limit weight by 5x if the request expires.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expires_after: Option<u64>,
}

/// Exchange response envelope from the `/exchange` endpoint.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HyperliquidExecResponse {
    /// Response status ("ok" for success).
    pub status: String,
    /// Response payload.
    pub response: HyperliquidExecResponseData,
}

/// Response data containing the actual response payload from exchange endpoint.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum HyperliquidExecResponseData {
    /// Response for order actions.
    #[serde(rename = "order")]
    Order {
        /// Order response data.
        data: HyperliquidExecOrderResponseData,
    },
    /// Response for cancel actions.
    #[serde(rename = "cancel")]
    Cancel {
        /// Cancel response data.
        data: HyperliquidExecCancelResponseData,
    },
    /// Response for modify actions.
    #[serde(rename = "modify")]
    Modify {
        /// Modify response data.
        data: HyperliquidExecModifyResponseData,
    },
    /// Generic response for other actions.
    #[serde(rename = "default")]
    Default,
    /// Catch-all for unknown response types.
    #[serde(other)]
    Unknown,
}

/// Order response data containing status for each order from exchange endpoint.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HyperliquidExecOrderResponseData {
    /// Status for each order in the request.
    pub statuses: Vec<HyperliquidExecOrderStatus>,
}

/// Cancel response data containing status for each cancellation from exchange endpoint.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HyperliquidExecCancelResponseData {
    /// Status for each cancellation in the request.
    pub statuses: Vec<HyperliquidExecCancelStatus>,
}

/// Modify response data containing status for each modification from exchange endpoint.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HyperliquidExecModifyResponseData {
    /// Status for each modification in the request.
    pub statuses: Vec<HyperliquidExecModifyStatus>,
}

/// Status of an individual order submission via exchange endpoint.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum HyperliquidExecOrderStatus {
    /// Order is resting on the order book.
    Resting {
        /// Resting order information.
        resting: HyperliquidExecRestingInfo,
    },
    /// Order was filled immediately.
    Filled {
        /// Fill information.
        filled: HyperliquidExecFilledInfo,
    },
    /// Order submission failed.
    Error {
        /// Error message.
        error: String,
    },
}

/// Information about a resting order via exchange endpoint.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HyperliquidExecRestingInfo {
    /// Order ID assigned by Hyperliquid.
    pub oid: OrderId,
}

/// Information about a filled order via exchange endpoint.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HyperliquidExecFilledInfo {
    /// Total filled size.
    #[serde(
        rename = "totalSz",
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub total_sz: Decimal,
    /// Average fill price.
    #[serde(
        rename = "avgPx",
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub avg_px: Decimal,
    /// Order ID.
    pub oid: OrderId,
}

/// Status of an individual order cancellation via exchange endpoint.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum HyperliquidExecCancelStatus {
    /// Cancellation succeeded.
    Success(String), // Usually "success"
    /// Cancellation failed.
    Error {
        /// Error message.
        error: String,
    },
}

/// Status of an individual order modification via exchange endpoint.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum HyperliquidExecModifyStatus {
    /// Modification succeeded.
    Success(String), // Usually "success"
    /// Modification failed.
    Error {
        /// Error message.
        error: String,
    },
}

/// Complete clearinghouse state response from `POST /info` with `{ "type": "clearinghouseState", "user": "address" }`.
/// This provides account positions, margin information, and balances.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClearinghouseState {
    /// List of asset positions (perpetual contracts).
    #[serde(default)]
    pub asset_positions: Vec<AssetPosition>,
    /// Cross margin summary information.
    #[serde(default)]
    pub cross_margin_summary: Option<CrossMarginSummary>,
    /// Withdrawable balance (top-level field).
    #[serde(
        default,
        serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
    )]
    pub withdrawable: Option<Decimal>,
    /// Time of the state snapshot (milliseconds since epoch).
    #[serde(default)]
    pub time: Option<u64>,
}

/// A single asset position in the clearinghouse state.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AssetPosition {
    /// Position information.
    pub position: PositionData,
    /// Type of position.
    #[serde(rename = "type")]
    pub position_type: HyperliquidPositionType,
}

/// Leverage information for a position.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LeverageInfo {
    #[serde(rename = "type")]
    pub leverage_type: HyperliquidLeverageType,
    /// Leverage value.
    pub value: u32,
}

/// Cumulative funding breakdown for a position.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CumFundingInfo {
    /// All-time cumulative funding.
    #[serde(
        rename = "allTime",
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub all_time: Decimal,
    /// Funding since position opened.
    #[serde(
        rename = "sinceOpen",
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub since_open: Decimal,
    /// Funding since last position change.
    #[serde(
        rename = "sinceChange",
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub since_change: Decimal,
}

/// Detailed position data for an asset.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PositionData {
    /// Asset symbol/coin (e.g., "BTC").
    pub coin: Ustr,
    /// Cumulative funding breakdown.
    #[serde(rename = "cumFunding")]
    pub cum_funding: CumFundingInfo,
    /// Entry price for the position.
    #[serde(
        rename = "entryPx",
        serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str",
        default
    )]
    pub entry_px: Option<Decimal>,
    /// Leverage information for the position.
    pub leverage: LeverageInfo,
    /// Liquidation price.
    #[serde(
        rename = "liquidationPx",
        serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str",
        default
    )]
    pub liquidation_px: Option<Decimal>,
    /// Margin used for this position.
    #[serde(
        rename = "marginUsed",
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub margin_used: Decimal,
    /// Maximum leverage allowed for this asset.
    #[serde(rename = "maxLeverage", default)]
    pub max_leverage: Option<u32>,
    /// Position value.
    #[serde(
        rename = "positionValue",
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub position_value: Decimal,
    /// Return on equity percentage.
    #[serde(
        rename = "returnOnEquity",
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub return_on_equity: Decimal,
    /// Position size (positive for long, negative for short).
    #[serde(
        rename = "szi",
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub szi: Decimal,
    /// Unrealized PnL.
    #[serde(
        rename = "unrealizedPnl",
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub unrealized_pnl: Decimal,
}

/// Cross margin summary information.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CrossMarginSummary {
    /// Account value in USD.
    #[serde(
        rename = "accountValue",
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub account_value: Decimal,
    /// Total notional position value.
    #[serde(
        rename = "totalNtlPos",
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub total_ntl_pos: Decimal,
    /// Total raw USD value (collateral).
    #[serde(
        rename = "totalRawUsd",
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub total_raw_usd: Decimal,
    /// Total margin used across all positions.
    #[serde(
        rename = "totalMarginUsed",
        serialize_with = "crate::common::parse::serialize_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_decimal_from_str"
    )]
    pub total_margin_used: Decimal,
    /// Withdrawable balance.
    #[serde(
        rename = "withdrawable",
        default,
        serialize_with = "crate::common::parse::serialize_optional_decimal_as_str",
        deserialize_with = "crate::common::parse::deserialize_optional_decimal_from_str"
    )]
    pub withdrawable: Option<Decimal>,
}