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 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
//! Represent closed positions.
use crate::prelude::*;
use std::fmt;
use std::fmt::{Display, Formatter};
use super::{LiquidationMargin, Position, PositionId, PositionQueryResponse};
/// Information on a closed position
#[cw_serde]
pub struct ClosedPosition {
/// Owner at the time the position closed
pub owner: Addr,
/// ID of the position
pub id: PositionId,
/// Direction (to base) of the position
pub direction_to_base: DirectionToBase,
/// Timestamp the position was created, block time.
pub created_at: Timestamp,
/// Timestamp of the price point used for creating this position.
pub price_point_created_at: Option<Timestamp>,
/// Timestamp of the last liquifunding
pub liquifunded_at: Timestamp,
/// The one-time fee paid when opening or updating a position
///
/// this value is the current balance, including all updates
pub trading_fee_collateral: Collateral,
/// Cumulative trading fees expressed in USD
pub trading_fee_usd: Usd,
/// The ongoing fee paid (and earned!) between positions
/// to incentivize keeping longs and shorts in balance
/// which in turn reduces risk for LPs
///
/// This value is the current balance, not a historical record of each payment
pub funding_fee_collateral: Signed<Collateral>,
/// Cumulative funding fee in USD
pub funding_fee_usd: Signed<Usd>,
/// The ongoing fee paid to LPs to lock up their deposit
/// as counter-size collateral in this position
///
/// This value is the current balance, not a historical record of each payment
pub borrow_fee_collateral: Collateral,
/// Cumulative borrow fee in USD
pub borrow_fee_usd: Usd,
/// Cumulative amount of crank fees paid by the position
pub crank_fee_collateral: Collateral,
/// Cumulative crank fees in USD
pub crank_fee_usd: Usd,
/// Cumulative amount of delta neutrality fees paid by (or received by) the position.
///
/// Positive == outgoing, negative == incoming, like funding_fee.
pub delta_neutrality_fee_collateral: Signed<Collateral>,
/// Cumulative delta neutrality fee in USD
pub delta_neutrality_fee_usd: Signed<Usd>,
/// Deposit collateral for the position.
///
/// This includes any updates from collateral being added or removed.
pub deposit_collateral: Signed<Collateral>,
/// Deposit collateral in USD, using cost basis analysis.
#[serde(default)]
pub deposit_collateral_usd: Signed<Usd>,
/// Final active collateral, the amount sent back to the trader on close
pub active_collateral: Collateral,
/// Profit or loss of the position in terms of collateral.
///
/// This is the final collateral send to the trader minus all deposits (including updates).
pub pnl_collateral: Signed<Collateral>,
/// Profit or loss, in USD
///
/// This is not simply the PnL in collateral converted to USD. It converts
/// each individual event to a USD representation using the historical
/// timestamp. This can be viewed as a _cost basis_ view of PnL.
pub pnl_usd: Signed<Usd>,
/// The notional size of the position at close.
pub notional_size: Signed<Notional>,
/// Entry price
pub entry_price_base: PriceBaseInQuote,
/// the time at which the position is actually closed
///
/// This will always be the block time when the crank closed the position,
/// whether via liquidation, deferred execution of a ClosePosition call, or
/// liquifunding.
pub close_time: Timestamp,
/// needed for calculating final settlement amounts
/// if by user: same as close time
/// if by liquidation: first time position became liquidatable
pub settlement_time: Timestamp,
/// the reason the position is closed
pub reason: PositionCloseReason,
/// liquidation margin at the time of close
/// Optional for the sake of backwards-compatibility
pub liquidation_margin: Option<LiquidationMargin>,
}
/// Reason the position was closed
#[cw_serde]
#[derive(Eq, Copy)]
pub enum PositionCloseReason {
/// Some kind of automated price trigger
Liquidated(LiquidationReason),
/// The trader directly chose to close the position
Direct,
}
impl Display for PositionCloseReason {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
PositionCloseReason::Liquidated(reason) => write!(f, "{reason}"),
PositionCloseReason::Direct => f.write_str("Manual close"),
}
}
}
/// Reason why a position was liquidated
#[cw_serde]
#[derive(Eq, Copy)]
pub enum LiquidationReason {
/// True liquidation: insufficient funds in active collateral.
Liquidated,
/// Maximum gains were achieved.
MaxGains,
/// Stop loss price override was triggered.
StopLoss,
/// Specifically take profit override, not max gains.
TakeProfit,
}
impl Display for LiquidationReason {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let str = match self {
LiquidationReason::Liquidated => "liquidated",
LiquidationReason::MaxGains => "max-gains",
LiquidationReason::StopLoss => "stop-loss",
LiquidationReason::TakeProfit => "take-profit",
};
write!(f, "{}", str)
}
}
/// Instructions to close a position.
///
/// Closing a position can occur for multiple reasons: explicit action by the
/// trader, settling price exposure (meaning: you hit a liquidation or take
/// profit), insufficient margin... the point of this data structure is to
/// capture all the information needed by the close position actions to do final
/// settlement on a position and move it to the closed position data structures.
#[derive(Clone, Debug)]
pub struct ClosePositionInstructions {
/// The position in its current state
pub pos: Position,
/// The capped exposure amount after taking liquidation margin into account.
///
/// Positive value means a transfer from counter collateral to active
/// collateral. Negative means active to counter collateral. This is not
/// reflected in the position itself, since Position requires non-zero
/// active and counter collateral, and it's entirely possible we will
/// consume the entirety of one of those fields.
pub capped_exposure: Signed<Collateral>,
/// Additional losses that the trader experienced that cut into liquidation margin.
///
/// If the trader
/// experienced max gains, then this value is 0. In the case where the trader
/// experienced a liquidation event and capped_exposure did not fully represent
/// losses due to liquidation margin, this value contains additional losses we would
/// like to take away from the trader after paying all pending fees.
pub additional_losses: Collateral,
/// The price point used for settling this position.
pub settlement_price: PricePoint,
/// See [ClosedPosition::reason]
pub reason: PositionCloseReason,
/// Did this occur because the position was closed during liquifunding?
pub closed_during_liquifunding: bool,
}
/// Outcome of operations which might require closing a position.
///
/// This can apply to liquifunding, settling price exposure, etc.
#[must_use]
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum MaybeClosedPosition {
/// The position stayed open, here's the current status
Open(Position),
/// We need to close the position
Close(ClosePositionInstructions),
}
impl From<MaybeClosedPosition> for Position {
fn from(maybe_closed: MaybeClosedPosition) -> Self {
match maybe_closed {
MaybeClosedPosition::Open(pos) => pos,
MaybeClosedPosition::Close(instructions) => instructions.pos,
}
}
}
/// Query response intermediate value on a position.
///
/// Positions which are open but need to be liquidated cannot be represented in
/// a [PositionQueryResponse], since many of the calculated fields will be
/// invalid. We use this data type to represent query responses for open
/// positions.
pub enum PositionOrPendingClose {
/// Position which should remain open.
Open(Box<PositionQueryResponse>),
/// The value stored here may change after actual close occurs due to pending payments.
PendingClose(Box<ClosedPosition>),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ser_de_position_close_reason() {
for value in [
PositionCloseReason::Direct,
PositionCloseReason::Liquidated(LiquidationReason::Liquidated),
PositionCloseReason::Liquidated(LiquidationReason::MaxGains),
PositionCloseReason::Liquidated(LiquidationReason::StopLoss),
PositionCloseReason::Liquidated(LiquidationReason::TakeProfit),
] {
let json = serde_json::to_string(&value).unwrap();
let parsed = serde_json::from_str(&json).unwrap();
assert_eq!(value, parsed);
}
}
}