levana_perpswap_cosmos/
leverage.rs

1//! Data types to represent leverage
2//!
3//! Within the perps platform, we have a few different varieties of leverage:
4//!
5//! - Does the leverage value include direction? Directioned leverage
6//!   uses negative values to represent shorts and positive to represent longs.
7//!   Undirectioned is the absolute leverage amount. We use the term "signed"
8//!   represent leverage types that include the direction.
9//!
10//! - Notional or base: leverage is given in terms of exposure to the base
11//!   asset. Within the protocol, for collateral-is-quote markets, the same
12//!   applies, since base and notional are the same asset. However, for
13//!   collateral-is-base, we have to convert the leverage in two ways: (1) flip
14//!   the direction from long to short or short to long, and (2) apply the
15//!   off-by-one factor to account for the exposure the trader experiences by
16//!   using the base asset as collateral.
17//!
18//! We end up with three different data types:
19//!
20//! * [LeverageToBase] is the the absolute leverage (without direction) from
21//!   the trader point of view in terms of exposure to the base asset.
22//!
23//! * [SignedLeverageToBase] is the trader perspective of leverage, but uses
24//!   negative values to represent shorts.
25//!
26//! * [SignedLeverageToNotional] is the protocol's perspective of leverage
27//!   including the sign.
28//!
29//! It's not necessary to provide a `LeverageToNotional`, since within the
30//! protocol we always use signed values. The unsigned version is only for
31//! trader/API convenience.
32//!
33//! To provide a worked example: suppose a trader wants to open a 5x leveraged
34//! short. If the market is collateral-is-quote, the [LeverageToBase] value
35//! would be `5`, [SignedLeverageToBase] would be `-5`, and
36//! [SignedLeverageToNotional] would also be `-5`.
37//!
38//! By contrast, if the market is collateral-is-base, the external values would
39//! remain the same, but [SignedLeverageToNotional] would be `6` from the formula
40//! `to_notional = 1 - to_base`.
41
42use crate::prelude::*;
43
44/// The absolute leverage for a position, in terms of the base asset.
45///
46/// Note that while leverage specified by the trader must be strictly positive
47/// (greater than 0), this type allows zero leverage to occur, since calculated
48/// leverage within the system based on the off-by-one exposure calculation may
49/// end up as 0.
50#[cw_serde]
51#[derive(Copy, Eq, PartialOrd, Ord)]
52pub struct LeverageToBase(Decimal256);
53
54impl LeverageToBase {
55    /// Get the raw underlying leverage value.
56    pub fn raw(self) -> Decimal256 {
57        self.0
58    }
59
60    /// Convert to an unsigned decimal.
61    pub fn into_decimal256(self) -> Decimal256 {
62        self.0
63    }
64
65    /// Convert to a signed decimal.
66    pub fn into_number(self) -> Signed<Decimal256> {
67        self.0.into_signed()
68    }
69
70    /// Convert into a [SignedLeverageToBase]
71    pub fn into_signed(self, direction: DirectionToBase) -> SignedLeverageToBase {
72        match direction {
73            DirectionToBase::Long => SignedLeverageToBase(self.0.into_signed()),
74            DirectionToBase::Short => SignedLeverageToBase(-self.0.into_signed()),
75        }
76    }
77}
78
79impl From<NonZero<Decimal256>> for LeverageToBase {
80    fn from(value: NonZero<Decimal256>) -> Self {
81        LeverageToBase(value.raw())
82    }
83}
84
85impl TryFrom<&str> for LeverageToBase {
86    type Error = anyhow::Error;
87
88    fn try_from(value: &str) -> Result<Self, Self::Error> {
89        Self::from_str(value)
90    }
91}
92
93impl FromStr for LeverageToBase {
94    type Err = anyhow::Error;
95
96    fn from_str(s: &str) -> Result<Self, Self::Err> {
97        s.parse()
98            .map(LeverageToBase)
99            .context("Invalid LeverageToBase")
100    }
101}
102
103impl Display for LeverageToBase {
104    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
105        self.0.fmt(f)
106    }
107}
108
109#[cfg(feature = "arbitrary")]
110impl<'a> arbitrary::Arbitrary<'a> for LeverageToBase {
111    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
112        Ok(Self(crate::number::arbitrary_decimal_256(u)?))
113    }
114}
115
116/// The user-specified leverage for a position, with direction expressed as the signed value
117///
118/// Leverage is always specified by the user in terms of the base currency. In a
119/// collateral-is-quote market, that directly becomes the exposure to notional.
120/// In a collateral-is-base market, we need to convert that exposure from
121/// collateral to notional for internal calculations.
122#[cw_serde]
123#[derive(Copy)]
124pub struct SignedLeverageToBase(Number);
125
126impl SignedLeverageToBase {
127    /// Get the leverage in terms of the notional currency.
128    ///
129    /// If the [MarketType] is [MarketType::CollateralIsQuote], the value is
130    /// already in terms of notional, and no change is needed. Otherwise, in a
131    /// [MarketType::CollateralIsBase], we have to convert from leverage in
132    /// terms of base/collateral into a notional value.
133    ///
134    /// The formula for converting is `leverage_to_notional = 1 -
135    /// leverage_to_base`. The motivation for that is:
136    ///
137    /// - Going long on notional is equivalent to going short on collateral and
138    ///   vice-versa, therefore we have a negative sign.
139    /// - By holding the collateral asset, the trader already has exposure to
140    ///   its price fluctuation, so we need to represent that by adding 1.
141    pub fn into_notional(self, market_type: MarketType) -> Result<SignedLeverageToNotional> {
142        Ok(SignedLeverageToNotional(match market_type {
143            MarketType::CollateralIsQuote => self.0,
144            MarketType::CollateralIsBase => (Number::ONE - self.0)?,
145        }))
146    }
147
148    /// Split up this value into the direction and absolute leverage.
149    pub fn split(self) -> (DirectionToBase, LeverageToBase) {
150        let (direction, leverage) = match self.0.try_into_non_negative_value() {
151            Some(x) => (DirectionToBase::Long, x),
152            None => (DirectionToBase::Short, self.0.abs_unsigned()),
153        };
154        (direction, LeverageToBase(leverage))
155    }
156
157    /// Multiply by active collateral of a position expressed in base
158    ///
159    /// This returns the position size from a trader perspective, aka the exposure to the base asset.
160    pub fn checked_mul_base(self, base: NonZero<Base>) -> Result<Signed<Base>> {
161        base.into_signed().checked_mul_number(self.0)
162    }
163}
164
165impl Display for SignedLeverageToBase {
166    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
167        self.0.fmt(f)
168    }
169}
170
171impl FromStr for SignedLeverageToBase {
172    type Err = anyhow::Error;
173
174    fn from_str(s: &str) -> Result<Self, Self::Err> {
175        s.parse().map(SignedLeverageToBase)
176    }
177}
178
179/// Leverage calculated based on the protocol's internal representation.
180///
181/// This is calculated by comparing the notional size of a position against some
182/// amount of collateral (either active collateral from the trader or counter
183/// collateral from the liquidity pool). One of these values needs to be
184/// converted using a [Price], so the leverage will change
185/// over time based on exchange rate.
186#[derive(Clone, Copy, Debug)]
187pub struct SignedLeverageToNotional(Signed<Decimal256>);
188
189impl From<Signed<Decimal256>> for SignedLeverageToNotional {
190    fn from(value: Signed<Decimal256>) -> Self {
191        SignedLeverageToNotional(value)
192    }
193}
194
195impl SignedLeverageToNotional {
196    /// Extract the direction value
197    pub fn direction(self) -> DirectionToNotional {
198        match self.0.try_into_non_negative_value() {
199            Some(_) => DirectionToNotional::Long,
200            None => DirectionToNotional::Short,
201        }
202    }
203
204    /// Calculate based on notional size, a price point, and some amount of collateral.
205    ///
206    /// Can fail because of overflow issues, but is otherwise guaranteed to
207    /// return a sensible value since the collateral is a non-zero value.
208    pub fn calculate(
209        notional_size: Signed<Notional>,
210        price_point: &PricePoint,
211        collateral: NonZero<Collateral>,
212    ) -> Self {
213        let notional_size_in_collateral =
214            notional_size.map(|x| price_point.notional_to_collateral(x));
215        SignedLeverageToNotional(notional_size_in_collateral.map(|x| x.div_non_zero(collateral)))
216    }
217
218    /// Convert into the raw value.
219    pub fn into_number(self) -> Signed<Decimal256> {
220        self.0
221    }
222
223    /// Convert into an [SignedLeverageToBase].
224    pub fn into_base(self, market_type: MarketType) -> Result<SignedLeverageToBase> {
225        Ok(SignedLeverageToBase(match market_type {
226            MarketType::CollateralIsQuote => self.0,
227            MarketType::CollateralIsBase => (Number::ONE - self.0)?,
228        }))
229    }
230
231    /// Multiply by active collateral of a position, returning the notional size in collateral of a position.
232    pub fn checked_mul_collateral(
233        self,
234        collateral: NonZero<Collateral>,
235    ) -> Result<Signed<Collateral>> {
236        collateral.into_signed().checked_mul_number(self.0)
237    }
238}