df_hyperliquid_core/account/
clearinghouse.rs1use serde::Deserialize;
6
7use crate::client::network::USDC_INDEX;
8use crate::markets::side::Side;
9use crate::utils::address::Address;
10use crate::utils::dec::{Dec, Round};
11
12const MARK_PLACES: u32 = 12;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum MarginMode {
20 Cross,
22 Isolated,
24}
25
26#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct Clearinghouse {
32 pub margin_summary: MarginSummary,
34 pub cross_margin_summary: MarginSummary,
36 pub cross_maintenance_margin_used: Dec,
37 pub withdrawable: Dec,
39 pub asset_positions: Vec<AssetPosition>,
40 #[serde(default)]
42 pub time: Option<u64>,
43}
44
45impl Clearinghouse {
46 pub fn positions(&self) -> impl Iterator<Item = &Position> {
48 self.asset_positions.iter().map(|entry| &entry.position).filter(|position| !position.szi.is_zero())
49 }
50
51 #[must_use]
53 pub fn position(&self, coin: &str) -> Option<&Position> {
54 self.positions().find(|position| position.coin == coin)
55 }
56
57 #[must_use]
59 pub fn holds_anything(&self) -> bool {
60 !self.margin_summary.account_value.is_zero() || self.positions().next().is_some()
61 }
62
63 #[must_use]
65 pub fn is_used(&self) -> bool {
66 self.holds_anything()
67 }
68}
69
70#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct MarginSummary {
74 pub account_value: Dec,
75 pub total_ntl_pos: Dec,
77 pub total_raw_usd: Dec,
80 pub total_margin_used: Dec,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
84pub struct AssetPosition {
85 #[serde(rename = "type")]
87 pub kind: String,
88 pub position: Position,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct Position {
95 pub coin: String,
97 pub szi: Dec,
99 pub leverage: Leverage,
100 #[serde(default)]
102 pub entry_px: Option<Dec>,
103 pub position_value: Dec,
105 pub unrealized_pnl: Dec,
106 #[serde(default)]
108 pub return_on_equity: Option<Dec>,
109 #[serde(default)]
111 pub liquidation_px: Option<Dec>,
112 pub margin_used: Dec,
113 pub max_leverage: u32,
115 #[serde(default)]
117 pub cum_funding: Option<CumFunding>,
118}
119
120impl Position {
121 #[must_use]
122 pub const fn is_long(&self) -> bool {
123 self.szi.is_positive()
124 }
125
126 #[must_use]
127 pub const fn is_short(&self) -> bool {
128 self.szi.is_negative()
129 }
130
131 #[must_use]
133 pub const fn size(&self) -> Dec {
134 self.szi.abs()
135 }
136
137 #[must_use]
140 pub const fn closing_side(&self) -> Option<Side> {
141 if self.szi.is_positive() {
142 Some(Side::Sell)
143 } else if self.szi.is_negative() {
144 Some(Side::Buy)
145 } else {
146 None
147 }
148 }
149
150 #[must_use]
152 pub fn mark(&self) -> Option<Dec> {
153 self.position_value.checked_div(self.size(), MARK_PLACES, Round::Nearest)
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
161#[serde(tag = "type", rename_all = "camelCase")]
162pub enum Leverage {
163 Cross {
164 value: u32,
165 },
166 Isolated {
169 value: u32,
170 #[serde(rename = "rawUsd")]
171 raw_usd: Dec,
172 },
173}
174
175impl Leverage {
176 #[must_use]
177 pub const fn value(&self) -> u32 {
178 match self {
179 Leverage::Cross { value } | Leverage::Isolated { value, .. } => *value,
180 }
181 }
182
183 #[must_use]
184 pub const fn mode(&self) -> MarginMode {
185 match self {
186 Leverage::Cross { .. } => MarginMode::Cross,
187 Leverage::Isolated { .. } => MarginMode::Isolated,
188 }
189 }
190
191 #[must_use]
193 pub const fn raw_usd(&self) -> Option<Dec> {
194 match self {
195 Leverage::Cross { .. } => None,
196 Leverage::Isolated { raw_usd, .. } => Some(*raw_usd),
197 }
198 }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
205#[serde(rename_all = "camelCase")]
206pub struct ActiveAssetData {
207 pub user: Address,
208 pub coin: String,
209 pub leverage: Leverage,
211 pub max_trade_szs: [Dec; 2],
213 pub available_to_trade: [Dec; 2],
215 pub mark_px: Dec,
216}
217
218impl ActiveAssetData {
219 #[must_use]
221 pub const fn max_size(&self, side: Side) -> Dec {
222 self.max_trade_szs[at(side)]
223 }
224
225 #[must_use]
227 pub const fn available(&self, side: Side) -> Dec {
228 self.available_to_trade[at(side)]
229 }
230}
231
232const fn at(side: Side) -> usize {
234 match side {
235 Side::Buy => 0,
236 Side::Sell => 1,
237 }
238}
239
240#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
242#[serde(rename_all = "camelCase")]
243pub struct CumFunding {
244 pub all_time: Dec,
246 pub since_open: Dec,
248 pub since_change: Dec,
250}
251
252#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
256#[serde(rename_all = "camelCase")]
257pub struct SpotState {
258 pub balances: Vec<SpotBalance>,
259 #[serde(default)]
262 pub portfolio_margin_enabled: Option<bool>,
263 #[serde(default)]
266 pub portfolio_margin_ratio: Option<Dec>,
267 #[serde(default)]
270 pub token_to_available_after_maintenance: Vec<(u32, Dec)>,
271}
272
273impl SpotState {
274 #[must_use]
275 pub fn balance(&self, token: u32) -> Option<&SpotBalance> {
276 self.balances.iter().find(|balance| balance.token == Some(token) && !balance.is_outcome())
277 }
278
279 #[must_use]
281 pub fn usdc(&self) -> Option<&SpotBalance> {
282 self.balance(USDC_INDEX)
283 }
284
285 pub fn held(&self) -> impl Iterator<Item = &SpotBalance> {
288 let mut held: Vec<&SpotBalance> = self.balances.iter().filter(|row| !row.total.is_zero()).collect();
289 held.sort_by(|a, b| b.total.cmp(&a.total).then_with(|| a.coin.cmp(&b.coin)));
290 held.into_iter()
291 }
292
293 #[must_use]
296 pub fn free(&self, token: u32) -> Dec {
297 self.balance(token).and_then(SpotBalance::available).unwrap_or(Dec::ZERO)
298 }
299
300 #[must_use]
302 pub fn available_after_maintenance(&self, token: u32) -> Option<Dec> {
303 self.token_to_available_after_maintenance.iter().find(|(held, _)| *held == token).map(|(_, free)| *free)
304 }
305}
306
307#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
309#[serde(rename_all = "camelCase")]
310pub struct SpotBalance {
311 pub coin: String,
312 #[serde(default)]
315 pub token: Option<u32>,
316 pub total: Dec,
317 pub hold: Dec,
321 pub entry_ntl: Dec,
323 #[serde(default)]
325 pub spot_hold: Option<Dec>,
326 #[serde(default)]
328 pub borrowed: Option<Dec>,
329 #[serde(default)]
331 pub supplied: Option<Dec>,
332 #[serde(default)]
334 pub ltv: Option<Dec>,
335}
336
337impl SpotBalance {
338 #[must_use]
341 pub fn available(&self) -> Option<Dec> {
342 let held = [Some(self.hold), self.spot_hold].into_iter().flatten().filter(|h| h.is_positive()).max();
343 let free = self.total.checked_sub(held.unwrap_or(Dec::ZERO))?;
344 Some(free.max(Dec::ZERO).min(self.total.max(Dec::ZERO)))
345 }
346
347 #[must_use]
350 pub fn is_outcome(&self) -> bool {
351 let coin = self.coin.as_str();
352 coin.starts_with(['+', '#'])
353 || coin.strip_prefix('o').is_some_and(|id| !id.is_empty() && id.bytes().all(|b| b.is_ascii_digit()))
354 }
355}