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}