mod closed;
mod collateral_and_usd;
pub use closed::*;
pub use collateral_and_usd::*;
use crate::prelude::*;
use anyhow::Result;
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Decimal256, OverflowError, StdResult};
use cw_storage_plus::{IntKey, Key, KeyDeserialize, Prefixer, PrimaryKey};
use std::fmt;
use std::hash::Hash;
use std::num::ParseIntError;
use std::str::FromStr;
use super::config::Config;
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Position {
pub owner: Addr,
pub id: PositionId,
pub deposit_collateral: SignedCollateralAndUsd,
pub active_collateral: NonZero<Collateral>,
pub counter_collateral: NonZero<Collateral>,
pub notional_size: Signed<Notional>,
pub created_at: Timestamp,
pub price_point_created_at: Option<Timestamp>,
pub trading_fee: CollateralAndUsd,
pub funding_fee: SignedCollateralAndUsd,
pub borrow_fee: CollateralAndUsd,
pub crank_fee: CollateralAndUsd,
pub delta_neutrality_fee: SignedCollateralAndUsd,
pub liquifunded_at: Timestamp,
pub next_liquifunding: Timestamp,
pub stop_loss_override: Option<PriceBaseInQuote>,
pub stop_loss_override_notional: Option<Price>,
pub liquidation_price: Option<Price>,
pub liquidation_margin: LiquidationMargin,
#[serde(rename = "take_profit_override")]
pub take_profit_trader: Option<TakeProfitTrader>,
#[serde(rename = "take_profit_override_notional")]
pub take_profit_trader_notional: Option<Price>,
#[serde(rename = "take_profit_price")]
pub take_profit_total: Option<Price>,
}
#[cw_serde]
#[derive(Default, Copy, Eq)]
pub struct LiquidationMargin {
pub borrow: Collateral,
pub funding: Collateral,
pub delta_neutrality: Collateral,
pub crank: Collateral,
#[serde(default)]
pub exposure: Collateral,
}
impl LiquidationMargin {
pub fn total(&self) -> Result<Collateral, OverflowError> {
((self.borrow + self.funding)? + (self.delta_neutrality + self.crank)?)? + self.exposure
}
}
#[cw_serde]
pub struct PositionsResp {
pub positions: Vec<PositionQueryResponse>,
pub pending_close: Vec<ClosedPosition>,
pub closed: Vec<ClosedPosition>,
}
#[cw_serde]
pub struct PositionQueryResponse {
pub owner: Addr,
pub id: PositionId,
pub direction_to_base: DirectionToBase,
pub leverage: LeverageToBase,
pub counter_leverage: LeverageToBase,
pub created_at: Timestamp,
pub price_point_created_at: Option<Timestamp>,
pub liquifunded_at: Timestamp,
pub trading_fee_collateral: Collateral,
pub trading_fee_usd: Usd,
pub funding_fee_collateral: Signed<Collateral>,
pub funding_fee_usd: Signed<Usd>,
pub borrow_fee_collateral: Collateral,
pub borrow_fee_usd: Usd,
pub crank_fee_collateral: Collateral,
pub crank_fee_usd: Usd,
pub delta_neutrality_fee_collateral: Signed<Collateral>,
pub delta_neutrality_fee_usd: Signed<Usd>,
pub deposit_collateral: Signed<Collateral>,
pub deposit_collateral_usd: Signed<Usd>,
pub active_collateral: NonZero<Collateral>,
pub active_collateral_usd: NonZero<Usd>,
pub counter_collateral: NonZero<Collateral>,
pub pnl_collateral: Signed<Collateral>,
pub pnl_usd: Signed<Usd>,
pub dnf_on_close_collateral: Signed<Collateral>,
pub notional_size: Signed<Notional>,
pub notional_size_in_collateral: Signed<Collateral>,
pub position_size_base: Signed<Base>,
pub position_size_usd: Signed<Usd>,
pub liquidation_price_base: Option<PriceBaseInQuote>,
pub liquidation_margin: LiquidationMargin,
#[deprecated(note = "Use take_profit_trader instead")]
pub max_gains_in_quote: Option<MaxGainsInQuote>,
pub entry_price_base: PriceBaseInQuote,
pub next_liquifunding: Timestamp,
pub stop_loss_override: Option<PriceBaseInQuote>,
#[serde(rename = "take_profit_override")]
pub take_profit_trader: Option<TakeProfitTrader>,
#[serde(rename = "take_profit_price_base")]
pub take_profit_total_base: Option<PriceBaseInQuote>,
}
impl Position {
pub fn direction(&self) -> DirectionToNotional {
if self.notional_size.is_negative() {
DirectionToNotional::Short
} else {
DirectionToNotional::Long
}
}
pub fn max_gains_in_quote(
&self,
market_type: MarketType,
price_point: &PricePoint,
) -> Result<MaxGainsInQuote> {
match market_type {
MarketType::CollateralIsQuote => Ok(MaxGainsInQuote::Finite(
self.counter_collateral
.checked_div_collateral(self.active_collateral)?,
)),
MarketType::CollateralIsBase => {
let take_profit_price = self.take_profit_price_total(price_point, market_type)?;
let take_profit_price = match take_profit_price {
Some(price) => price,
None => return Ok(MaxGainsInQuote::PosInfinity),
};
let take_profit_collateral = self
.active_collateral
.checked_add(self.counter_collateral.raw())?;
let take_profit_in_notional =
take_profit_price.collateral_to_notional_non_zero(take_profit_collateral);
let active_collateral_in_notional =
price_point.collateral_to_notional_non_zero(self.active_collateral);
anyhow::ensure!(
take_profit_in_notional > active_collateral_in_notional,
"Max gains in quote is negative, this should not be possible.
Take profit: {take_profit_in_notional}.
Active collateral: {active_collateral_in_notional}"
);
let res = (take_profit_in_notional.into_decimal256()
- active_collateral_in_notional.into_decimal256())
.checked_div(active_collateral_in_notional.into_decimal256())?;
Ok(MaxGainsInQuote::Finite(
NonZero::new(res).context("Max gains of 0")?,
))
}
}
}
pub fn active_leverage_to_notional(
&self,
price_point: &PricePoint,
) -> SignedLeverageToNotional {
SignedLeverageToNotional::calculate(self.notional_size, price_point, self.active_collateral)
}
pub fn counter_leverage_to_notional(
&self,
price_point: &PricePoint,
) -> SignedLeverageToNotional {
SignedLeverageToNotional::calculate(
self.notional_size,
price_point,
self.counter_collateral,
)
}
pub fn notional_size_in_collateral(&self, price_point: &PricePoint) -> Signed<Collateral> {
self.notional_size
.map(|x| price_point.notional_to_collateral(x))
}
pub fn position_size_base(
&self,
market_type: MarketType,
price_point: &PricePoint,
) -> Result<Signed<Base>> {
let leverage = self
.active_leverage_to_notional(price_point)
.into_base(market_type)?;
let active_collateral = price_point.collateral_to_base_non_zero(self.active_collateral);
leverage.checked_mul_base(active_collateral)
}
pub fn pnl_in_collateral(&self) -> Result<Signed<Collateral>> {
self.active_collateral.into_signed() - self.deposit_collateral.collateral()
}
pub fn pnl_in_usd(&self, price_point: &PricePoint) -> Result<Signed<Usd>> {
let active_collateral_in_usd =
price_point.collateral_to_usd_non_zero(self.active_collateral);
active_collateral_in_usd.into_signed() - self.deposit_collateral.usd()
}
pub fn liquidation_margin(
&self,
price_point: &PricePoint,
config: &Config,
) -> Result<LiquidationMargin> {
const SEC_PER_YEAR: u64 = 31_536_000;
const MS_PER_YEAR: u64 = SEC_PER_YEAR * 1000;
let ms_per_year = Decimal256::from_atomics(MS_PER_YEAR, 0).unwrap();
let duration =
Duration::from_seconds(config.liquifunding_delay_seconds.into()).as_ms_decimal_lossy();
let borrow_fee_max_rate =
config.borrow_fee_rate_max_annualized.raw() * duration / ms_per_year;
let borrow_fee_max_payment = (self
.active_collateral
.raw()
.checked_add(self.counter_collateral.raw())?)
.checked_mul_dec(borrow_fee_max_rate)?;
let max_price = match self.direction() {
DirectionToNotional::Long => {
price_point.price_notional.into_decimal256()
+ self.counter_collateral.into_decimal256()
/ self.notional_size.abs_unsigned().into_decimal256()
}
DirectionToNotional::Short => {
price_point.price_notional.into_decimal256()
+ self.active_collateral.into_decimal256()
/ self.notional_size.abs_unsigned().into_decimal256()
}
};
let funding_max_rate = config.funding_rate_max_annualized * duration / ms_per_year;
let funding_max_payment =
funding_max_rate * self.notional_size.abs_unsigned().into_decimal256() * max_price;
let slippage_max = config.delta_neutrality_fee_cap.into_decimal256()
* self.notional_size.abs_unsigned().into_decimal256()
* max_price;
Ok(LiquidationMargin {
borrow: borrow_fee_max_payment,
funding: Collateral::from_decimal256(funding_max_payment),
delta_neutrality: Collateral::from_decimal256(slippage_max),
crank: price_point.usd_to_collateral(config.crank_fee_charged),
exposure: price_point
.notional_to_collateral(self.notional_size.abs_unsigned())
.checked_mul_dec(config.exposure_margin_ratio)?,
})
}
pub fn liquidation_price(
&self,
price: Price,
active_collateral: NonZero<Collateral>,
liquidation_margin: &LiquidationMargin,
) -> Option<Price> {
let liquidation_margin = liquidation_margin.total().ok()?.into_number();
let liquidation_price = (price.into_number()
- ((active_collateral.into_number() - liquidation_margin).ok()?
/ self.notional_size.into_number())
.ok()?)
.ok()?;
Price::try_from_number(liquidation_price).ok()
}
pub fn take_profit_price_total(
&self,
price_point: &PricePoint,
market_type: MarketType,
) -> Result<Option<Price>> {
let take_profit_price_raw = price_point.price_notional.into_number().checked_add(
self.counter_collateral
.into_number()
.checked_div(self.notional_size.into_number())?,
)?;
let take_profit_price = if take_profit_price_raw.approx_eq(Number::ZERO)? {
None
} else {
debug_assert!(
take_profit_price_raw.is_positive_or_zero(),
"There should never be a calculated take profit price which is negative. In production, this is treated as 0 to indicate infinite max gains."
);
Price::try_from_number(take_profit_price_raw).ok()
};
match take_profit_price {
Some(price) => Ok(Some(price)),
None =>
match market_type {
MarketType::CollateralIsBase => Ok(None),
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)),
}
}
}
pub fn add_delta_neutrality_fee(
&mut self,
amount: Signed<Collateral>,
price_point: &PricePoint,
) -> Result<()> {
self.delta_neutrality_fee
.checked_add_assign(amount, price_point)
}
pub fn get_price_exposure(
&self,
start_price: Price,
end_price: PricePoint,
) -> Result<Signed<Collateral>> {
let price_delta = (end_price.price_notional.into_number() - start_price.into_number())?;
Ok(Signed::<Collateral>::from_number(
(price_delta * self.notional_size.into_number())?,
))
}
pub fn settle_price_exposure(
mut self,
start_price: Price,
end_price: PricePoint,
liquidation_margin: Collateral,
) -> Result<(MaybeClosedPosition, Signed<Collateral>)> {
let exposure = self.get_price_exposure(start_price, end_price)?;
let min_exposure = liquidation_margin
.into_signed()
.checked_sub(self.active_collateral.into_signed())?;
let max_exposure = self.counter_collateral.into_signed();
Ok(if exposure <= min_exposure {
(
MaybeClosedPosition::Close(ClosePositionInstructions {
pos: self,
capped_exposure: min_exposure,
additional_losses: min_exposure
.checked_sub(exposure)?
.try_into_non_negative_value()
.context("Calculated additional_losses is negative")?,
settlement_price: end_price,
reason: PositionCloseReason::Liquidated(LiquidationReason::Liquidated),
closed_during_liquifunding: true,
}),
min_exposure,
)
} else if exposure >= max_exposure {
(
MaybeClosedPosition::Close(ClosePositionInstructions {
pos: self,
capped_exposure: max_exposure,
additional_losses: Collateral::zero(),
settlement_price: end_price,
reason: PositionCloseReason::Liquidated(LiquidationReason::MaxGains),
closed_during_liquifunding: true,
}),
max_exposure,
)
} else {
self.active_collateral = self.active_collateral.checked_add_signed(exposure)?;
self.counter_collateral = self.counter_collateral.checked_sub_signed(exposure)?;
(MaybeClosedPosition::Open(self), exposure)
})
}
#[allow(clippy::too_many_arguments)]
pub fn into_query_response_extrapolate_exposure(
mut self,
start_price: PricePoint,
end_price: PricePoint,
entry_price: Price,
market_type: MarketType,
dnf_on_close_collateral: Signed<Collateral>,
) -> Result<PositionOrPendingClose> {
let exposure = self.get_price_exposure(start_price.price_notional, end_price)?;
let is_profit = exposure.is_strictly_positive();
let exposure = exposure.abs_unsigned();
let is_open = if is_profit {
match self.counter_collateral.checked_sub(exposure) {
Ok(counter_collateral) => {
self.counter_collateral = counter_collateral;
self.active_collateral = self.active_collateral.checked_add(exposure)?;
true
}
Err(_) => false,
}
} else {
match self.active_collateral.checked_sub(exposure) {
Ok(active_collateral) => {
self.active_collateral = active_collateral;
self.counter_collateral = self.counter_collateral.checked_add(exposure)?;
true
}
Err(_) => false,
}
};
if is_open {
self.into_query_response(end_price, entry_price, market_type, dnf_on_close_collateral)
.map(|pos| PositionOrPendingClose::Open(Box::new(pos)))
} else {
let direction_to_base = self.direction().into_base(market_type);
let entry_price_base = entry_price.into_base_price(market_type);
let active_collateral = if is_profit {
self.active_collateral.raw().checked_add(exposure)?
} else {
Collateral::zero()
};
let active_collateral_usd = end_price.collateral_to_usd(active_collateral);
Ok(PositionOrPendingClose::PendingClose(Box::new(
ClosedPosition {
owner: self.owner,
id: self.id,
direction_to_base,
created_at: self.created_at,
price_point_created_at: self.price_point_created_at,
liquifunded_at: self.liquifunded_at,
trading_fee_collateral: self.trading_fee.collateral(),
trading_fee_usd: self.trading_fee.usd(),
funding_fee_collateral: self.funding_fee.collateral(),
funding_fee_usd: self.funding_fee.usd(),
borrow_fee_collateral: self.borrow_fee.collateral(),
borrow_fee_usd: self.borrow_fee.usd(),
crank_fee_collateral: self.crank_fee.collateral(),
crank_fee_usd: self.crank_fee.usd(),
deposit_collateral: self.deposit_collateral.collateral(),
deposit_collateral_usd: self.deposit_collateral.usd(),
pnl_collateral: active_collateral
.into_signed()
.checked_sub(self.deposit_collateral.collateral())?,
pnl_usd: active_collateral_usd
.into_signed()
.checked_sub(self.deposit_collateral.usd())?,
notional_size: self.notional_size,
entry_price_base,
close_time: end_price.timestamp,
settlement_time: end_price.timestamp,
reason: PositionCloseReason::Liquidated(if is_profit {
LiquidationReason::MaxGains
} else {
LiquidationReason::Liquidated
}),
active_collateral,
delta_neutrality_fee_collateral: self.delta_neutrality_fee.collateral(),
delta_neutrality_fee_usd: self.delta_neutrality_fee.usd(),
liquidation_margin: Some(self.liquidation_margin),
},
)))
}
}
pub fn into_query_response(
self,
end_price: PricePoint,
entry_price: Price,
market_type: MarketType,
dnf_on_close_collateral: Signed<Collateral>,
) -> Result<PositionQueryResponse> {
let (direction_to_base, leverage) = self
.active_leverage_to_notional(&end_price)
.into_base(market_type)?
.split();
let counter_leverage = self
.counter_leverage_to_notional(&end_price)
.into_base(market_type)?
.split()
.1;
let pnl_collateral = self.pnl_in_collateral()?;
let pnl_usd = self.pnl_in_usd(&end_price)?;
let notional_size_in_collateral = self.notional_size_in_collateral(&end_price);
let position_size_base = self.position_size_base(market_type, &end_price)?;
let Self {
owner,
id,
active_collateral,
deposit_collateral,
counter_collateral,
notional_size,
created_at,
price_point_created_at,
trading_fee,
funding_fee,
borrow_fee,
crank_fee,
delta_neutrality_fee,
liquifunded_at,
next_liquifunding,
stop_loss_override,
liquidation_margin,
liquidation_price,
stop_loss_override_notional: _,
take_profit_trader,
take_profit_trader_notional: _,
take_profit_total,
} = self;
#[allow(deprecated)]
Ok(PositionQueryResponse {
owner,
id,
created_at,
price_point_created_at,
liquifunded_at,
direction_to_base,
leverage,
counter_leverage,
trading_fee_collateral: trading_fee.collateral(),
trading_fee_usd: trading_fee.usd(),
funding_fee_collateral: funding_fee.collateral(),
funding_fee_usd: funding_fee.usd(),
borrow_fee_collateral: borrow_fee.collateral(),
borrow_fee_usd: borrow_fee.usd(),
delta_neutrality_fee_collateral: delta_neutrality_fee.collateral(),
delta_neutrality_fee_usd: delta_neutrality_fee.usd(),
active_collateral,
active_collateral_usd: end_price.collateral_to_usd_non_zero(active_collateral),
deposit_collateral: deposit_collateral.collateral(),
deposit_collateral_usd: deposit_collateral.usd(),
pnl_collateral,
pnl_usd,
dnf_on_close_collateral,
notional_size,
notional_size_in_collateral,
position_size_base,
position_size_usd: position_size_base.map(|x| end_price.base_to_usd(x)),
counter_collateral,
max_gains_in_quote: None,
liquidation_price_base: liquidation_price.map(|x| x.into_base_price(market_type)),
liquidation_margin,
take_profit_total_base: take_profit_total.map(|x| x.into_base_price(market_type)),
entry_price_base: entry_price.into_base_price(market_type),
next_liquifunding,
stop_loss_override,
take_profit_trader,
crank_fee_collateral: crank_fee.collateral(),
crank_fee_usd: crank_fee.usd(),
})
}
pub fn attributes(&self) -> Vec<(&'static str, String)> {
let LiquidationMargin {
borrow: borrow_fee_max,
funding: funding_max,
delta_neutrality: slippage_max,
crank,
exposure,
} = &self.liquidation_margin;
vec![
("pos-owner", self.owner.to_string()),
("pos-id", self.id.to_string()),
("pos-active-collateral", self.active_collateral.to_string()),
(
"pos-deposit-collateral",
self.deposit_collateral.collateral().to_string(),
),
(
"pos-deposit-collateral-usd",
self.deposit_collateral.usd().to_string(),
),
("pos-trading-fee", self.trading_fee.collateral().to_string()),
("pos-trading-fee-usd", self.trading_fee.usd().to_string()),
("pos-crank-fee", self.crank_fee.collateral().to_string()),
("pos-crank-fee-usd", self.crank_fee.usd().to_string()),
(
"pos-counter-collateral",
self.counter_collateral.to_string(),
),
("pos-notional-size", self.notional_size.to_string()),
("pos-created-at", self.created_at.to_string()),
("pos-liquifunded-at", self.liquifunded_at.to_string()),
("pos-next-liquifunding", self.next_liquifunding.to_string()),
(
"pos-borrow-fee-liquidation-margin",
borrow_fee_max.to_string(),
),
("pos-funding-liquidation-margin", funding_max.to_string()),
("pos-slippage-liquidation-margin", slippage_max.to_string()),
("pos-crank-liquidation-margin", crank.to_string()),
("pos-exposure-liquidation-margin", exposure.to_string()),
]
}
}
#[cw_serde]
#[derive(Copy, PartialOrd, Ord, Eq)]
pub struct PositionId(Uint64);
#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for PositionId {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
u64::arbitrary(u).map(PositionId::new)
}
}
#[allow(clippy::derived_hash_with_manual_eq)]
impl Hash for PositionId {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.u64().hash(state);
}
}
impl PositionId {
pub fn new(x: u64) -> Self {
PositionId(x.into())
}
pub fn u64(self) -> u64 {
self.0.u64()
}
pub fn next(self) -> Self {
PositionId((self.u64() + 1).into())
}
}
impl<'a> PrimaryKey<'a> for PositionId {
type Prefix = ();
type SubPrefix = ();
type Suffix = Self;
type SuperSuffix = Self;
fn key(&self) -> Vec<Key> {
vec![Key::Val64(self.0.u64().to_cw_bytes())]
}
}
impl<'a> Prefixer<'a> for PositionId {
fn prefix(&self) -> Vec<Key> {
vec![Key::Val64(self.0.u64().to_cw_bytes())]
}
}
impl KeyDeserialize for PositionId {
type Output = PositionId;
const KEY_ELEMS: u16 = 1;
#[inline(always)]
fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
u64::from_vec(value).map(|x| PositionId(Uint64::new(x)))
}
}
impl fmt::Display for PositionId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for PositionId {
type Err = ParseIntError;
fn from_str(src: &str) -> Result<Self, ParseIntError> {
src.parse().map(|x| PositionId(Uint64::new(x)))
}
}
pub mod events {
use super::*;
use crate::constants::{event_key, event_val};
use cosmwasm_std::Event;
#[cw_serde]
pub struct PositionCollaterals {
pub deposit_collateral: Signed<Collateral>,
pub deposit_collateral_usd: Signed<Usd>,
pub active_collateral: NonZero<Collateral>,
pub counter_collateral: NonZero<Collateral>,
}
#[cw_serde]
pub struct PositionTradingFee {
pub trading_fee: Collateral,
pub trading_fee_usd: Usd,
}
pub fn calculate_base_and_quote(
market_type: MarketType,
price: Price,
amount: Number,
) -> Result<(Number, Number)> {
Ok(match market_type {
MarketType::CollateralIsQuote => (amount.checked_div(price.into_number())?, amount),
MarketType::CollateralIsBase => (amount, amount.checked_mul(price.into_number())?),
})
}
pub fn calculate_position_collaterals(pos: &Position) -> Result<PositionCollaterals> {
Ok(PositionCollaterals {
deposit_collateral: pos.deposit_collateral.collateral(),
deposit_collateral_usd: pos.deposit_collateral.usd(),
active_collateral: pos.active_collateral,
counter_collateral: pos.counter_collateral,
})
}
#[cw_serde]
pub struct PositionAttributes {
pub pos_id: PositionId,
pub owner: Addr,
pub collaterals: PositionCollaterals,
pub market_type: MarketType,
pub notional_size: Signed<Notional>,
pub notional_size_in_collateral: Signed<Collateral>,
pub notional_size_usd: Signed<Usd>,
pub trading_fee: PositionTradingFee,
pub direction: DirectionToBase,
pub leverage: LeverageToBase,
pub counter_leverage: LeverageToBase,
pub stop_loss_override: Option<PriceBaseInQuote>,
#[serde(rename = "take_profit_override")]
pub take_profit_trader: Option<TakeProfitTrader>,
}
impl PositionAttributes {
fn add_to_event(&self, event: &Event) -> Event {
let mut event = event
.clone()
.add_attribute(event_key::POS_ID, self.pos_id.to_string())
.add_attribute(event_key::POS_OWNER, self.owner.clone())
.add_attribute(
event_key::DEPOSIT_COLLATERAL,
self.collaterals.deposit_collateral.to_string(),
)
.add_attribute(
event_key::DEPOSIT_COLLATERAL_USD,
self.collaterals.deposit_collateral_usd.to_string(),
)
.add_attribute(
event_key::ACTIVE_COLLATERAL,
self.collaterals.active_collateral.to_string(),
)
.add_attribute(
event_key::COUNTER_COLLATERAL,
self.collaterals.counter_collateral.to_string(),
)
.add_attribute(
event_key::MARKET_TYPE,
match self.market_type {
MarketType::CollateralIsQuote => event_val::NOTIONAL_BASE,
MarketType::CollateralIsBase => event_val::COLLATERAL_BASE,
},
)
.add_attribute(event_key::NOTIONAL_SIZE, self.notional_size.to_string())
.add_attribute(
event_key::NOTIONAL_SIZE_IN_COLLATERAL,
self.notional_size_in_collateral.to_string(),
)
.add_attribute(
event_key::NOTIONAL_SIZE_USD,
self.notional_size_usd.to_string(),
)
.add_attribute(
event_key::TRADING_FEE,
self.trading_fee.trading_fee.to_string(),
)
.add_attribute(
event_key::TRADING_FEE_USD,
self.trading_fee.trading_fee_usd.to_string(),
)
.add_attribute(event_key::DIRECTION, self.direction.as_str())
.add_attribute(event_key::LEVERAGE, self.leverage.to_string())
.add_attribute(
event_key::COUNTER_LEVERAGE,
self.counter_leverage.to_string(),
);
if let Some(stop_loss_override) = self.stop_loss_override {
event = event.add_attribute(
event_key::STOP_LOSS_OVERRIDE,
stop_loss_override.to_string(),
);
}
if let Some(take_profit_trader) = self.take_profit_trader {
event = event.add_attribute(
event_key::TAKE_PROFIT_OVERRIDE,
take_profit_trader.to_string(),
);
}
event
}
}
impl TryFrom<Event> for PositionAttributes {
type Error = anyhow::Error;
fn try_from(evt: Event) -> anyhow::Result<Self> {
Ok(Self {
pos_id: PositionId::new(evt.u64_attr(event_key::POS_ID)?),
owner: evt.unchecked_addr_attr(event_key::POS_OWNER)?,
collaterals: PositionCollaterals {
deposit_collateral: evt.number_attr(event_key::DEPOSIT_COLLATERAL)?,
deposit_collateral_usd: evt.number_attr(event_key::DEPOSIT_COLLATERAL_USD)?,
active_collateral: evt.non_zero_attr(event_key::ACTIVE_COLLATERAL)?,
counter_collateral: evt.non_zero_attr(event_key::COUNTER_COLLATERAL)?,
},
market_type: evt.map_attr_result(event_key::MARKET_TYPE, |s| match s {
event_val::NOTIONAL_BASE => Ok(MarketType::CollateralIsQuote),
event_val::COLLATERAL_BASE => Ok(MarketType::CollateralIsBase),
_ => Err(PerpError::unimplemented().into()),
})?,
notional_size: evt.number_attr(event_key::NOTIONAL_SIZE)?,
notional_size_in_collateral: evt
.number_attr(event_key::NOTIONAL_SIZE_IN_COLLATERAL)?,
notional_size_usd: evt.number_attr(event_key::NOTIONAL_SIZE_USD)?,
trading_fee: PositionTradingFee {
trading_fee: evt.decimal_attr(event_key::TRADING_FEE)?,
trading_fee_usd: evt.decimal_attr(event_key::TRADING_FEE_USD)?,
},
direction: evt.direction_attr(event_key::DIRECTION)?,
leverage: evt.leverage_to_base_attr(event_key::LEVERAGE)?,
counter_leverage: evt.leverage_to_base_attr(event_key::COUNTER_LEVERAGE)?,
stop_loss_override: match evt.try_number_attr(event_key::STOP_LOSS_OVERRIDE)? {
None => None,
Some(stop_loss_override) => {
Some(PriceBaseInQuote::try_from_number(stop_loss_override)?)
}
},
take_profit_trader: evt
.try_map_attr(event_key::TAKE_PROFIT_OVERRIDE, |s| {
TakeProfitTrader::try_from(s)
})
.transpose()?,
})
}
}
#[derive(Debug, Clone)]
pub struct PositionCloseEvent {
pub closed_position: ClosedPosition,
}
impl TryFrom<PositionCloseEvent> for Event {
type Error = anyhow::Error;
fn try_from(
PositionCloseEvent {
closed_position:
ClosedPosition {
owner,
id,
direction_to_base,
created_at,
price_point_created_at,
liquifunded_at,
trading_fee_collateral,
trading_fee_usd,
funding_fee_collateral,
funding_fee_usd,
borrow_fee_collateral,
borrow_fee_usd,
crank_fee_collateral,
crank_fee_usd,
delta_neutrality_fee_collateral,
delta_neutrality_fee_usd,
deposit_collateral,
deposit_collateral_usd,
active_collateral,
pnl_collateral,
pnl_usd,
notional_size,
entry_price_base,
close_time,
settlement_time,
reason,
liquidation_margin,
},
}: PositionCloseEvent,
) -> anyhow::Result<Self> {
let mut event = Event::new(event_key::POSITION_CLOSE)
.add_attribute(event_key::POS_OWNER, owner.to_string())
.add_attribute(event_key::POS_ID, id.to_string())
.add_attribute(event_key::DIRECTION, direction_to_base.as_str())
.add_attribute(event_key::CREATED_AT, created_at.to_string())
.add_attribute(event_key::LIQUIFUNDED_AT, liquifunded_at.to_string())
.add_attribute(event_key::TRADING_FEE, trading_fee_collateral.to_string())
.add_attribute(event_key::TRADING_FEE_USD, trading_fee_usd.to_string())
.add_attribute(event_key::FUNDING_FEE, funding_fee_collateral.to_string())
.add_attribute(event_key::FUNDING_FEE_USD, funding_fee_usd.to_string())
.add_attribute(event_key::BORROW_FEE, borrow_fee_collateral.to_string())
.add_attribute(event_key::BORROW_FEE_USD, borrow_fee_usd.to_string())
.add_attribute(
event_key::DELTA_NEUTRALITY_FEE,
delta_neutrality_fee_collateral.to_string(),
)
.add_attribute(
event_key::DELTA_NEUTRALITY_FEE_USD,
delta_neutrality_fee_usd.to_string(),
)
.add_attribute(event_key::CRANK_FEE, crank_fee_collateral.to_string())
.add_attribute(event_key::CRANK_FEE_USD, crank_fee_usd.to_string())
.add_attribute(
event_key::DEPOSIT_COLLATERAL,
deposit_collateral.to_string(),
)
.add_attribute(
event_key::DEPOSIT_COLLATERAL_USD,
deposit_collateral_usd.to_string(),
)
.add_attribute(event_key::PNL, pnl_collateral.to_string())
.add_attribute(event_key::PNL_USD, pnl_usd.to_string())
.add_attribute(event_key::NOTIONAL_SIZE, notional_size.to_string())
.add_attribute(event_key::ENTRY_PRICE, entry_price_base.to_string())
.add_attribute(event_key::CLOSED_AT, close_time.to_string())
.add_attribute(event_key::SETTLED_AT, settlement_time.to_string())
.add_attribute(
event_key::CLOSE_REASON,
match reason {
PositionCloseReason::Liquidated(LiquidationReason::Liquidated) => {
event_val::LIQUIDATED
}
PositionCloseReason::Liquidated(LiquidationReason::MaxGains) => {
event_val::MAX_GAINS
}
PositionCloseReason::Liquidated(LiquidationReason::StopLoss) => {
event_val::STOP_LOSS
}
PositionCloseReason::Liquidated(LiquidationReason::TakeProfit) => {
event_val::TAKE_PROFIT
}
PositionCloseReason::Direct => event_val::DIRECT,
},
)
.add_attribute(event_key::ACTIVE_COLLATERAL, active_collateral.to_string());
if let Some(x) = price_point_created_at {
event = event.add_attribute(event_key::PRICE_POINT_CREATED_AT, x.to_string());
}
if let Some(x) = liquidation_margin {
event = event
.add_attribute(event_key::LIQUIDATION_MARGIN_BORROW, x.borrow.to_string())
.add_attribute(event_key::LIQUIDATION_MARGIN_FUNDING, x.funding.to_string())
.add_attribute(
event_key::LIQUIDATION_MARGIN_DNF,
x.delta_neutrality.to_string(),
)
.add_attribute(event_key::LIQUIDATION_MARGIN_CRANK, x.crank.to_string())
.add_attribute(
event_key::LIQUIDATION_MARGIN_EXPOSURE,
x.exposure.to_string(),
)
.add_attribute(event_key::LIQUIDATION_MARGIN_TOTAL, x.total()?.to_string());
}
Ok(event)
}
}
impl TryFrom<Event> for PositionCloseEvent {
type Error = anyhow::Error;
fn try_from(evt: Event) -> anyhow::Result<Self> {
let closed_position = ClosedPosition {
close_time: evt.timestamp_attr(event_key::CLOSED_AT)?,
settlement_time: evt.timestamp_attr(event_key::SETTLED_AT)?,
reason: evt.map_attr_result(event_key::CLOSE_REASON, |s| match s {
event_val::LIQUIDATED => Ok(PositionCloseReason::Liquidated(
LiquidationReason::Liquidated,
)),
event_val::MAX_GAINS => {
Ok(PositionCloseReason::Liquidated(LiquidationReason::MaxGains))
}
event_val::STOP_LOSS => {
Ok(PositionCloseReason::Liquidated(LiquidationReason::StopLoss))
}
event_val::TAKE_PROFIT => Ok(PositionCloseReason::Liquidated(
LiquidationReason::TakeProfit,
)),
event_val::DIRECT => Ok(PositionCloseReason::Direct),
_ => Err(PerpError::unimplemented().into()),
})?,
owner: evt.unchecked_addr_attr(event_key::POS_OWNER)?,
id: PositionId::new(evt.u64_attr(event_key::POS_ID)?),
direction_to_base: evt.direction_attr(event_key::DIRECTION)?,
created_at: evt.timestamp_attr(event_key::CREATED_AT)?,
price_point_created_at: evt
.try_timestamp_attr(event_key::PRICE_POINT_CREATED_AT)?,
liquifunded_at: evt.timestamp_attr(event_key::LIQUIFUNDED_AT)?,
trading_fee_collateral: evt.decimal_attr(event_key::TRADING_FEE)?,
trading_fee_usd: evt.decimal_attr(event_key::TRADING_FEE_USD)?,
funding_fee_collateral: evt.number_attr(event_key::FUNDING_FEE)?,
funding_fee_usd: evt.number_attr(event_key::FUNDING_FEE_USD)?,
borrow_fee_collateral: evt.decimal_attr(event_key::BORROW_FEE)?,
borrow_fee_usd: evt.decimal_attr(event_key::BORROW_FEE_USD)?,
crank_fee_collateral: evt.decimal_attr(event_key::CRANK_FEE)?,
crank_fee_usd: evt.decimal_attr(event_key::CRANK_FEE_USD)?,
delta_neutrality_fee_collateral: evt
.number_attr(event_key::DELTA_NEUTRALITY_FEE)?,
delta_neutrality_fee_usd: evt.number_attr(event_key::DELTA_NEUTRALITY_FEE_USD)?,
deposit_collateral: evt.number_attr(event_key::DEPOSIT_COLLATERAL)?,
deposit_collateral_usd: evt
.try_number_attr(event_key::DEPOSIT_COLLATERAL_USD)?
.unwrap_or_default(),
pnl_collateral: evt.number_attr(event_key::PNL)?,
pnl_usd: evt.number_attr(event_key::PNL_USD)?,
notional_size: evt.number_attr(event_key::NOTIONAL_SIZE)?,
entry_price_base: PriceBaseInQuote::try_from_number(
evt.number_attr(event_key::ENTRY_PRICE)?,
)?,
active_collateral: evt.decimal_attr(event_key::ACTIVE_COLLATERAL)?,
liquidation_margin: match (
evt.try_decimal_attr::<Collateral>(event_key::LIQUIDATION_MARGIN_BORROW)?,
evt.try_decimal_attr::<Collateral>(event_key::LIQUIDATION_MARGIN_FUNDING)?,
evt.try_decimal_attr::<Collateral>(event_key::LIQUIDATION_MARGIN_DNF)?,
evt.try_decimal_attr::<Collateral>(event_key::LIQUIDATION_MARGIN_CRANK)?,
evt.try_decimal_attr::<Collateral>(event_key::LIQUIDATION_MARGIN_EXPOSURE)?,
) {
(
Some(borrow),
Some(funding),
Some(delta_neutrality),
Some(crank),
Some(exposure),
) => Some(LiquidationMargin {
borrow,
funding,
delta_neutrality,
crank,
exposure,
}),
_ => None,
},
};
Ok(PositionCloseEvent { closed_position })
}
}
pub struct PositionOpenEvent {
pub position_attributes: PositionAttributes,
pub created_at: Timestamp,
pub price_point_created_at: Timestamp,
}
impl From<PositionOpenEvent> for Event {
fn from(src: PositionOpenEvent) -> Self {
let event = Event::new(event_key::POSITION_OPEN)
.add_attribute(event_key::CREATED_AT, src.created_at.to_string());
src.position_attributes.add_to_event(&event)
}
}
impl TryFrom<Event> for PositionOpenEvent {
type Error = anyhow::Error;
fn try_from(evt: Event) -> anyhow::Result<Self> {
Ok(Self {
created_at: evt.timestamp_attr(event_key::CREATED_AT)?,
price_point_created_at: evt.timestamp_attr(event_key::PRICE_POINT_CREATED_AT)?,
position_attributes: evt.try_into()?,
})
}
}
#[cw_serde]
pub struct PositionUpdateEvent {
pub position_attributes: PositionAttributes,
pub deposit_collateral_delta: Signed<Collateral>,
pub deposit_collateral_delta_usd: Signed<Usd>,
pub active_collateral_delta: Signed<Collateral>,
pub active_collateral_delta_usd: Signed<Usd>,
pub counter_collateral_delta: Signed<Collateral>,
pub counter_collateral_delta_usd: Signed<Usd>,
pub leverage_delta: Signed<Decimal256>,
pub counter_leverage_delta: Signed<Decimal256>,
pub notional_size_delta: Signed<Notional>,
pub notional_size_delta_usd: Signed<Usd>,
pub notional_size_abs_delta: Signed<Notional>,
pub notional_size_abs_delta_usd: Signed<Usd>,
pub trading_fee_delta: Collateral,
pub trading_fee_delta_usd: Usd,
pub delta_neutrality_fee_delta: Signed<Collateral>,
pub delta_neutrality_fee_delta_usd: Signed<Usd>,
pub updated_at: Timestamp,
}
impl From<PositionUpdateEvent> for Event {
fn from(
PositionUpdateEvent {
position_attributes,
deposit_collateral_delta,
deposit_collateral_delta_usd,
active_collateral_delta,
active_collateral_delta_usd,
counter_collateral_delta,
counter_collateral_delta_usd,
leverage_delta,
counter_leverage_delta,
notional_size_delta,
notional_size_delta_usd,
notional_size_abs_delta,
notional_size_abs_delta_usd,
trading_fee_delta,
trading_fee_delta_usd,
delta_neutrality_fee_delta,
delta_neutrality_fee_delta_usd,
updated_at,
}: PositionUpdateEvent,
) -> Self {
let event = Event::new(event_key::POSITION_UPDATE)
.add_attribute(event_key::UPDATED_AT, updated_at.to_string())
.add_attribute(
event_key::DEPOSIT_COLLATERAL_DELTA,
deposit_collateral_delta.to_string(),
)
.add_attribute(
event_key::DEPOSIT_COLLATERAL_DELTA_USD,
deposit_collateral_delta_usd.to_string(),
)
.add_attribute(
event_key::ACTIVE_COLLATERAL_DELTA,
active_collateral_delta.to_string(),
)
.add_attribute(
event_key::ACTIVE_COLLATERAL_DELTA_USD,
active_collateral_delta_usd.to_string(),
)
.add_attribute(
event_key::COUNTER_COLLATERAL_DELTA,
counter_collateral_delta.to_string(),
)
.add_attribute(
event_key::COUNTER_COLLATERAL_DELTA_USD,
counter_collateral_delta_usd.to_string(),
)
.add_attribute(event_key::LEVERAGE_DELTA, leverage_delta.to_string())
.add_attribute(
event_key::COUNTER_LEVERAGE_DELTA,
counter_leverage_delta.to_string(),
)
.add_attribute(
event_key::NOTIONAL_SIZE_DELTA,
notional_size_delta.to_string(),
)
.add_attribute(
event_key::NOTIONAL_SIZE_DELTA_USD,
notional_size_delta_usd.to_string(),
)
.add_attribute(
event_key::NOTIONAL_SIZE_ABS_DELTA,
notional_size_abs_delta.to_string(),
)
.add_attribute(
event_key::NOTIONAL_SIZE_ABS_DELTA_USD,
notional_size_abs_delta_usd.to_string(),
)
.add_attribute(event_key::TRADING_FEE_DELTA, trading_fee_delta.to_string())
.add_attribute(
event_key::TRADING_FEE_DELTA_USD,
trading_fee_delta_usd.to_string(),
)
.add_attribute(
event_key::DELTA_NEUTRALITY_FEE_DELTA,
delta_neutrality_fee_delta.to_string(),
)
.add_attribute(
event_key::DELTA_NEUTRALITY_FEE_DELTA_USD,
delta_neutrality_fee_delta_usd.to_string(),
);
position_attributes.add_to_event(&event)
}
}
impl TryFrom<Event> for PositionUpdateEvent {
type Error = anyhow::Error;
fn try_from(evt: Event) -> anyhow::Result<Self> {
Ok(Self {
updated_at: evt.timestamp_attr(event_key::UPDATED_AT)?,
deposit_collateral_delta: evt.number_attr(event_key::DEPOSIT_COLLATERAL_DELTA)?,
deposit_collateral_delta_usd: evt
.number_attr(event_key::DEPOSIT_COLLATERAL_DELTA_USD)?,
active_collateral_delta: evt.number_attr(event_key::ACTIVE_COLLATERAL_DELTA)?,
active_collateral_delta_usd: evt
.number_attr(event_key::ACTIVE_COLLATERAL_DELTA_USD)?,
counter_collateral_delta: evt.number_attr(event_key::COUNTER_COLLATERAL_DELTA)?,
counter_collateral_delta_usd: evt
.number_attr(event_key::COUNTER_COLLATERAL_DELTA_USD)?,
leverage_delta: evt.number_attr(event_key::LEVERAGE_DELTA)?,
counter_leverage_delta: evt.number_attr(event_key::COUNTER_LEVERAGE_DELTA)?,
notional_size_delta: evt.number_attr(event_key::NOTIONAL_SIZE_DELTA)?,
notional_size_delta_usd: evt.number_attr(event_key::NOTIONAL_SIZE_DELTA_USD)?,
notional_size_abs_delta: evt.number_attr(event_key::NOTIONAL_SIZE_ABS_DELTA)?,
notional_size_abs_delta_usd: evt
.number_attr(event_key::NOTIONAL_SIZE_ABS_DELTA_USD)?,
trading_fee_delta: evt.decimal_attr(event_key::TRADING_FEE_DELTA)?,
trading_fee_delta_usd: evt.decimal_attr(event_key::TRADING_FEE_DELTA_USD)?,
delta_neutrality_fee_delta: evt
.number_attr(event_key::DELTA_NEUTRALITY_FEE_DELTA)?,
delta_neutrality_fee_delta_usd: evt
.number_attr(event_key::DELTA_NEUTRALITY_FEE_DELTA_USD)?,
position_attributes: evt.try_into()?,
})
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct PositionSaveEvent {
pub id: PositionId,
pub reason: PositionSaveReason,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum PositionSaveReason {
OpenMarket,
Update,
Crank,
ExecuteLimitOrder,
SetTrigger,
}
impl PositionSaveReason {
pub fn into_congestion_reason(self) -> Option<CongestionReason> {
match self {
PositionSaveReason::OpenMarket => Some(CongestionReason::OpenMarket),
PositionSaveReason::Update => Some(CongestionReason::Update),
PositionSaveReason::Crank => None,
PositionSaveReason::ExecuteLimitOrder => None,
PositionSaveReason::SetTrigger => Some(CongestionReason::SetTrigger),
}
}
pub fn as_str(self) -> &'static str {
match self {
PositionSaveReason::OpenMarket => "open",
PositionSaveReason::Update => "update",
PositionSaveReason::Crank => "crank",
PositionSaveReason::ExecuteLimitOrder => "limit-order",
PositionSaveReason::SetTrigger => "set-trigger",
}
}
}
impl From<PositionSaveEvent> for Event {
fn from(PositionSaveEvent { id, reason }: PositionSaveEvent) -> Self {
Event::new("position-save")
.add_attribute("id", id.0)
.add_attribute("reason", reason.as_str())
}
}
}