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}