1mod 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#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
22pub struct Position {
23 pub owner: Addr,
25 pub id: PositionId,
27 pub deposit_collateral: SignedCollateralAndUsd,
34 pub active_collateral: NonZero<Collateral>,
39 pub counter_collateral: NonZero<Collateral>,
41 pub notional_size: Signed<Notional>,
43 pub created_at: Timestamp,
45 pub price_point_created_at: Option<Timestamp>,
49 pub trading_fee: CollateralAndUsd,
53 pub funding_fee: SignedCollateralAndUsd,
59 pub borrow_fee: CollateralAndUsd,
64
65 pub crank_fee: CollateralAndUsd,
67
68 pub delta_neutrality_fee: SignedCollateralAndUsd,
72
73 pub liquifunded_at: Timestamp,
77 pub next_liquifunding: Timestamp,
83 pub stop_loss_override: Option<PriceBaseInQuote>,
85 pub stop_loss_override_notional: Option<Price>,
87 pub liquidation_price: Option<Price>,
89 pub liquidation_margin: LiquidationMargin,
91 #[serde(rename = "take_profit_override")]
94 pub take_profit_trader: Option<TakeProfitTrader>,
95 #[serde(rename = "take_profit_override_notional")]
100 pub take_profit_trader_notional: Option<Price>,
101 #[serde(rename = "take_profit_price")]
104 pub take_profit_total: Option<Price>,
105}
106
107#[cw_serde]
112#[derive(Default, Copy, Eq)]
113pub struct LiquidationMargin {
114 pub borrow: Collateral,
116 pub funding: Collateral,
118 pub delta_neutrality: Collateral,
120 pub crank: Collateral,
122 #[serde(default)]
124 pub exposure: Collateral,
125}
126
127impl LiquidationMargin {
128 pub fn total(&self) -> Result<Collateral, OverflowError> {
130 ((self.borrow + self.funding)? + (self.delta_neutrality + self.crank)?)? + self.exposure
131 }
132}
133
134#[cw_serde]
136pub struct PositionsResp {
137 pub positions: Vec<PositionQueryResponse>,
139 pub pending_close: Vec<ClosedPosition>,
144 pub closed: Vec<ClosedPosition>,
146}
147
148#[cw_serde]
150pub struct PositionQueryResponse {
151 pub owner: Addr,
153 pub id: PositionId,
155 pub direction_to_base: DirectionToBase,
157 pub leverage: LeverageToBase,
161 pub counter_leverage: LeverageToBase,
163 pub created_at: Timestamp,
165 pub price_point_created_at: Option<Timestamp>,
167 pub liquifunded_at: Timestamp,
169
170 pub trading_fee_collateral: Collateral,
174 pub trading_fee_usd: Usd,
176 pub funding_fee_collateral: Signed<Collateral>,
182 pub funding_fee_usd: Signed<Usd>,
184 pub borrow_fee_collateral: Collateral,
189 pub borrow_fee_usd: Usd,
191
192 pub crank_fee_collateral: Collateral,
194 pub crank_fee_usd: Usd,
196
197 pub delta_neutrality_fee_collateral: Signed<Collateral>,
199 pub delta_neutrality_fee_usd: Signed<Usd>,
201
202 pub deposit_collateral: Signed<Collateral>,
204 pub deposit_collateral_usd: Signed<Usd>,
206 pub active_collateral: NonZero<Collateral>,
208 pub active_collateral_usd: NonZero<Usd>,
210 pub counter_collateral: NonZero<Collateral>,
212
213 pub pnl_collateral: Signed<Collateral>,
215 pub pnl_usd: Signed<Usd>,
217
218 pub dnf_on_close_collateral: Signed<Collateral>,
220
221 pub notional_size: Signed<Notional>,
223 pub notional_size_in_collateral: Signed<Collateral>,
225
226 pub position_size_base: Signed<Base>,
232
233 pub position_size_usd: Signed<Usd>,
235
236 pub liquidation_price_base: Option<PriceBaseInQuote>,
238 pub liquidation_margin: LiquidationMargin,
240
241 #[deprecated(note = "Use take_profit_trader instead")]
243 pub max_gains_in_quote: Option<MaxGainsInQuote>,
244
245 pub entry_price_base: PriceBaseInQuote,
247
248 pub next_liquifunding: Timestamp,
250
251 pub stop_loss_override: Option<PriceBaseInQuote>,
253
254 #[serde(rename = "take_profit_override")]
257 pub take_profit_trader: Option<TakeProfitTrader>,
258 #[serde(rename = "take_profit_price_base")]
260 pub take_profit_total_base: Option<PriceBaseInQuote>,
261}
262
263impl Position {
264 pub fn direction(&self) -> DirectionToNotional {
266 if self.notional_size.is_negative() {
267 DirectionToNotional::Short
268 } else {
269 DirectionToNotional::Long
270 }
271 }
272
273 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 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 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 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 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 pub fn pnl_in_collateral(&self) -> Result<Signed<Collateral>> {
358 self.active_collateral.into_signed() - self.deposit_collateral.collateral()
359 }
360
361 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 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 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 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 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 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 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 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 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 #[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 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 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 #[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 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#[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 pub fn new(x: u64) -> Self {
802 PositionId(x.into())
803 }
804
805 pub fn u64(self) -> u64 {
807 self.0.u64()
808 }
809
810 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
859pub mod events {
861 use super::*;
862 use crate::constants::{event_key, event_val};
863 use cosmwasm_std::Event;
864
865 #[cw_serde]
867 pub struct PositionCollaterals {
868 pub deposit_collateral: Signed<Collateral>,
870 pub deposit_collateral_usd: Signed<Usd>,
872 pub active_collateral: NonZero<Collateral>,
874 pub counter_collateral: NonZero<Collateral>,
876 }
877
878 #[cw_serde]
880 pub struct PositionTradingFee {
881 pub trading_fee: Collateral,
883 pub trading_fee_usd: Usd,
885 }
886
887 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 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 #[cw_serde]
911 pub struct PositionAttributes {
912 pub pos_id: PositionId,
914 pub owner: Addr,
916 pub collaterals: PositionCollaterals,
918 pub market_type: MarketType,
920 pub notional_size: Signed<Notional>,
922 pub notional_size_in_collateral: Signed<Collateral>,
924 pub notional_size_usd: Signed<Usd>,
926 pub trading_fee: PositionTradingFee,
928 pub direction: DirectionToBase,
930 pub leverage: LeverageToBase,
932 pub counter_leverage: LeverageToBase,
934 pub stop_loss_override: Option<PriceBaseInQuote>,
936 #[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 #[derive(Debug, Clone)]
1059 pub struct PositionCloseEvent {
1060 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 .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 pub struct PositionOpenEvent {
1260 pub position_attributes: PositionAttributes,
1262 pub created_at: Timestamp,
1264 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 #[cw_serde]
1290 pub struct PositionUpdateEvent {
1291 pub position_attributes: PositionAttributes,
1293 pub deposit_collateral_delta: Signed<Collateral>,
1295 pub deposit_collateral_delta_usd: Signed<Usd>,
1297 pub active_collateral_delta: Signed<Collateral>,
1299 pub active_collateral_delta_usd: Signed<Usd>,
1301 pub counter_collateral_delta: Signed<Collateral>,
1303 pub counter_collateral_delta_usd: Signed<Usd>,
1305 pub leverage_delta: Signed<Decimal256>,
1307 pub counter_leverage_delta: Signed<Decimal256>,
1309 pub notional_size_delta: Signed<Notional>,
1311 pub notional_size_delta_usd: Signed<Usd>,
1313 pub notional_size_abs_delta: Signed<Notional>,
1319 pub notional_size_abs_delta_usd: Signed<Usd>,
1321 pub trading_fee_delta: Collateral,
1323 pub trading_fee_delta_usd: Usd,
1325 pub delta_neutrality_fee_delta: Signed<Collateral>,
1327 pub delta_neutrality_fee_delta_usd: Signed<Usd>,
1329 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 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
1456 pub struct PositionSaveEvent {
1457 pub id: PositionId,
1459 pub reason: PositionSaveReason,
1461 }
1462
1463 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
1465 pub enum PositionSaveReason {
1466 OpenMarket,
1468 Update,
1470 Crank,
1472 ExecuteLimitOrder,
1474 SetTrigger,
1476 }
1477
1478 impl PositionSaveReason {
1479 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 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}