1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
//! Max gains of a position in terms of the quote asset.
use schemars::{
    schema::{InstanceType, SchemaObject},
    JsonSchema,
};

use crate::prelude::*;

/// String representation of positive infinity.
const POS_INF_STR: &str = "+Inf";

/// The max gains for a position.
///
/// Max gains are always specified by the user in terms of the quote currency.
///
/// Note that when opening long positions in collateral-is-base markets,
/// infinite max gains is possible. However, this is an error in the case of
/// short positions or collateral-is-quote markets.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub enum MaxGainsInQuote {
    /// Finite max gains
    Finite(NonZero<Decimal256>),
    /// Infinite max gains
    PosInfinity,
}

impl Display for MaxGainsInQuote {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            MaxGainsInQuote::Finite(val) => val.fmt(f),
            MaxGainsInQuote::PosInfinity => write!(f, "{}", POS_INF_STR),
        }
    }
}

impl FromStr for MaxGainsInQuote {
    type Err = PerpError;
    fn from_str(src: &str) -> Result<Self, PerpError> {
        match src {
            POS_INF_STR => Ok(MaxGainsInQuote::PosInfinity),
            _ => match src.parse() {
                Ok(number) => Ok(MaxGainsInQuote::Finite(number)),
                Err(err) => Err(PerpError::new(
                    ErrorId::Conversion,
                    ErrorDomain::Default,
                    format!("error converting {} to MaxGainsInQuote, {}", src, err),
                )),
            },
        }
    }
}

impl TryFrom<&str> for MaxGainsInQuote {
    type Error = anyhow::Error;

    fn try_from(val: &str) -> Result<Self, Self::Error> {
        Self::from_str(val).map_err(|err| err.into())
    }
}

impl serde::Serialize for MaxGainsInQuote {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match self {
            MaxGainsInQuote::Finite(number) => number.serialize(serializer),
            MaxGainsInQuote::PosInfinity => serializer.serialize_str(POS_INF_STR),
        }
    }
}

impl<'de> serde::Deserialize<'de> for MaxGainsInQuote {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        deserializer.deserialize_str(MaxGainsInQuoteVisitor)
    }
}

impl JsonSchema for MaxGainsInQuote {
    fn schema_name() -> String {
        "MaxGainsInQuote".to_owned()
    }

    fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
        SchemaObject {
            instance_type: Some(InstanceType::String.into()),
            format: Some("leverage".to_owned()),
            ..Default::default()
        }
        .into()
    }
}

struct MaxGainsInQuoteVisitor;

impl<'de> serde::de::Visitor<'de> for MaxGainsInQuoteVisitor {
    type Value = MaxGainsInQuote;

    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
        formatter.write_str("MaxGainsInQuote")
    }

    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        v.parse()
            .map_err(|_| E::custom(format!("Invalid MaxGainsInQuote: {v}")))
    }
}

impl MaxGainsInQuote {
    /// Calculate the needed counter collateral
    pub fn calculate_counter_collateral(
        self,
        market_type: MarketType,
        collateral: NonZero<Collateral>,
        notional_size_in_collateral: Signed<Collateral>,
        leverage_to_notional: SignedLeverageToNotional,
    ) -> Result<NonZero<Collateral>> {
        let direction_to_base = leverage_to_notional.direction().into_base(market_type);
        Ok(match market_type {
            MarketType::CollateralIsQuote => match self {
                MaxGainsInQuote::Finite(max_gains_in_collateral) => {
                    collateral.checked_mul_non_zero(max_gains_in_collateral)?
                }
                MaxGainsInQuote::PosInfinity => {
                    return Err(MarketError::InvalidInfiniteMaxGains {
                        market_type,
                        direction: direction_to_base,
                    }
                    .into_anyhow());
                }
            },
            MarketType::CollateralIsBase => {
                match self {
                    MaxGainsInQuote::PosInfinity => {
                        // In a Collateral-is-base market, infinite max gains are only allowed on
                        // short positions. This is because going short in this market type is betting
                        // on the asset going up (the equivalent of taking a long position in a
                        // Collateral-is-quote market). Note, the error message purposefully describes
                        // this as a "Long" position to keep things clear and consistent for the user.
                        if leverage_to_notional.direction() == DirectionToNotional::Long {
                            return Err(MarketError::InvalidInfiniteMaxGains {
                                market_type,
                                direction: direction_to_base,
                            }
                            .into_anyhow());
                        }

                        NonZero::new(notional_size_in_collateral.abs_unsigned())
                            .context("notional_size_in_collateral is zero")?
                    }
                    MaxGainsInQuote::Finite(max_gains_in_notional) => {
                        let max_gains_multiple = (Number::ONE
                            - (max_gains_in_notional.into_number() + Number::ONE)?
                                .checked_div(leverage_to_notional.into_number())?)?;

                        if max_gains_multiple.approx_lt_relaxed(Number::ZERO)? {
                            return Err(MarketError::MaxGainsTooLarge {}.into());
                        }

                        let counter_collateral = collateral
                            .into_number()
                            .checked_mul(max_gains_in_notional.into_number())?
                            .checked_div(max_gains_multiple)?;
                        NonZero::<Collateral>::try_from_number(counter_collateral).with_context(|| format!("Calculated an invalid counter_collateral: {counter_collateral}"))?
                    }
                }
            }
        })
    }
}