Skip to main content

df_hyperliquid_core/trade/margin/
leverage.rs

1//! `updateLeverage`: the leverage and margin mode of a perp position, main dex or HIP-3, checked
2//! against the perp's meta and margin table, or against a ticket's [`Market`], before anything is
3//! signed.
4
5use crate::markets::perps::meta::PerpAsset;
6use crate::trade::error::TradeError;
7use crate::trade::margin::tables::{LeverageError, MarginTable, checked_leverage};
8use crate::trade::ticket::{Market, Refusal};
9use crate::utils::sign::{L1Action, Map};
10use crate::utils::{Asset, Dec};
11
12/// How a position is margined, the same mode a position read back carries.
13pub use crate::account::holdings::MarginMode;
14
15/// `updateLeverage`: `{type, asset, isCross, leverage}`.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct UpdateLeverage {
18    asset: Asset,
19    mode: MarginMode,
20    leverage: u32,
21}
22
23impl UpdateLeverage {
24    /// A whole number, at least 1. What the asset allows (its `maxLeverage`, whether it takes cross)
25    /// is checked by [`crate::trade::checked_leverage`].
26    pub fn new(asset: Asset, mode: MarginMode, leverage: u32) -> Result<UpdateLeverage, TradeError> {
27        match leverage {
28            0 => Err(TradeError::NoLeverage),
29            leverage => Ok(UpdateLeverage { asset, mode, leverage }),
30        }
31    }
32
33    #[must_use]
34    pub const fn leverage(&self) -> u32 {
35        self.leverage
36    }
37
38    #[must_use]
39    pub const fn mode(&self) -> MarginMode {
40        self.mode
41    }
42}
43
44impl L1Action for UpdateLeverage {
45    fn action(&self) -> Map {
46        Map::new()
47            .put("type", "updateLeverage")
48            .put("asset", self.asset)
49            .put("isCross", matches!(self.mode, MarginMode::Cross))
50            .put("leverage", self.leverage)
51    }
52}
53
54/// `updateLeverage` for `perp` at `asset`: refused over its `maxLeverage`, and refused as cross
55/// where it is isolated only.
56pub fn update_leverage(
57    asset: Asset,
58    perp: &PerpAsset,
59    mode: MarginMode,
60    leverage: u32,
61) -> Result<UpdateLeverage, LeverageError> {
62    checked_leverage(asset, &perp.name, perp.max_leverage, perp.allows_cross(), mode, leverage)
63}
64
65/// [`update_leverage`], and held to the tier of `table` (the perp's, from
66/// [`crate::markets::perps::Meta::table_of`]) that a position of `notional` reaches: BTC takes 40x
67/// up to 150M and 20x past it.
68pub fn update_leverage_for(
69    asset: Asset,
70    perp: &PerpAsset,
71    table: &MarginTable,
72    mode: MarginMode,
73    leverage: u32,
74    notional: Dec,
75) -> Result<UpdateLeverage, LeverageError> {
76    let set = update_leverage(asset, perp, mode, leverage)?;
77    table.check(&perp.name, leverage, notional)?;
78    Ok(set)
79}
80
81/// `updateLeverage`, held to the market's ceiling and margin mode.
82pub fn leverage(market: &Market, mode: MarginMode, leverage: u32) -> Result<UpdateLeverage, Refusal> {
83    let Some(most) = market.max_leverage() else {
84        return Err(Refusal::Spot("leverage"));
85    };
86    // A ticket's refusals name no perp, so neither does the check.
87    checked_leverage(market.asset(), "", most, market.allows_cross(), mode, leverage).map_err(|error| match error {
88        LeverageError::TooHigh { .. }
89        | LeverageError::OverTier { .. }
90        | LeverageError::Trade(TradeError::NoLeverage) => Refusal::Leverage { asked: leverage, most },
91        LeverageError::NoCross(_) => Refusal::IsolatedOnly,
92        LeverageError::Trade(error) => Refusal::Trade(error),
93    })
94}
95
96#[cfg(test)]
97#[path = "tests/leverage.rs"]
98mod tests;