Skip to main content

df_hyperliquid_core/trade/
margin.rs

1//! Margin tables and the leverage a perp allows, checked before anything is signed. A perp names
2//! its table by `marginTableId`; an id under [`FLAT_TABLES_BELOW`] is a single tier at that
3//! leverage, never listed. The main dex and every HIP-3 dex read them the same way.
4
5use crate::trade::error::TradeError;
6use crate::trade::leverage::{MarginMode, UpdateLeverage};
7use crate::utils::{Asset, Dec};
8use serde::Deserialize;
9
10/// A table id under this is a flat table at that leverage.
11pub const FLAT_TABLES_BELOW: u32 = 50;
12
13#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct MarginTable {
16    /// `tiered 40x`; empty on some.
17    pub description: String,
18    /// Lowest first.
19    pub margin_tiers: Vec<MarginTier>,
20}
21
22/// From `lower_bound` of notional on, at most `max_leverage`.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct MarginTier {
26    /// In the dex's collateral.
27    pub lower_bound: Dec,
28    pub max_leverage: u32,
29}
30
31impl MarginTable {
32    /// One tier, from nothing on: what an id under [`FLAT_TABLES_BELOW`] is.
33    #[must_use]
34    pub fn flat(max_leverage: u32) -> MarginTable {
35        MarginTable {
36            description: String::new(),
37            margin_tiers: vec![MarginTier { lower_bound: Dec::ZERO, max_leverage }],
38        }
39    }
40
41    /// A table by its id: flat under [`FLAT_TABLES_BELOW`], else one of
42    /// `listed`, a meta's `marginTables`. No perp is on table 0.
43    #[must_use]
44    pub fn by_id(id: u32, listed: &[(u32, MarginTable)]) -> Option<MarginTable> {
45        match id {
46            0 => None,
47            id if id < FLAT_TABLES_BELOW => Some(MarginTable::flat(id)),
48            id => listed.iter().find(|(at, _)| *at == id).map(|(_, table)| table.clone()),
49        }
50    }
51
52    /// The most leverage a position of `notional` may take: the tier with the
53    /// highest lower bound at or under it. A short's notional counts as its
54    /// size. The docs do not say which tier a notional exactly on a bound
55    /// falls in; here it is the higher one, the bound being its lower.
56    #[must_use]
57    pub fn max_leverage_at(&self, notional: Dec) -> Option<u32> {
58        let notional = notional.abs();
59        self.margin_tiers
60            .iter()
61            .filter(|tier| tier.lower_bound <= notional)
62            .max_by_key(|tier| tier.lower_bound)
63            .map(|tier| tier.max_leverage)
64    }
65
66    /// `leverage` on the perp `name`, refused over what the tier a position
67    /// of `notional` reaches allows.
68    pub fn check(&self, name: &str, leverage: u32, notional: Dec) -> Result<(), LeverageError> {
69        match self.max_leverage_at(notional) {
70            Some(max) if leverage > max => {
71                Err(LeverageError::OverTier { name: name.to_owned(), notional: notional.abs(), max, asked: leverage })
72            }
73            _ => Ok(()),
74        }
75    }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
79pub enum LeverageError {
80    #[error("{name} allows at most {max}x, not {asked}x")]
81    TooHigh { name: String, max: u32, asked: u32 },
82    #[error("{name} allows at most {max}x at a notional of {notional}, not {asked}x")]
83    OverTier { name: String, notional: Dec, max: u32, asked: u32 },
84    #[error("{0} is isolated only")]
85    NoCross(String),
86    #[error(transparent)]
87    Trade(#[from] TradeError),
88}
89
90/// `updateLeverage` at `asset` for the perp `name`: refused over `max`, its `maxLeverage`, and
91/// refused as cross where `cross` is false, the perp being isolated only. Both come from the
92/// perp's meta.
93pub fn checked_leverage(
94    asset: Asset,
95    name: &str,
96    max: u32,
97    cross: bool,
98    mode: MarginMode,
99    leverage: u32,
100) -> Result<UpdateLeverage, LeverageError> {
101    if leverage > max {
102        return Err(LeverageError::TooHigh { name: name.to_owned(), max, asked: leverage });
103    }
104    if mode == MarginMode::Cross && !cross {
105        return Err(LeverageError::NoCross(name.to_owned()));
106    }
107    Ok(UpdateLeverage::new(asset, mode, leverage)?)
108}
109
110#[cfg(test)]
111#[path = "tests/margin.rs"]
112mod tests;