levana_perpswap_cosmos/contracts/market/position/
closed.rs

1//! Represent closed positions.
2
3use crate::prelude::*;
4use std::fmt;
5use std::fmt::{Display, Formatter};
6
7use super::{LiquidationMargin, Position, PositionId, PositionQueryResponse};
8
9/// Information on a closed position
10#[cw_serde]
11pub struct ClosedPosition {
12    /// Owner at the time the position closed
13    pub owner: Addr,
14    /// ID of the position
15    pub id: PositionId,
16    /// Direction (to base) of the position
17    pub direction_to_base: DirectionToBase,
18    /// Timestamp the position was created, block time.
19    pub created_at: Timestamp,
20    /// Timestamp of the price point used for creating this position.
21    pub price_point_created_at: Option<Timestamp>,
22    /// Timestamp of the last liquifunding
23    pub liquifunded_at: Timestamp,
24
25    /// The one-time fee paid when opening or updating a position
26    ///
27    /// this value is the current balance, including all updates
28    pub trading_fee_collateral: Collateral,
29    /// Cumulative trading fees expressed in USD
30    pub trading_fee_usd: Usd,
31    /// The ongoing fee paid (and earned!) between positions
32    /// to incentivize keeping longs and shorts in balance
33    /// which in turn reduces risk for LPs
34    ///
35    /// This value is the current balance, not a historical record of each payment
36    pub funding_fee_collateral: Signed<Collateral>,
37    /// Cumulative funding fee in USD
38    pub funding_fee_usd: Signed<Usd>,
39    /// The ongoing fee paid to LPs to lock up their deposit
40    /// as counter-size collateral in this position
41    ///
42    /// This value is the current balance, not a historical record of each payment
43    pub borrow_fee_collateral: Collateral,
44    /// Cumulative borrow fee in USD
45    pub borrow_fee_usd: Usd,
46
47    /// Cumulative amount of crank fees paid by the position
48    pub crank_fee_collateral: Collateral,
49    /// Cumulative crank fees in USD
50    pub crank_fee_usd: Usd,
51
52    /// Cumulative amount of delta neutrality fees paid by (or received by) the position.
53    ///
54    /// Positive == outgoing, negative == incoming, like funding_fee.
55    pub delta_neutrality_fee_collateral: Signed<Collateral>,
56    /// Cumulative delta neutrality fee in USD
57    pub delta_neutrality_fee_usd: Signed<Usd>,
58
59    /// Deposit collateral for the position.
60    ///
61    /// This includes any updates from collateral being added or removed.
62    pub deposit_collateral: Signed<Collateral>,
63
64    /// Deposit collateral in USD, using cost basis analysis.
65    #[serde(default)]
66    pub deposit_collateral_usd: Signed<Usd>,
67
68    /// Final active collateral, the amount sent back to the trader on close
69    pub active_collateral: Collateral,
70
71    /// Profit or loss of the position in terms of collateral.
72    ///
73    /// This is the final collateral send to the trader minus all deposits (including updates).
74    pub pnl_collateral: Signed<Collateral>,
75
76    /// Profit or loss, in USD
77    ///
78    /// This is not simply the PnL in collateral converted to USD. It converts
79    /// each individual event to a USD representation using the historical
80    /// timestamp. This can be viewed as a _cost basis_ view of PnL.
81    pub pnl_usd: Signed<Usd>,
82
83    /// The notional size of the position at close.
84    pub notional_size: Signed<Notional>,
85
86    /// Entry price
87    pub entry_price_base: PriceBaseInQuote,
88
89    /// the time at which the position is actually closed
90    ///
91    /// This will always be the block time when the crank closed the position,
92    /// whether via liquidation, deferred execution of a ClosePosition call, or
93    /// liquifunding.
94    pub close_time: Timestamp,
95    /// needed for calculating final settlement amounts
96    /// if by user: same as close time
97    /// if by liquidation: first time position became liquidatable
98    pub settlement_time: Timestamp,
99
100    /// the reason the position is closed
101    pub reason: PositionCloseReason,
102
103    /// liquidation margin at the time of close
104    /// Optional for the sake of backwards-compatibility
105    pub liquidation_margin: Option<LiquidationMargin>,
106}
107
108/// Reason the position was closed
109#[cw_serde]
110#[derive(Eq, Copy)]
111pub enum PositionCloseReason {
112    /// Some kind of automated price trigger
113    Liquidated(LiquidationReason),
114    /// The trader directly chose to close the position
115    Direct,
116}
117
118impl Display for PositionCloseReason {
119    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
120        match self {
121            PositionCloseReason::Liquidated(reason) => write!(f, "{reason}"),
122            PositionCloseReason::Direct => f.write_str("Manual close"),
123        }
124    }
125}
126
127/// Reason why a position was liquidated
128#[cw_serde]
129#[derive(Eq, Copy)]
130pub enum LiquidationReason {
131    /// True liquidation: insufficient funds in active collateral.
132    Liquidated,
133    /// Maximum gains were achieved.
134    MaxGains,
135    /// Stop loss price override was triggered.
136    StopLoss,
137    /// Specifically take profit override, not max gains.
138    TakeProfit,
139}
140
141impl Display for LiquidationReason {
142    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
143        let str = match self {
144            LiquidationReason::Liquidated => "liquidated",
145            LiquidationReason::MaxGains => "max-gains",
146            LiquidationReason::StopLoss => "stop-loss",
147            LiquidationReason::TakeProfit => "take-profit",
148        };
149
150        write!(f, "{}", str)
151    }
152}
153
154/// Instructions to close a position.
155///
156/// Closing a position can occur for multiple reasons: explicit action by the
157/// trader, settling price exposure (meaning: you hit a liquidation or take
158/// profit), insufficient margin... the point of this data structure is to
159/// capture all the information needed by the close position actions to do final
160/// settlement on a position and move it to the closed position data structures.
161#[derive(Clone, Debug)]
162pub struct ClosePositionInstructions {
163    /// The position in its current state
164    pub pos: Position,
165    /// The capped exposure amount after taking liquidation margin into account.
166    ///
167    /// Positive value means a transfer from counter collateral to active
168    /// collateral. Negative means active to counter collateral. This is not
169    /// reflected in the position itself, since Position requires non-zero
170    /// active and counter collateral, and it's entirely possible we will
171    /// consume the entirety of one of those fields.
172    pub capped_exposure: Signed<Collateral>,
173    /// Additional losses that the trader experienced that cut into liquidation margin.
174    ///
175    /// If the trader
176    /// experienced max gains, then this value is 0. In the case where the trader
177    /// experienced a liquidation event and capped_exposure did not fully represent
178    /// losses due to liquidation margin, this value contains additional losses we would
179    /// like to take away from the trader after paying all pending fees.
180    pub additional_losses: Collateral,
181
182    /// The price point used for settling this position.
183    pub settlement_price: PricePoint,
184    /// See [ClosedPosition::reason]
185    pub reason: PositionCloseReason,
186
187    /// Did this occur because the position was closed during liquifunding?
188    pub closed_during_liquifunding: bool,
189}
190
191/// Outcome of operations which might require closing a position.
192///
193/// This can apply to liquifunding, settling price exposure, etc.
194#[must_use]
195#[derive(Debug)]
196#[allow(clippy::large_enum_variant)]
197pub enum MaybeClosedPosition {
198    /// The position stayed open, here's the current status
199    Open(Position),
200    /// We need to close the position
201    Close(ClosePositionInstructions),
202}
203
204impl From<MaybeClosedPosition> for Position {
205    fn from(maybe_closed: MaybeClosedPosition) -> Self {
206        match maybe_closed {
207            MaybeClosedPosition::Open(pos) => pos,
208            MaybeClosedPosition::Close(instructions) => instructions.pos,
209        }
210    }
211}
212
213/// Query response intermediate value on a position.
214///
215/// Positions which are open but need to be liquidated cannot be represented in
216/// a [PositionQueryResponse], since many of the calculated fields will be
217/// invalid. We use this data type to represent query responses for open
218/// positions.
219pub enum PositionOrPendingClose {
220    /// Position which should remain open.
221    Open(Box<PositionQueryResponse>),
222    /// The value stored here may change after actual close occurs due to pending payments.
223    PendingClose(Box<ClosedPosition>),
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn ser_de_position_close_reason() {
232        for value in [
233            PositionCloseReason::Direct,
234            PositionCloseReason::Liquidated(LiquidationReason::Liquidated),
235            PositionCloseReason::Liquidated(LiquidationReason::MaxGains),
236            PositionCloseReason::Liquidated(LiquidationReason::StopLoss),
237            PositionCloseReason::Liquidated(LiquidationReason::TakeProfit),
238        ] {
239            let json = serde_json::to_string(&value).unwrap();
240            let parsed = serde_json::from_str(&json).unwrap();
241            assert_eq!(value, parsed);
242        }
243    }
244}