levana_perpswap_cosmos/
max_gains.rs

1//! Max gains of a position in terms of the quote asset.
2use schemars::{
3    schema::{InstanceType, SchemaObject},
4    JsonSchema,
5};
6
7use crate::prelude::*;
8
9/// String representation of positive infinity.
10const POS_INF_STR: &str = "+Inf";
11
12/// The max gains for a position.
13///
14/// Max gains are always specified by the user in terms of the quote currency.
15///
16/// Note that when opening long positions in collateral-is-base markets,
17/// infinite max gains is possible. However, this is an error in the case of
18/// short positions or collateral-is-quote markets.
19#[derive(Debug, Clone, Copy, Eq, PartialEq)]
20#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
21pub enum MaxGainsInQuote {
22    /// Finite max gains
23    Finite(NonZero<Decimal256>),
24    /// Infinite max gains
25    PosInfinity,
26}
27
28impl Display for MaxGainsInQuote {
29    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
30        match self {
31            MaxGainsInQuote::Finite(val) => val.fmt(f),
32            MaxGainsInQuote::PosInfinity => write!(f, "{}", POS_INF_STR),
33        }
34    }
35}
36
37impl FromStr for MaxGainsInQuote {
38    type Err = PerpError;
39    fn from_str(src: &str) -> Result<Self, PerpError> {
40        match src {
41            POS_INF_STR => Ok(MaxGainsInQuote::PosInfinity),
42            _ => match src.parse() {
43                Ok(number) => Ok(MaxGainsInQuote::Finite(number)),
44                Err(err) => Err(PerpError::new(
45                    ErrorId::Conversion,
46                    ErrorDomain::Default,
47                    format!("error converting {} to MaxGainsInQuote, {}", src, err),
48                )),
49            },
50        }
51    }
52}
53
54impl TryFrom<&str> for MaxGainsInQuote {
55    type Error = anyhow::Error;
56
57    fn try_from(val: &str) -> Result<Self, Self::Error> {
58        Self::from_str(val).map_err(|err| err.into())
59    }
60}
61
62impl serde::Serialize for MaxGainsInQuote {
63    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
64    where
65        S: serde::Serializer,
66    {
67        match self {
68            MaxGainsInQuote::Finite(number) => number.serialize(serializer),
69            MaxGainsInQuote::PosInfinity => serializer.serialize_str(POS_INF_STR),
70        }
71    }
72}
73
74impl<'de> serde::Deserialize<'de> for MaxGainsInQuote {
75    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
76    where
77        D: serde::Deserializer<'de>,
78    {
79        deserializer.deserialize_str(MaxGainsInQuoteVisitor)
80    }
81}
82
83impl JsonSchema for MaxGainsInQuote {
84    fn schema_name() -> String {
85        "MaxGainsInQuote".to_owned()
86    }
87
88    fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
89        SchemaObject {
90            instance_type: Some(InstanceType::String.into()),
91            format: Some("leverage".to_owned()),
92            ..Default::default()
93        }
94        .into()
95    }
96}
97
98struct MaxGainsInQuoteVisitor;
99
100impl<'de> serde::de::Visitor<'de> for MaxGainsInQuoteVisitor {
101    type Value = MaxGainsInQuote;
102
103    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
104        formatter.write_str("MaxGainsInQuote")
105    }
106
107    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
108    where
109        E: serde::de::Error,
110    {
111        v.parse()
112            .map_err(|_| E::custom(format!("Invalid MaxGainsInQuote: {v}")))
113    }
114}
115
116impl MaxGainsInQuote {
117    /// Calculate the needed counter collateral
118    pub fn calculate_counter_collateral(
119        self,
120        market_type: MarketType,
121        collateral: NonZero<Collateral>,
122        notional_size_in_collateral: Signed<Collateral>,
123        leverage_to_notional: SignedLeverageToNotional,
124    ) -> Result<NonZero<Collateral>> {
125        let direction_to_base = leverage_to_notional.direction().into_base(market_type);
126        Ok(match market_type {
127            MarketType::CollateralIsQuote => match self {
128                MaxGainsInQuote::Finite(max_gains_in_collateral) => {
129                    collateral.checked_mul_non_zero(max_gains_in_collateral)?
130                }
131                MaxGainsInQuote::PosInfinity => {
132                    return Err(MarketError::InvalidInfiniteMaxGains {
133                        market_type,
134                        direction: direction_to_base,
135                    }
136                    .into_anyhow());
137                }
138            },
139            MarketType::CollateralIsBase => {
140                match self {
141                    MaxGainsInQuote::PosInfinity => {
142                        // In a Collateral-is-base market, infinite max gains are only allowed on
143                        // short positions. This is because going short in this market type is betting
144                        // on the asset going up (the equivalent of taking a long position in a
145                        // Collateral-is-quote market). Note, the error message purposefully describes
146                        // this as a "Long" position to keep things clear and consistent for the user.
147                        if leverage_to_notional.direction() == DirectionToNotional::Long {
148                            return Err(MarketError::InvalidInfiniteMaxGains {
149                                market_type,
150                                direction: direction_to_base,
151                            }
152                            .into_anyhow());
153                        }
154
155                        NonZero::new(notional_size_in_collateral.abs_unsigned())
156                            .context("notional_size_in_collateral is zero")?
157                    }
158                    MaxGainsInQuote::Finite(max_gains_in_notional) => {
159                        let max_gains_multiple = (Number::ONE
160                            - (max_gains_in_notional.into_number() + Number::ONE)?
161                                .checked_div(leverage_to_notional.into_number())?)?;
162
163                        if max_gains_multiple.approx_lt_relaxed(Number::ZERO)? {
164                            return Err(MarketError::MaxGainsTooLarge {}.into());
165                        }
166
167                        let counter_collateral = collateral
168                            .into_number()
169                            .checked_mul(max_gains_in_notional.into_number())?
170                            .checked_div(max_gains_multiple)?;
171                        NonZero::<Collateral>::try_from_number(counter_collateral).with_context(|| format!("Calculated an invalid counter_collateral: {counter_collateral}"))?
172                    }
173                }
174            }
175        })
176    }
177}