levana_perpswap_cosmos/contracts/market/
position.rs

1//! Data structures and events for positions
2mod closed;
3mod collateral_and_usd;
4
5pub use closed::*;
6pub use collateral_and_usd::*;
7
8use crate::prelude::*;
9use anyhow::Result;
10use cosmwasm_schema::cw_serde;
11use cosmwasm_std::{Addr, Decimal256, OverflowError, StdResult};
12use cw_storage_plus::{IntKey, Key, KeyDeserialize, Prefixer, PrimaryKey};
13use std::fmt;
14use std::hash::Hash;
15use std::num::ParseIntError;
16use std::str::FromStr;
17
18use super::config::Config;
19
20/// The position itself
21#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
22pub struct Position {
23    /// Owner of the position
24    pub owner: Addr,
25    /// Unique identifier for a position
26    pub id: PositionId,
27    /// The amount of collateral deposited by the trader to create this position.
28    ///
29    /// It would seem like the type here should be `NonZero<Collateral>`.
30    /// However, due to updates, this isn't accurate. It's possible for someone
31    /// to update a position and withdraw more collateral than the original
32    /// deposit.
33    pub deposit_collateral: SignedCollateralAndUsd,
34    /// Active collateral for the position
35    ///
36    /// As a position stays open, we liquifund to realize price exposure and
37    /// take fees. This is the current trader-side collateral after those steps.
38    pub active_collateral: NonZero<Collateral>,
39    /// Collateral owned by the liquidity pool that is locked in this position.
40    pub counter_collateral: NonZero<Collateral>,
41    /// This is signed, where negative represents a short and positive is a long
42    pub notional_size: Signed<Notional>,
43    /// When the position was created, in terms of block time.
44    pub created_at: Timestamp,
45    /// Price point timestamp of the crank that created this position.
46    ///
47    /// This field is only used since deferred execution, before that it is `None`.
48    pub price_point_created_at: Option<Timestamp>,
49    /// The one-time fee paid when opening or updating a position
50    ///
51    /// this value is the current balance, including all updates
52    pub trading_fee: CollateralAndUsd,
53    /// The ongoing fee paid (and earned!) between positions
54    /// to incentivize keeping longs and shorts in balance
55    /// which in turn reduces risk for LPs
56    ///
57    /// This value is the current balance, not a historical record of each payment
58    pub funding_fee: SignedCollateralAndUsd,
59    /// The ongoing fee paid to LPs to lock up their deposit
60    /// as counter-size collateral in this position
61    ///
62    /// This value is the current balance, not a historical record of each payment
63    pub borrow_fee: CollateralAndUsd,
64
65    /// Total crank fees paid
66    pub crank_fee: CollateralAndUsd,
67
68    /// Cumulative amount of delta neutrality fees paid by (or received by) the position.
69    ///
70    /// Positive == outgoing, negative == incoming, like funding_fee.
71    pub delta_neutrality_fee: SignedCollateralAndUsd,
72
73    /// Last time the position was liquifunded.
74    ///
75    /// For newly opened positions, this is the same as the creation time.
76    pub liquifunded_at: Timestamp,
77    /// When is our next scheduled liquifunding?
78    ///
79    /// The crank will automatically liquifund this position once this timestamp
80    /// has passed. Additionally, liquifunding may be triggered by updating the
81    /// position.
82    pub next_liquifunding: Timestamp,
83    /// A trader specified price at which the position will be liquidated
84    pub stop_loss_override: Option<PriceBaseInQuote>,
85    /// Stored separately to ensure there are no rounding errors, since we need precise binary equivalence for lookups.
86    pub stop_loss_override_notional: Option<Price>,
87    /// The most recently calculated liquidation price
88    pub liquidation_price: Option<Price>,
89    /// The amount of liquidation margin set aside
90    pub liquidation_margin: LiquidationMargin,
91    /// The take profit value set by the trader in a message.
92    /// For historical reasons, this value can be optional if the user provided a max gains price.
93    #[serde(rename = "take_profit_override")]
94    pub take_profit_trader: Option<TakeProfitTrader>,
95    /// Derived directly from `take_profit_trader` to get the PriceNotionalInCollateral representation.
96    /// This will be `None` if `take_profit_trader` is infinite.
97    /// For historical reasons, this value will also be `None` if the user provided a max gains price.
98    /// Stored separately to ensure there are no rounding errors, since we need precise binary equivalence for lookups.
99    #[serde(rename = "take_profit_override_notional")]
100    pub take_profit_trader_notional: Option<Price>,
101    /// The most recently calculated price at which the trader will achieve maximum gains and take all counter collateral.
102    /// This is the notional price, not the base price, to avoid rounding errors
103    #[serde(rename = "take_profit_price")]
104    pub take_profit_total: Option<Price>,
105}
106
107/// Liquidation margin for a position, broken down by component.
108///
109/// Each field represents how much collateral has been set aside for the given
110/// fees, or the maximum amount the position can pay at liquifunding.
111#[cw_serde]
112#[derive(Default, Copy, Eq)]
113pub struct LiquidationMargin {
114    /// Maximum borrow fee payment.
115    pub borrow: Collateral,
116    /// Maximum funding payment.
117    pub funding: Collateral,
118    /// Maximum delta neutrality fee.
119    pub delta_neutrality: Collateral,
120    /// Funds set aside for a single crank fee.
121    pub crank: Collateral,
122    /// Funds set aside to cover additional price exposure losses from sparse price updates.
123    #[serde(default)]
124    pub exposure: Collateral,
125}
126
127impl LiquidationMargin {
128    /// Total value of the liquidation margin fields
129    pub fn total(&self) -> Result<Collateral, OverflowError> {
130        ((self.borrow + self.funding)? + (self.delta_neutrality + self.crank)?)? + self.exposure
131    }
132}
133
134/// Response from [crate::contracts::market::entry::QueryMsg::Positions]
135#[cw_serde]
136pub struct PositionsResp {
137    /// Open positions
138    pub positions: Vec<PositionQueryResponse>,
139    /// Positions which are pending a liquidation/take profit
140    ///
141    /// The closed position information is not the final version of the data,
142    /// the close process itself still needs to make final payments.
143    pub pending_close: Vec<ClosedPosition>,
144    /// Positions which have already been closed.
145    pub closed: Vec<ClosedPosition>,
146}
147
148/// Query response representing current state of a position
149#[cw_serde]
150pub struct PositionQueryResponse {
151    /// Owner
152    pub owner: Addr,
153    /// Unique ID
154    pub id: PositionId,
155    /// Direction
156    pub direction_to_base: DirectionToBase,
157    /// Current leverage
158    ///
159    /// This is impacted by fees and price exposure
160    pub leverage: LeverageToBase,
161    /// Leverage of the counter collateral
162    pub counter_leverage: LeverageToBase,
163    /// When the position was opened, block time
164    pub created_at: Timestamp,
165    /// Price point used for creating this position
166    pub price_point_created_at: Option<Timestamp>,
167    /// When the position was last liquifunded
168    pub liquifunded_at: Timestamp,
169
170    /// The one-time fee paid when opening or updating a position
171    ///
172    /// This value is the current balance, including all updates
173    pub trading_fee_collateral: Collateral,
174    /// USD expression of [Self::trading_fee_collateral] using cost-basis calculation.
175    pub trading_fee_usd: Usd,
176    /// The ongoing fee paid (and earned!) between positions
177    /// to incentivize keeping longs and shorts in balance
178    /// which in turn reduces risk for LPs
179    ///
180    /// This value is the current balance, not a historical record of each payment
181    pub funding_fee_collateral: Signed<Collateral>,
182    /// USD expression of [Self::funding_fee_collateral] using cost-basis calculation.
183    pub funding_fee_usd: Signed<Usd>,
184    /// The ongoing fee paid to LPs to lock up their deposit
185    /// as counter-size collateral in this position
186    ///
187    /// This value is the current balance, not a historical record of each payment
188    pub borrow_fee_collateral: Collateral,
189    /// USD expression of [Self::borrow_fee_collateral] using cost-basis calculation.
190    pub borrow_fee_usd: Usd,
191
192    /// Cumulative amount of crank fees paid by the position
193    pub crank_fee_collateral: Collateral,
194    /// USD expression of [Self::crank_fee_collateral] using cost-basis calculation.
195    pub crank_fee_usd: Usd,
196
197    /// Aggregate delta neutrality fees paid or received through position opens and upates.
198    pub delta_neutrality_fee_collateral: Signed<Collateral>,
199    /// USD expression of [Self::delta_neutrality_fee_collateral] using cost-basis calculation.
200    pub delta_neutrality_fee_usd: Signed<Usd>,
201
202    /// See [Position::deposit_collateral]
203    pub deposit_collateral: Signed<Collateral>,
204    /// USD expression of [Self::deposit_collateral] using cost-basis calculation.
205    pub deposit_collateral_usd: Signed<Usd>,
206    /// See [Position::active_collateral]
207    pub active_collateral: NonZero<Collateral>,
208    /// [Self::active_collateral] converted to USD at the current exchange rate
209    pub active_collateral_usd: NonZero<Usd>,
210    /// See [Position::counter_collateral]
211    pub counter_collateral: NonZero<Collateral>,
212
213    /// Unrealized PnL on this position, in terms of collateral.
214    pub pnl_collateral: Signed<Collateral>,
215    /// Unrealized PnL on this position, in USD, using cost-basis analysis.
216    pub pnl_usd: Signed<Usd>,
217
218    /// DNF that would be charged (positive) or received (negative) if position was closed now.
219    pub dnf_on_close_collateral: Signed<Collateral>,
220
221    /// Notional size of the position
222    pub notional_size: Signed<Notional>,
223    /// Notional size converted to collateral at the current price
224    pub notional_size_in_collateral: Signed<Collateral>,
225
226    /// The size of the position in terms of the base asset.
227    ///
228    /// Note that this is not a simple conversion from notional size. Instead,
229    /// this needs to account for the off-by-one leverage that occurs in
230    /// collateral-is-base markets.
231    pub position_size_base: Signed<Base>,
232
233    /// Convert [Self::position_size_base] into USD at the current exchange rate.
234    pub position_size_usd: Signed<Usd>,
235
236    /// Price at which liquidation will occur
237    pub liquidation_price_base: Option<PriceBaseInQuote>,
238    /// The liquidation margin set aside on this position
239    pub liquidation_margin: LiquidationMargin,
240
241    /// Maximum gains, in terms of quote, the trader can achieve
242    #[deprecated(note = "Use take_profit_trader instead")]
243    pub max_gains_in_quote: Option<MaxGainsInQuote>,
244
245    /// Entry price
246    pub entry_price_base: PriceBaseInQuote,
247
248    /// When the next liquifunding is scheduled
249    pub next_liquifunding: Timestamp,
250
251    /// Stop loss price set by the trader
252    pub stop_loss_override: Option<PriceBaseInQuote>,
253
254    /// The take profit value set by the trader in a message.
255    /// For historical reasons, this value can be optional if the user provided a max gains price.
256    #[serde(rename = "take_profit_override")]
257    pub take_profit_trader: Option<TakeProfitTrader>,
258    /// The most recently calculated price at which the trader will achieve maximum gains and take all counter collateral.
259    #[serde(rename = "take_profit_price_base")]
260    pub take_profit_total_base: Option<PriceBaseInQuote>,
261}
262
263impl Position {
264    /// Direction of the position
265    pub fn direction(&self) -> DirectionToNotional {
266        if self.notional_size.is_negative() {
267            DirectionToNotional::Short
268        } else {
269            DirectionToNotional::Long
270        }
271    }
272
273    /// Maximum gains for the position
274    pub fn max_gains_in_quote(
275        &self,
276        market_type: MarketType,
277        price_point: &PricePoint,
278    ) -> Result<MaxGainsInQuote> {
279        match market_type {
280            MarketType::CollateralIsQuote => Ok(MaxGainsInQuote::Finite(
281                self.counter_collateral
282                    .checked_div_collateral(self.active_collateral)?,
283            )),
284            MarketType::CollateralIsBase => {
285                let take_profit_price = self.take_profit_price_total(price_point, market_type)?;
286                let take_profit_price = match take_profit_price {
287                    Some(price) => price,
288                    None => return Ok(MaxGainsInQuote::PosInfinity),
289                };
290                let take_profit_collateral = self
291                    .active_collateral
292                    .checked_add(self.counter_collateral.raw())?;
293                let take_profit_in_notional =
294                    take_profit_price.collateral_to_notional_non_zero(take_profit_collateral);
295                let active_collateral_in_notional =
296                    price_point.collateral_to_notional_non_zero(self.active_collateral);
297                anyhow::ensure!(
298                    take_profit_in_notional > active_collateral_in_notional,
299                    "Max gains in quote is negative, this should not be possible.
300                    Take profit: {take_profit_in_notional}.
301                    Active collateral: {active_collateral_in_notional}"
302                );
303                let res = (take_profit_in_notional.into_decimal256()
304                    - active_collateral_in_notional.into_decimal256())
305                .checked_div(active_collateral_in_notional.into_decimal256())?;
306                Ok(MaxGainsInQuote::Finite(
307                    NonZero::new(res).context("Max gains of 0")?,
308                ))
309            }
310        }
311    }
312
313    /// Compute the internal leverage active collateral.
314    pub fn active_leverage_to_notional(
315        &self,
316        price_point: &PricePoint,
317    ) -> SignedLeverageToNotional {
318        SignedLeverageToNotional::calculate(self.notional_size, price_point, self.active_collateral)
319    }
320
321    /// Compute the internal leverage for the counter collateral.
322    pub fn counter_leverage_to_notional(
323        &self,
324        price_point: &PricePoint,
325    ) -> SignedLeverageToNotional {
326        SignedLeverageToNotional::calculate(
327            self.notional_size,
328            price_point,
329            self.counter_collateral,
330        )
331    }
332
333    /// Convert the notional size into collateral at the given price point.
334    pub fn notional_size_in_collateral(&self, price_point: &PricePoint) -> Signed<Collateral> {
335        self.notional_size
336            .map(|x| price_point.notional_to_collateral(x))
337    }
338
339    /// Calculate the size of the position in terms of the base asset.
340    ///
341    /// This represents what the users' perception of their position is, and
342    /// needs to take into account the off-by-one leverage impact of
343    /// collateral-is-base markets.
344    pub fn position_size_base(
345        &self,
346        market_type: MarketType,
347        price_point: &PricePoint,
348    ) -> Result<Signed<Base>> {
349        let leverage = self
350            .active_leverage_to_notional(price_point)
351            .into_base(market_type)?;
352        let active_collateral = price_point.collateral_to_base_non_zero(self.active_collateral);
353        leverage.checked_mul_base(active_collateral)
354    }
355
356    /// Calculate the PnL of this position in terms of the collateral.
357    pub fn pnl_in_collateral(&self) -> Result<Signed<Collateral>> {
358        self.active_collateral.into_signed() - self.deposit_collateral.collateral()
359    }
360
361    /// Calculate the PnL of this position in terms of USD.
362    ///
363    /// Note that this is not equivalent to converting the collateral PnL into
364    /// USD, since we follow a cost basis model in this function, tracking the
365    /// price of the collateral asset in terms of USD for each transaction.
366    pub fn pnl_in_usd(&self, price_point: &PricePoint) -> Result<Signed<Usd>> {
367        let active_collateral_in_usd =
368            price_point.collateral_to_usd_non_zero(self.active_collateral);
369
370        active_collateral_in_usd.into_signed() - self.deposit_collateral.usd()
371    }
372
373    /// Computes the liquidation margin for the position
374    ///
375    /// Takes the price point of the last liquifunding.
376    pub fn liquidation_margin(
377        &self,
378        price_point: &PricePoint,
379        config: &Config,
380    ) -> Result<LiquidationMargin> {
381        const SEC_PER_YEAR: u64 = 31_536_000;
382        const MS_PER_YEAR: u64 = SEC_PER_YEAR * 1000;
383        // Panicking is fine here, it's a hard-coded value
384        let ms_per_year = Decimal256::from_atomics(MS_PER_YEAR, 0).unwrap();
385
386        let duration =
387            Duration::from_seconds(config.liquifunding_delay_seconds.into()).as_ms_decimal_lossy();
388
389        let borrow_fee_max_rate =
390            config.borrow_fee_rate_max_annualized.raw() * duration / ms_per_year;
391        let borrow_fee_max_payment = (self
392            .active_collateral
393            .raw()
394            .checked_add(self.counter_collateral.raw())?)
395        .checked_mul_dec(borrow_fee_max_rate)?;
396
397        let max_price = match self.direction() {
398            DirectionToNotional::Long => {
399                price_point.price_notional.into_decimal256()
400                    + self.counter_collateral.into_decimal256()
401                        / self.notional_size.abs_unsigned().into_decimal256()
402            }
403            DirectionToNotional::Short => {
404                price_point.price_notional.into_decimal256()
405                    + self.active_collateral.into_decimal256()
406                        / self.notional_size.abs_unsigned().into_decimal256()
407            }
408        };
409
410        let funding_max_rate = config.funding_rate_max_annualized * duration / ms_per_year;
411        let funding_max_payment =
412            funding_max_rate * self.notional_size.abs_unsigned().into_decimal256() * max_price;
413
414        let slippage_max = config.delta_neutrality_fee_cap.into_decimal256()
415            * self.notional_size.abs_unsigned().into_decimal256()
416            * max_price;
417
418        Ok(LiquidationMargin {
419            borrow: borrow_fee_max_payment,
420            funding: Collateral::from_decimal256(funding_max_payment),
421            delta_neutrality: Collateral::from_decimal256(slippage_max),
422            crank: price_point.usd_to_collateral(config.crank_fee_charged),
423            exposure: price_point
424                .notional_to_collateral(self.notional_size.abs_unsigned())
425                .checked_mul_dec(config.exposure_margin_ratio)?,
426        })
427    }
428
429    /// Computes the liquidation price for the position at a given spot price.
430    pub fn liquidation_price(
431        &self,
432        price: Price,
433        active_collateral: NonZero<Collateral>,
434        liquidation_margin: &LiquidationMargin,
435    ) -> Option<Price> {
436        let liquidation_margin = liquidation_margin.total().ok()?.into_number();
437        let liquidation_price = (price.into_number()
438            - ((active_collateral.into_number() - liquidation_margin).ok()?
439                / self.notional_size.into_number())
440            .ok()?)
441        .ok()?;
442
443        Price::try_from_number(liquidation_price).ok()
444    }
445
446    /// Computes the take-profit price for the position at a given spot price.
447    pub fn take_profit_price_total(
448        &self,
449        price_point: &PricePoint,
450        market_type: MarketType,
451    ) -> Result<Option<Price>> {
452        let take_profit_price_raw = price_point.price_notional.into_number().checked_add(
453            self.counter_collateral
454                .into_number()
455                .checked_div(self.notional_size.into_number())?,
456        )?;
457
458        let take_profit_price = if take_profit_price_raw.approx_eq(Number::ZERO)? {
459            None
460        } else {
461            debug_assert!(
462                take_profit_price_raw.is_positive_or_zero(),
463                "There should never be a calculated take profit price which is negative. In production, this is treated as 0 to indicate infinite max gains."
464            );
465            Price::try_from_number(take_profit_price_raw).ok()
466        };
467
468        match take_profit_price {
469            Some(price) => Ok(Some(price)),
470            None =>
471            match market_type {
472                // Infinite max gains results in a notional take profit price of 0
473                MarketType::CollateralIsBase => Ok(None),
474                MarketType::CollateralIsQuote => Err(anyhow!("Calculated a take profit price of {take_profit_price_raw} in a collateral-is-quote market. Spot notional price: {}. Counter collateral: {}. Notional size: {}.", price_point.price_notional, self.counter_collateral,self.notional_size)),
475            }
476        }
477    }
478
479    /// Add a new delta neutrality fee to the position.
480    pub fn add_delta_neutrality_fee(
481        &mut self,
482        amount: Signed<Collateral>,
483        price_point: &PricePoint,
484    ) -> Result<()> {
485        self.delta_neutrality_fee
486            .checked_add_assign(amount, price_point)
487    }
488
489    /// Get the price exposure for a given price movement.
490    pub fn get_price_exposure(
491        &self,
492        start_price: Price,
493        end_price: PricePoint,
494    ) -> Result<Signed<Collateral>> {
495        let price_delta = (end_price.price_notional.into_number() - start_price.into_number())?;
496        Ok(Signed::<Collateral>::from_number(
497            (price_delta * self.notional_size.into_number())?,
498        ))
499    }
500
501    /// Apply a price change to this position.
502    ///
503    /// This will determine the exposure (positive == to trader, negative == to
504    /// liquidity pool) impact and return whether the position can remain open
505    /// or instructions to close it.
506    pub fn settle_price_exposure(
507        mut self,
508        start_price: Price,
509        end_price: PricePoint,
510        liquidation_margin: Collateral,
511    ) -> Result<(MaybeClosedPosition, Signed<Collateral>)> {
512        let exposure = self.get_price_exposure(start_price, end_price)?;
513        let min_exposure = liquidation_margin
514            .into_signed()
515            .checked_sub(self.active_collateral.into_signed())?;
516        let max_exposure = self.counter_collateral.into_signed();
517
518        Ok(if exposure <= min_exposure {
519            (
520                MaybeClosedPosition::Close(ClosePositionInstructions {
521                    pos: self,
522                    capped_exposure: min_exposure,
523                    additional_losses: min_exposure
524                        .checked_sub(exposure)?
525                        .try_into_non_negative_value()
526                        .context("Calculated additional_losses is negative")?,
527                    settlement_price: end_price,
528                    reason: PositionCloseReason::Liquidated(LiquidationReason::Liquidated),
529                    closed_during_liquifunding: true,
530                }),
531                min_exposure,
532            )
533        } else if exposure >= max_exposure {
534            (
535                MaybeClosedPosition::Close(ClosePositionInstructions {
536                    pos: self,
537                    capped_exposure: max_exposure,
538                    additional_losses: Collateral::zero(),
539                    settlement_price: end_price,
540                    reason: PositionCloseReason::Liquidated(LiquidationReason::MaxGains),
541                    closed_during_liquifunding: true,
542                }),
543                max_exposure,
544            )
545        } else {
546            self.active_collateral = self.active_collateral.checked_add_signed(exposure)?;
547            self.counter_collateral = self.counter_collateral.checked_sub_signed(exposure)?;
548            (MaybeClosedPosition::Open(self), exposure)
549        })
550    }
551
552    /// Convert a position into a query response, calculating price exposure impact.
553    #[allow(clippy::too_many_arguments)]
554    pub fn into_query_response_extrapolate_exposure(
555        mut self,
556        start_price: PricePoint,
557        end_price: PricePoint,
558        entry_price: Price,
559        market_type: MarketType,
560        dnf_on_close_collateral: Signed<Collateral>,
561    ) -> Result<PositionOrPendingClose> {
562        let exposure = self.get_price_exposure(start_price.price_notional, end_price)?;
563
564        let is_profit = exposure.is_strictly_positive();
565        let exposure = exposure.abs_unsigned();
566
567        let is_open = if is_profit {
568            match self.counter_collateral.checked_sub(exposure) {
569                Ok(counter_collateral) => {
570                    self.counter_collateral = counter_collateral;
571                    self.active_collateral = self.active_collateral.checked_add(exposure)?;
572                    true
573                }
574                Err(_) => false,
575            }
576        } else {
577            match self.active_collateral.checked_sub(exposure) {
578                Ok(active_collateral) => {
579                    self.active_collateral = active_collateral;
580                    self.counter_collateral = self.counter_collateral.checked_add(exposure)?;
581                    true
582                }
583                Err(_) => false,
584            }
585        };
586
587        if is_open {
588            self.into_query_response(end_price, entry_price, market_type, dnf_on_close_collateral)
589                .map(|pos| PositionOrPendingClose::Open(Box::new(pos)))
590        } else {
591            let direction_to_base = self.direction().into_base(market_type);
592            let entry_price_base = entry_price.into_base_price(market_type);
593
594            // Figure out the final active collateral.
595            let active_collateral = if is_profit {
596                self.active_collateral.raw().checked_add(exposure)?
597            } else {
598                Collateral::zero()
599            };
600
601            let active_collateral_usd = end_price.collateral_to_usd(active_collateral);
602            Ok(PositionOrPendingClose::PendingClose(Box::new(
603                ClosedPosition {
604                    owner: self.owner,
605                    id: self.id,
606                    direction_to_base,
607                    created_at: self.created_at,
608                    price_point_created_at: self.price_point_created_at,
609                    liquifunded_at: self.liquifunded_at,
610                    trading_fee_collateral: self.trading_fee.collateral(),
611                    trading_fee_usd: self.trading_fee.usd(),
612                    funding_fee_collateral: self.funding_fee.collateral(),
613                    funding_fee_usd: self.funding_fee.usd(),
614                    borrow_fee_collateral: self.borrow_fee.collateral(),
615                    borrow_fee_usd: self.borrow_fee.usd(),
616                    crank_fee_collateral: self.crank_fee.collateral(),
617                    crank_fee_usd: self.crank_fee.usd(),
618                    deposit_collateral: self.deposit_collateral.collateral(),
619                    deposit_collateral_usd: self.deposit_collateral.usd(),
620                    pnl_collateral: active_collateral
621                        .into_signed()
622                        .checked_sub(self.deposit_collateral.collateral())?,
623                    pnl_usd: active_collateral_usd
624                        .into_signed()
625                        .checked_sub(self.deposit_collateral.usd())?,
626                    notional_size: self.notional_size,
627                    entry_price_base,
628                    close_time: end_price.timestamp,
629                    settlement_time: end_price.timestamp,
630                    reason: PositionCloseReason::Liquidated(if is_profit {
631                        LiquidationReason::MaxGains
632                    } else {
633                        LiquidationReason::Liquidated
634                    }),
635                    active_collateral,
636                    delta_neutrality_fee_collateral: self.delta_neutrality_fee.collateral(),
637                    delta_neutrality_fee_usd: self.delta_neutrality_fee.usd(),
638                    liquidation_margin: Some(self.liquidation_margin),
639                },
640            )))
641        }
642    }
643
644    /// Convert into a query response, without calculating price exposure impact.
645    pub fn into_query_response(
646        self,
647        end_price: PricePoint,
648        entry_price: Price,
649        market_type: MarketType,
650        dnf_on_close_collateral: Signed<Collateral>,
651    ) -> Result<PositionQueryResponse> {
652        let (direction_to_base, leverage) = self
653            .active_leverage_to_notional(&end_price)
654            .into_base(market_type)?
655            .split();
656        let counter_leverage = self
657            .counter_leverage_to_notional(&end_price)
658            .into_base(market_type)?
659            .split()
660            .1;
661        let pnl_collateral = self.pnl_in_collateral()?;
662        let pnl_usd = self.pnl_in_usd(&end_price)?;
663        let notional_size_in_collateral = self.notional_size_in_collateral(&end_price);
664        let position_size_base = self.position_size_base(market_type, &end_price)?;
665
666        let Self {
667            owner,
668            id,
669            active_collateral,
670            deposit_collateral,
671            counter_collateral,
672            notional_size,
673            created_at,
674            price_point_created_at,
675            trading_fee,
676            funding_fee,
677            borrow_fee,
678            crank_fee,
679            delta_neutrality_fee,
680            liquifunded_at,
681            next_liquifunding,
682            stop_loss_override,
683            liquidation_margin,
684            liquidation_price,
685            stop_loss_override_notional: _,
686            take_profit_trader,
687            take_profit_trader_notional: _,
688            take_profit_total,
689        } = self;
690
691        // TODO: remove this once the deprecated fields are fully removed
692        #[allow(deprecated)]
693        Ok(PositionQueryResponse {
694            owner,
695            id,
696            created_at,
697            price_point_created_at,
698            liquifunded_at,
699            direction_to_base,
700            leverage,
701            counter_leverage,
702            trading_fee_collateral: trading_fee.collateral(),
703            trading_fee_usd: trading_fee.usd(),
704            funding_fee_collateral: funding_fee.collateral(),
705            funding_fee_usd: funding_fee.usd(),
706            borrow_fee_collateral: borrow_fee.collateral(),
707            borrow_fee_usd: borrow_fee.usd(),
708            delta_neutrality_fee_collateral: delta_neutrality_fee.collateral(),
709            delta_neutrality_fee_usd: delta_neutrality_fee.usd(),
710            active_collateral,
711            active_collateral_usd: end_price.collateral_to_usd_non_zero(active_collateral),
712            deposit_collateral: deposit_collateral.collateral(),
713            deposit_collateral_usd: deposit_collateral.usd(),
714            pnl_collateral,
715            pnl_usd,
716            dnf_on_close_collateral,
717            notional_size,
718            notional_size_in_collateral,
719            position_size_base,
720            position_size_usd: position_size_base.map(|x| end_price.base_to_usd(x)),
721            counter_collateral,
722            max_gains_in_quote: None,
723            liquidation_price_base: liquidation_price.map(|x| x.into_base_price(market_type)),
724            liquidation_margin,
725            take_profit_total_base: take_profit_total.map(|x| x.into_base_price(market_type)),
726            entry_price_base: entry_price.into_base_price(market_type),
727            next_liquifunding,
728            stop_loss_override,
729            take_profit_trader,
730            crank_fee_collateral: crank_fee.collateral(),
731            crank_fee_usd: crank_fee.usd(),
732        })
733    }
734
735    /// Attributes for a position which can be emitted in events.
736    pub fn attributes(&self) -> Vec<(&'static str, String)> {
737        let LiquidationMargin {
738            borrow: borrow_fee_max,
739            funding: funding_max,
740            delta_neutrality: slippage_max,
741            crank,
742            exposure,
743        } = &self.liquidation_margin;
744        vec![
745            ("pos-owner", self.owner.to_string()),
746            ("pos-id", self.id.to_string()),
747            ("pos-active-collateral", self.active_collateral.to_string()),
748            (
749                "pos-deposit-collateral",
750                self.deposit_collateral.collateral().to_string(),
751            ),
752            (
753                "pos-deposit-collateral-usd",
754                self.deposit_collateral.usd().to_string(),
755            ),
756            ("pos-trading-fee", self.trading_fee.collateral().to_string()),
757            ("pos-trading-fee-usd", self.trading_fee.usd().to_string()),
758            ("pos-crank-fee", self.crank_fee.collateral().to_string()),
759            ("pos-crank-fee-usd", self.crank_fee.usd().to_string()),
760            (
761                "pos-counter-collateral",
762                self.counter_collateral.to_string(),
763            ),
764            ("pos-notional-size", self.notional_size.to_string()),
765            ("pos-created-at", self.created_at.to_string()),
766            ("pos-liquifunded-at", self.liquifunded_at.to_string()),
767            ("pos-next-liquifunding", self.next_liquifunding.to_string()),
768            (
769                "pos-borrow-fee-liquidation-margin",
770                borrow_fee_max.to_string(),
771            ),
772            ("pos-funding-liquidation-margin", funding_max.to_string()),
773            ("pos-slippage-liquidation-margin", slippage_max.to_string()),
774            ("pos-crank-liquidation-margin", crank.to_string()),
775            ("pos-exposure-liquidation-margin", exposure.to_string()),
776        ]
777    }
778}
779
780/// PositionId
781#[cw_serde]
782#[derive(Copy, PartialOrd, Ord, Eq)]
783pub struct PositionId(Uint64);
784
785#[cfg(feature = "arbitrary")]
786impl<'a> arbitrary::Arbitrary<'a> for PositionId {
787    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
788        u64::arbitrary(u).map(PositionId::new)
789    }
790}
791
792#[allow(clippy::derived_hash_with_manual_eq)]
793impl Hash for PositionId {
794    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
795        self.u64().hash(state);
796    }
797}
798
799impl PositionId {
800    /// Construct a new value from a [u64].
801    pub fn new(x: u64) -> Self {
802        PositionId(x.into())
803    }
804
805    /// The underlying `u64` representation.
806    pub fn u64(self) -> u64 {
807        self.0.u64()
808    }
809
810    /// Generate the next position ID
811    ///
812    /// Panics on overflow
813    pub fn next(self) -> Self {
814        PositionId((self.u64() + 1).into())
815    }
816}
817
818impl<'a> PrimaryKey<'a> for PositionId {
819    type Prefix = ();
820    type SubPrefix = ();
821    type Suffix = Self;
822    type SuperSuffix = Self;
823
824    fn key(&self) -> Vec<Key> {
825        vec![Key::Val64(self.0.u64().to_cw_bytes())]
826    }
827}
828
829impl<'a> Prefixer<'a> for PositionId {
830    fn prefix(&self) -> Vec<Key> {
831        vec![Key::Val64(self.0.u64().to_cw_bytes())]
832    }
833}
834
835impl KeyDeserialize for PositionId {
836    type Output = PositionId;
837
838    const KEY_ELEMS: u16 = 1;
839
840    #[inline(always)]
841    fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
842        u64::from_vec(value).map(|x| PositionId(Uint64::new(x)))
843    }
844}
845
846impl fmt::Display for PositionId {
847    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
848        write!(f, "{}", self.0)
849    }
850}
851
852impl FromStr for PositionId {
853    type Err = ParseIntError;
854    fn from_str(src: &str) -> Result<Self, ParseIntError> {
855        src.parse().map(|x| PositionId(Uint64::new(x)))
856    }
857}
858
859/// Events
860pub mod events {
861    use super::*;
862    use crate::constants::{event_key, event_val};
863    use cosmwasm_std::Event;
864
865    /// Collaterals calculated on a position.
866    #[cw_serde]
867    pub struct PositionCollaterals {
868        /// [PositionQueryResponse::deposit_collateral]
869        pub deposit_collateral: Signed<Collateral>,
870        /// [PositionQueryResponse::deposit_collateral_usd]
871        pub deposit_collateral_usd: Signed<Usd>,
872        /// [PositionQueryResponse::active_collateral]
873        pub active_collateral: NonZero<Collateral>,
874        /// [PositionQueryResponse::counter_collateral]
875        pub counter_collateral: NonZero<Collateral>,
876    }
877
878    /// Trading fees paid for a position
879    #[cw_serde]
880    pub struct PositionTradingFee {
881        /// In collateral
882        pub trading_fee: Collateral,
883        /// In USD
884        pub trading_fee_usd: Usd,
885    }
886
887    /// Returns a tuple containing (base, quote) calculated based on the market type
888    pub fn calculate_base_and_quote(
889        market_type: MarketType,
890        price: Price,
891        amount: Number,
892    ) -> Result<(Number, Number)> {
893        Ok(match market_type {
894            MarketType::CollateralIsQuote => (amount.checked_div(price.into_number())?, amount),
895            MarketType::CollateralIsBase => (amount, amount.checked_mul(price.into_number())?),
896        })
897    }
898
899    /// Calculate the collaterals for a position
900    pub fn calculate_position_collaterals(pos: &Position) -> Result<PositionCollaterals> {
901        Ok(PositionCollaterals {
902            deposit_collateral: pos.deposit_collateral.collateral(),
903            deposit_collateral_usd: pos.deposit_collateral.usd(),
904            active_collateral: pos.active_collateral,
905            counter_collateral: pos.counter_collateral,
906        })
907    }
908
909    /// All attributes for a position
910    #[cw_serde]
911    pub struct PositionAttributes {
912        /// [Position::id]
913        pub pos_id: PositionId,
914        /// [Position::owner]
915        pub owner: Addr,
916        /// Collaterals calculated on the position
917        pub collaterals: PositionCollaterals,
918        /// Type of the market it was opened in
919        pub market_type: MarketType,
920        /// [Position::notional_size]
921        pub notional_size: Signed<Notional>,
922        /// [PositionQueryResponse::notional_size_in_collateral]
923        pub notional_size_in_collateral: Signed<Collateral>,
924        /// Calculated using the exchange rate when the event was emitted
925        pub notional_size_usd: Signed<Usd>,
926        /// Trading fee
927        pub trading_fee: PositionTradingFee,
928        /// Direction
929        pub direction: DirectionToBase,
930        /// Trader leverage
931        pub leverage: LeverageToBase,
932        /// Counter leverage
933        pub counter_leverage: LeverageToBase,
934        /// Stop loss price
935        pub stop_loss_override: Option<PriceBaseInQuote>,
936        /// Take profit price set by trader
937        /// For historical reasons, this value can be optional if the user provided a max gains price.
938        #[serde(rename = "take_profit_override")]
939        pub take_profit_trader: Option<TakeProfitTrader>,
940    }
941
942    impl PositionAttributes {
943        fn add_to_event(&self, event: &Event) -> Event {
944            let mut event = event
945                .clone()
946                .add_attribute(event_key::POS_ID, self.pos_id.to_string())
947                .add_attribute(event_key::POS_OWNER, self.owner.clone())
948                .add_attribute(
949                    event_key::DEPOSIT_COLLATERAL,
950                    self.collaterals.deposit_collateral.to_string(),
951                )
952                .add_attribute(
953                    event_key::DEPOSIT_COLLATERAL_USD,
954                    self.collaterals.deposit_collateral_usd.to_string(),
955                )
956                .add_attribute(
957                    event_key::ACTIVE_COLLATERAL,
958                    self.collaterals.active_collateral.to_string(),
959                )
960                .add_attribute(
961                    event_key::COUNTER_COLLATERAL,
962                    self.collaterals.counter_collateral.to_string(),
963                )
964                .add_attribute(
965                    event_key::MARKET_TYPE,
966                    match self.market_type {
967                        MarketType::CollateralIsQuote => event_val::NOTIONAL_BASE,
968                        MarketType::CollateralIsBase => event_val::COLLATERAL_BASE,
969                    },
970                )
971                .add_attribute(event_key::NOTIONAL_SIZE, self.notional_size.to_string())
972                .add_attribute(
973                    event_key::NOTIONAL_SIZE_IN_COLLATERAL,
974                    self.notional_size_in_collateral.to_string(),
975                )
976                .add_attribute(
977                    event_key::NOTIONAL_SIZE_USD,
978                    self.notional_size_usd.to_string(),
979                )
980                .add_attribute(
981                    event_key::TRADING_FEE,
982                    self.trading_fee.trading_fee.to_string(),
983                )
984                .add_attribute(
985                    event_key::TRADING_FEE_USD,
986                    self.trading_fee.trading_fee_usd.to_string(),
987                )
988                .add_attribute(event_key::DIRECTION, self.direction.as_str())
989                .add_attribute(event_key::LEVERAGE, self.leverage.to_string())
990                .add_attribute(
991                    event_key::COUNTER_LEVERAGE,
992                    self.counter_leverage.to_string(),
993                );
994
995            if let Some(stop_loss_override) = self.stop_loss_override {
996                event = event.add_attribute(
997                    event_key::STOP_LOSS_OVERRIDE,
998                    stop_loss_override.to_string(),
999                );
1000            }
1001
1002            if let Some(take_profit_trader) = self.take_profit_trader {
1003                event = event.add_attribute(
1004                    event_key::TAKE_PROFIT_OVERRIDE,
1005                    take_profit_trader.to_string(),
1006                );
1007            }
1008
1009            event
1010        }
1011    }
1012
1013    impl TryFrom<Event> for PositionAttributes {
1014        type Error = anyhow::Error;
1015
1016        fn try_from(evt: Event) -> anyhow::Result<Self> {
1017            Ok(Self {
1018                pos_id: PositionId::new(evt.u64_attr(event_key::POS_ID)?),
1019                owner: evt.unchecked_addr_attr(event_key::POS_OWNER)?,
1020                collaterals: PositionCollaterals {
1021                    deposit_collateral: evt.number_attr(event_key::DEPOSIT_COLLATERAL)?,
1022                    deposit_collateral_usd: evt.number_attr(event_key::DEPOSIT_COLLATERAL_USD)?,
1023                    active_collateral: evt.non_zero_attr(event_key::ACTIVE_COLLATERAL)?,
1024                    counter_collateral: evt.non_zero_attr(event_key::COUNTER_COLLATERAL)?,
1025                },
1026                market_type: evt.map_attr_result(event_key::MARKET_TYPE, |s| match s {
1027                    event_val::NOTIONAL_BASE => Ok(MarketType::CollateralIsQuote),
1028                    event_val::COLLATERAL_BASE => Ok(MarketType::CollateralIsBase),
1029                    _ => Err(PerpError::unimplemented().into()),
1030                })?,
1031                notional_size: evt.number_attr(event_key::NOTIONAL_SIZE)?,
1032                notional_size_in_collateral: evt
1033                    .number_attr(event_key::NOTIONAL_SIZE_IN_COLLATERAL)?,
1034                notional_size_usd: evt.number_attr(event_key::NOTIONAL_SIZE_USD)?,
1035                trading_fee: PositionTradingFee {
1036                    trading_fee: evt.decimal_attr(event_key::TRADING_FEE)?,
1037                    trading_fee_usd: evt.decimal_attr(event_key::TRADING_FEE_USD)?,
1038                },
1039                direction: evt.direction_attr(event_key::DIRECTION)?,
1040                leverage: evt.leverage_to_base_attr(event_key::LEVERAGE)?,
1041                counter_leverage: evt.leverage_to_base_attr(event_key::COUNTER_LEVERAGE)?,
1042                stop_loss_override: match evt.try_number_attr(event_key::STOP_LOSS_OVERRIDE)? {
1043                    None => None,
1044                    Some(stop_loss_override) => {
1045                        Some(PriceBaseInQuote::try_from_number(stop_loss_override)?)
1046                    }
1047                },
1048                take_profit_trader: evt
1049                    .try_map_attr(event_key::TAKE_PROFIT_OVERRIDE, |s| {
1050                        TakeProfitTrader::try_from(s)
1051                    })
1052                    .transpose()?,
1053            })
1054        }
1055    }
1056
1057    /// A position was closed
1058    #[derive(Debug, Clone)]
1059    pub struct PositionCloseEvent {
1060        /// Details on the closed position
1061        pub closed_position: ClosedPosition,
1062    }
1063
1064    impl TryFrom<PositionCloseEvent> for Event {
1065        type Error = anyhow::Error;
1066
1067        fn try_from(
1068            PositionCloseEvent {
1069                closed_position:
1070                    ClosedPosition {
1071                        owner,
1072                        id,
1073                        direction_to_base,
1074                        created_at,
1075                        price_point_created_at,
1076                        liquifunded_at,
1077                        trading_fee_collateral,
1078                        trading_fee_usd,
1079                        funding_fee_collateral,
1080                        funding_fee_usd,
1081                        borrow_fee_collateral,
1082                        borrow_fee_usd,
1083                        crank_fee_collateral,
1084                        crank_fee_usd,
1085                        delta_neutrality_fee_collateral,
1086                        delta_neutrality_fee_usd,
1087                        deposit_collateral,
1088                        deposit_collateral_usd,
1089                        active_collateral,
1090                        pnl_collateral,
1091                        pnl_usd,
1092                        notional_size,
1093                        entry_price_base,
1094                        close_time,
1095                        settlement_time,
1096                        reason,
1097                        liquidation_margin,
1098                    },
1099            }: PositionCloseEvent,
1100        ) -> anyhow::Result<Self> {
1101            let mut event = Event::new(event_key::POSITION_CLOSE)
1102                .add_attribute(event_key::POS_OWNER, owner.to_string())
1103                .add_attribute(event_key::POS_ID, id.to_string())
1104                .add_attribute(event_key::DIRECTION, direction_to_base.as_str())
1105                .add_attribute(event_key::CREATED_AT, created_at.to_string())
1106                .add_attribute(event_key::LIQUIFUNDED_AT, liquifunded_at.to_string())
1107                .add_attribute(event_key::TRADING_FEE, trading_fee_collateral.to_string())
1108                .add_attribute(event_key::TRADING_FEE_USD, trading_fee_usd.to_string())
1109                .add_attribute(event_key::FUNDING_FEE, funding_fee_collateral.to_string())
1110                .add_attribute(event_key::FUNDING_FEE_USD, funding_fee_usd.to_string())
1111                .add_attribute(event_key::BORROW_FEE, borrow_fee_collateral.to_string())
1112                .add_attribute(event_key::BORROW_FEE_USD, borrow_fee_usd.to_string())
1113                .add_attribute(
1114                    event_key::DELTA_NEUTRALITY_FEE,
1115                    delta_neutrality_fee_collateral.to_string(),
1116                )
1117                .add_attribute(
1118                    event_key::DELTA_NEUTRALITY_FEE_USD,
1119                    delta_neutrality_fee_usd.to_string(),
1120                )
1121                .add_attribute(event_key::CRANK_FEE, crank_fee_collateral.to_string())
1122                .add_attribute(event_key::CRANK_FEE_USD, crank_fee_usd.to_string())
1123                .add_attribute(
1124                    event_key::DEPOSIT_COLLATERAL,
1125                    deposit_collateral.to_string(),
1126                )
1127                .add_attribute(
1128                    event_key::DEPOSIT_COLLATERAL_USD,
1129                    deposit_collateral_usd.to_string(),
1130                )
1131                .add_attribute(event_key::PNL, pnl_collateral.to_string())
1132                .add_attribute(event_key::PNL_USD, pnl_usd.to_string())
1133                .add_attribute(event_key::NOTIONAL_SIZE, notional_size.to_string())
1134                .add_attribute(event_key::ENTRY_PRICE, entry_price_base.to_string())
1135                .add_attribute(event_key::CLOSED_AT, close_time.to_string())
1136                .add_attribute(event_key::SETTLED_AT, settlement_time.to_string())
1137                .add_attribute(
1138                    event_key::CLOSE_REASON,
1139                    match reason {
1140                        PositionCloseReason::Liquidated(LiquidationReason::Liquidated) => {
1141                            event_val::LIQUIDATED
1142                        }
1143                        PositionCloseReason::Liquidated(LiquidationReason::MaxGains) => {
1144                            event_val::MAX_GAINS
1145                        }
1146                        PositionCloseReason::Liquidated(LiquidationReason::StopLoss) => {
1147                            event_val::STOP_LOSS
1148                        }
1149                        PositionCloseReason::Liquidated(LiquidationReason::TakeProfit) => {
1150                            event_val::TAKE_PROFIT
1151                        }
1152                        PositionCloseReason::Direct => event_val::DIRECT,
1153                    },
1154                )
1155                .add_attribute(event_key::ACTIVE_COLLATERAL, active_collateral.to_string());
1156            if let Some(x) = price_point_created_at {
1157                event = event.add_attribute(event_key::PRICE_POINT_CREATED_AT, x.to_string());
1158            }
1159            if let Some(x) = liquidation_margin {
1160                event = event
1161                    .add_attribute(event_key::LIQUIDATION_MARGIN_BORROW, x.borrow.to_string())
1162                    .add_attribute(event_key::LIQUIDATION_MARGIN_FUNDING, x.funding.to_string())
1163                    .add_attribute(
1164                        event_key::LIQUIDATION_MARGIN_DNF,
1165                        x.delta_neutrality.to_string(),
1166                    )
1167                    .add_attribute(event_key::LIQUIDATION_MARGIN_CRANK, x.crank.to_string())
1168                    .add_attribute(
1169                        event_key::LIQUIDATION_MARGIN_EXPOSURE,
1170                        x.exposure.to_string(),
1171                    )
1172                    .add_attribute(event_key::LIQUIDATION_MARGIN_TOTAL, x.total()?.to_string());
1173            }
1174
1175            Ok(event)
1176        }
1177    }
1178    impl TryFrom<Event> for PositionCloseEvent {
1179        type Error = anyhow::Error;
1180
1181        fn try_from(evt: Event) -> anyhow::Result<Self> {
1182            let closed_position = ClosedPosition {
1183                close_time: evt.timestamp_attr(event_key::CLOSED_AT)?,
1184                settlement_time: evt.timestamp_attr(event_key::SETTLED_AT)?,
1185                reason: evt.map_attr_result(event_key::CLOSE_REASON, |s| match s {
1186                    event_val::LIQUIDATED => Ok(PositionCloseReason::Liquidated(
1187                        LiquidationReason::Liquidated,
1188                    )),
1189                    event_val::MAX_GAINS => {
1190                        Ok(PositionCloseReason::Liquidated(LiquidationReason::MaxGains))
1191                    }
1192                    event_val::STOP_LOSS => {
1193                        Ok(PositionCloseReason::Liquidated(LiquidationReason::StopLoss))
1194                    }
1195                    event_val::TAKE_PROFIT => Ok(PositionCloseReason::Liquidated(
1196                        LiquidationReason::TakeProfit,
1197                    )),
1198                    event_val::DIRECT => Ok(PositionCloseReason::Direct),
1199                    _ => Err(PerpError::unimplemented().into()),
1200                })?,
1201                owner: evt.unchecked_addr_attr(event_key::POS_OWNER)?,
1202                id: PositionId::new(evt.u64_attr(event_key::POS_ID)?),
1203                direction_to_base: evt.direction_attr(event_key::DIRECTION)?,
1204                created_at: evt.timestamp_attr(event_key::CREATED_AT)?,
1205                price_point_created_at: evt
1206                    .try_timestamp_attr(event_key::PRICE_POINT_CREATED_AT)?,
1207                liquifunded_at: evt.timestamp_attr(event_key::LIQUIFUNDED_AT)?,
1208                trading_fee_collateral: evt.decimal_attr(event_key::TRADING_FEE)?,
1209                trading_fee_usd: evt.decimal_attr(event_key::TRADING_FEE_USD)?,
1210                funding_fee_collateral: evt.number_attr(event_key::FUNDING_FEE)?,
1211                funding_fee_usd: evt.number_attr(event_key::FUNDING_FEE_USD)?,
1212                borrow_fee_collateral: evt.decimal_attr(event_key::BORROW_FEE)?,
1213                borrow_fee_usd: evt.decimal_attr(event_key::BORROW_FEE_USD)?,
1214                crank_fee_collateral: evt.decimal_attr(event_key::CRANK_FEE)?,
1215                crank_fee_usd: evt.decimal_attr(event_key::CRANK_FEE_USD)?,
1216                delta_neutrality_fee_collateral: evt
1217                    .number_attr(event_key::DELTA_NEUTRALITY_FEE)?,
1218                delta_neutrality_fee_usd: evt.number_attr(event_key::DELTA_NEUTRALITY_FEE_USD)?,
1219                deposit_collateral: evt.number_attr(event_key::DEPOSIT_COLLATERAL)?,
1220                deposit_collateral_usd: evt
1221                    // For migrations, this data wasn't always present
1222                    .try_number_attr(event_key::DEPOSIT_COLLATERAL_USD)?
1223                    .unwrap_or_default(),
1224                pnl_collateral: evt.number_attr(event_key::PNL)?,
1225                pnl_usd: evt.number_attr(event_key::PNL_USD)?,
1226                notional_size: evt.number_attr(event_key::NOTIONAL_SIZE)?,
1227                entry_price_base: PriceBaseInQuote::try_from_number(
1228                    evt.number_attr(event_key::ENTRY_PRICE)?,
1229                )?,
1230                active_collateral: evt.decimal_attr(event_key::ACTIVE_COLLATERAL)?,
1231                liquidation_margin: match (
1232                    evt.try_decimal_attr::<Collateral>(event_key::LIQUIDATION_MARGIN_BORROW)?,
1233                    evt.try_decimal_attr::<Collateral>(event_key::LIQUIDATION_MARGIN_FUNDING)?,
1234                    evt.try_decimal_attr::<Collateral>(event_key::LIQUIDATION_MARGIN_DNF)?,
1235                    evt.try_decimal_attr::<Collateral>(event_key::LIQUIDATION_MARGIN_CRANK)?,
1236                    evt.try_decimal_attr::<Collateral>(event_key::LIQUIDATION_MARGIN_EXPOSURE)?,
1237                ) {
1238                    (
1239                        Some(borrow),
1240                        Some(funding),
1241                        Some(delta_neutrality),
1242                        Some(crank),
1243                        Some(exposure),
1244                    ) => Some(LiquidationMargin {
1245                        borrow,
1246                        funding,
1247                        delta_neutrality,
1248                        crank,
1249                        exposure,
1250                    }),
1251                    _ => None,
1252                },
1253            };
1254            Ok(PositionCloseEvent { closed_position })
1255        }
1256    }
1257
1258    /// A position was opened
1259    pub struct PositionOpenEvent {
1260        /// Details of the position
1261        pub position_attributes: PositionAttributes,
1262        /// When it was opened, block time
1263        pub created_at: Timestamp,
1264        /// Price point used for creating this
1265        pub price_point_created_at: Timestamp,
1266    }
1267
1268    impl From<PositionOpenEvent> for Event {
1269        fn from(src: PositionOpenEvent) -> Self {
1270            let event = Event::new(event_key::POSITION_OPEN)
1271                .add_attribute(event_key::CREATED_AT, src.created_at.to_string());
1272
1273            src.position_attributes.add_to_event(&event)
1274        }
1275    }
1276    impl TryFrom<Event> for PositionOpenEvent {
1277        type Error = anyhow::Error;
1278
1279        fn try_from(evt: Event) -> anyhow::Result<Self> {
1280            Ok(Self {
1281                created_at: evt.timestamp_attr(event_key::CREATED_AT)?,
1282                price_point_created_at: evt.timestamp_attr(event_key::PRICE_POINT_CREATED_AT)?,
1283                position_attributes: evt.try_into()?,
1284            })
1285        }
1286    }
1287
1288    /// Event when a position has been updated
1289    #[cw_serde]
1290    pub struct PositionUpdateEvent {
1291        /// Attributes about the position
1292        pub position_attributes: PositionAttributes,
1293        /// Amount of collateral added or removed to the position
1294        pub deposit_collateral_delta: Signed<Collateral>,
1295        /// [Self::deposit_collateral_delta] converted to USD at the current price.
1296        pub deposit_collateral_delta_usd: Signed<Usd>,
1297        /// Change to active collateral
1298        pub active_collateral_delta: Signed<Collateral>,
1299        /// [Self::active_collateral_delta] converted to USD at the current price.
1300        pub active_collateral_delta_usd: Signed<Usd>,
1301        /// Change to counter collateral
1302        pub counter_collateral_delta: Signed<Collateral>,
1303        /// [Self::counter_collateral_delta] converted to USD at the current price.
1304        pub counter_collateral_delta_usd: Signed<Usd>,
1305        /// Change in trader leverage
1306        pub leverage_delta: Signed<Decimal256>,
1307        /// Change in counter collateral leverage
1308        pub counter_leverage_delta: Signed<Decimal256>,
1309        /// Change in the notional size
1310        pub notional_size_delta: Signed<Notional>,
1311        /// [Self::notional_size_delta] converted to USD at the current price.
1312        pub notional_size_delta_usd: Signed<Usd>,
1313        /// The change in notional size from the absolute value
1314        ///
1315        /// not the absolute value of delta itself
1316        /// e.g. from -10 to -15 will be 5, because it's the delta of 15-10
1317        /// but -15 to -10 will be -5, because it's the delta of 10-15
1318        pub notional_size_abs_delta: Signed<Notional>,
1319        /// [Self::notional_size_abs_delta] converted to USD at the current price.
1320        pub notional_size_abs_delta_usd: Signed<Usd>,
1321        /// Additional trading fee paid
1322        pub trading_fee_delta: Collateral,
1323        /// [Self::trading_fee_delta] converted to USD at the current price.
1324        pub trading_fee_delta_usd: Usd,
1325        /// Additional delta neutrality fee paid (or received)
1326        pub delta_neutrality_fee_delta: Signed<Collateral>,
1327        /// [Self::delta_neutrality_fee_delta] converted to USD at the current price.
1328        pub delta_neutrality_fee_delta_usd: Signed<Usd>,
1329        /// When the update occurred
1330        pub updated_at: Timestamp,
1331    }
1332
1333    impl From<PositionUpdateEvent> for Event {
1334        fn from(
1335            PositionUpdateEvent {
1336                position_attributes,
1337                deposit_collateral_delta,
1338                deposit_collateral_delta_usd,
1339                active_collateral_delta,
1340                active_collateral_delta_usd,
1341                counter_collateral_delta,
1342                counter_collateral_delta_usd,
1343                leverage_delta,
1344                counter_leverage_delta,
1345                notional_size_delta,
1346                notional_size_delta_usd,
1347                notional_size_abs_delta,
1348                notional_size_abs_delta_usd,
1349                trading_fee_delta,
1350                trading_fee_delta_usd,
1351                delta_neutrality_fee_delta,
1352                delta_neutrality_fee_delta_usd,
1353                updated_at,
1354            }: PositionUpdateEvent,
1355        ) -> Self {
1356            let event = Event::new(event_key::POSITION_UPDATE)
1357                .add_attribute(event_key::UPDATED_AT, updated_at.to_string())
1358                .add_attribute(
1359                    event_key::DEPOSIT_COLLATERAL_DELTA,
1360                    deposit_collateral_delta.to_string(),
1361                )
1362                .add_attribute(
1363                    event_key::DEPOSIT_COLLATERAL_DELTA_USD,
1364                    deposit_collateral_delta_usd.to_string(),
1365                )
1366                .add_attribute(
1367                    event_key::ACTIVE_COLLATERAL_DELTA,
1368                    active_collateral_delta.to_string(),
1369                )
1370                .add_attribute(
1371                    event_key::ACTIVE_COLLATERAL_DELTA_USD,
1372                    active_collateral_delta_usd.to_string(),
1373                )
1374                .add_attribute(
1375                    event_key::COUNTER_COLLATERAL_DELTA,
1376                    counter_collateral_delta.to_string(),
1377                )
1378                .add_attribute(
1379                    event_key::COUNTER_COLLATERAL_DELTA_USD,
1380                    counter_collateral_delta_usd.to_string(),
1381                )
1382                .add_attribute(event_key::LEVERAGE_DELTA, leverage_delta.to_string())
1383                .add_attribute(
1384                    event_key::COUNTER_LEVERAGE_DELTA,
1385                    counter_leverage_delta.to_string(),
1386                )
1387                .add_attribute(
1388                    event_key::NOTIONAL_SIZE_DELTA,
1389                    notional_size_delta.to_string(),
1390                )
1391                .add_attribute(
1392                    event_key::NOTIONAL_SIZE_DELTA_USD,
1393                    notional_size_delta_usd.to_string(),
1394                )
1395                .add_attribute(
1396                    event_key::NOTIONAL_SIZE_ABS_DELTA,
1397                    notional_size_abs_delta.to_string(),
1398                )
1399                .add_attribute(
1400                    event_key::NOTIONAL_SIZE_ABS_DELTA_USD,
1401                    notional_size_abs_delta_usd.to_string(),
1402                )
1403                .add_attribute(event_key::TRADING_FEE_DELTA, trading_fee_delta.to_string())
1404                .add_attribute(
1405                    event_key::TRADING_FEE_DELTA_USD,
1406                    trading_fee_delta_usd.to_string(),
1407                )
1408                .add_attribute(
1409                    event_key::DELTA_NEUTRALITY_FEE_DELTA,
1410                    delta_neutrality_fee_delta.to_string(),
1411                )
1412                .add_attribute(
1413                    event_key::DELTA_NEUTRALITY_FEE_DELTA_USD,
1414                    delta_neutrality_fee_delta_usd.to_string(),
1415                );
1416
1417            position_attributes.add_to_event(&event)
1418        }
1419    }
1420
1421    impl TryFrom<Event> for PositionUpdateEvent {
1422        type Error = anyhow::Error;
1423
1424        fn try_from(evt: Event) -> anyhow::Result<Self> {
1425            Ok(Self {
1426                updated_at: evt.timestamp_attr(event_key::UPDATED_AT)?,
1427                deposit_collateral_delta: evt.number_attr(event_key::DEPOSIT_COLLATERAL_DELTA)?,
1428                deposit_collateral_delta_usd: evt
1429                    .number_attr(event_key::DEPOSIT_COLLATERAL_DELTA_USD)?,
1430                active_collateral_delta: evt.number_attr(event_key::ACTIVE_COLLATERAL_DELTA)?,
1431                active_collateral_delta_usd: evt
1432                    .number_attr(event_key::ACTIVE_COLLATERAL_DELTA_USD)?,
1433                counter_collateral_delta: evt.number_attr(event_key::COUNTER_COLLATERAL_DELTA)?,
1434                counter_collateral_delta_usd: evt
1435                    .number_attr(event_key::COUNTER_COLLATERAL_DELTA_USD)?,
1436                leverage_delta: evt.number_attr(event_key::LEVERAGE_DELTA)?,
1437                counter_leverage_delta: evt.number_attr(event_key::COUNTER_LEVERAGE_DELTA)?,
1438                notional_size_delta: evt.number_attr(event_key::NOTIONAL_SIZE_DELTA)?,
1439                notional_size_delta_usd: evt.number_attr(event_key::NOTIONAL_SIZE_DELTA_USD)?,
1440                notional_size_abs_delta: evt.number_attr(event_key::NOTIONAL_SIZE_ABS_DELTA)?,
1441                notional_size_abs_delta_usd: evt
1442                    .number_attr(event_key::NOTIONAL_SIZE_ABS_DELTA_USD)?,
1443                trading_fee_delta: evt.decimal_attr(event_key::TRADING_FEE_DELTA)?,
1444                trading_fee_delta_usd: evt.decimal_attr(event_key::TRADING_FEE_DELTA_USD)?,
1445                delta_neutrality_fee_delta: evt
1446                    .number_attr(event_key::DELTA_NEUTRALITY_FEE_DELTA)?,
1447                delta_neutrality_fee_delta_usd: evt
1448                    .number_attr(event_key::DELTA_NEUTRALITY_FEE_DELTA_USD)?,
1449                position_attributes: evt.try_into()?,
1450            })
1451        }
1452    }
1453
1454    /// Emitted each time a position is saved
1455    #[derive(Clone, Copy, PartialEq, Eq, Debug)]
1456    pub struct PositionSaveEvent {
1457        /// ID of the position
1458        pub id: PositionId,
1459        /// Reason the position was saved
1460        pub reason: PositionSaveReason,
1461    }
1462
1463    /// Why was a position saved?
1464    #[derive(Clone, Copy, PartialEq, Eq, Debug)]
1465    pub enum PositionSaveReason {
1466        /// Newly opened position via market order
1467        OpenMarket,
1468        /// Update to an existing position
1469        Update,
1470        /// The crank processed this position for liquifunding
1471        Crank,
1472        /// A limit order was executed
1473        ExecuteLimitOrder,
1474        /// User attempted to set a trigger price on an existing position
1475        SetTrigger,
1476    }
1477
1478    impl PositionSaveReason {
1479        /// Get the [CongestionReason] for this value.
1480        ///
1481        /// If this user action can result in a congestion error message,
1482        /// provide the [CongestionReason] value. If [None], then this
1483        /// [PositionSaveReason] cannot be blocked because of congestion.
1484        pub fn into_congestion_reason(self) -> Option<CongestionReason> {
1485            match self {
1486                PositionSaveReason::OpenMarket => Some(CongestionReason::OpenMarket),
1487                PositionSaveReason::Update => Some(CongestionReason::Update),
1488                PositionSaveReason::Crank => None,
1489                PositionSaveReason::ExecuteLimitOrder => None,
1490                PositionSaveReason::SetTrigger => Some(CongestionReason::SetTrigger),
1491            }
1492        }
1493
1494        /// Represent as a string
1495        pub fn as_str(self) -> &'static str {
1496            match self {
1497                PositionSaveReason::OpenMarket => "open",
1498                PositionSaveReason::Update => "update",
1499                PositionSaveReason::Crank => "crank",
1500                PositionSaveReason::ExecuteLimitOrder => "limit-order",
1501                PositionSaveReason::SetTrigger => "set-trigger",
1502            }
1503        }
1504    }
1505
1506    impl From<PositionSaveEvent> for Event {
1507        fn from(PositionSaveEvent { id, reason }: PositionSaveEvent) -> Self {
1508            Event::new("position-save")
1509                .add_attribute("id", id.0)
1510                .add_attribute("reason", reason.as_str())
1511        }
1512    }
1513}