1use num_traits::{CheckedAdd, CheckedDiv, CheckedSub, Zero};
2
3use crate::{
4 market::{PerpMarket, PerpMarketExt, SwapMarketMutExt},
5 num::{MulDiv, Unsigned},
6 params::fee::PositionFees,
7 pool::delta::PriceImpact,
8 position::{
9 CollateralDelta, Position, PositionExt, PositionMut, PositionMutExt, PositionStateExt,
10 WillCollateralBeSufficient,
11 },
12 price::{Price, Prices},
13 BorrowingFeeMarketExt, PerpMarketMut, PoolExt,
14};
15
16use self::collateral_processor::{CollateralProcessor, ProcessResult};
17
18mod claimable;
19mod collateral_processor;
20mod report;
21mod utils;
22
23pub use self::{
24 claimable::ClaimableCollateral,
25 report::{DecreasePositionReport, OutputAmounts, Pnl},
26};
27
28use super::{swap::SwapReport, MarketAction};
29
30#[must_use = "actions do nothing unless you `execute` them"]
32pub struct DecreasePosition<P: Position<DECIMALS>, const DECIMALS: u8> {
33 position: P,
34 params: DecreasePositionParams<P::Num>,
35 withdrawable_collateral_amount: P::Num,
36 size_delta_usd: P::Num,
37}
38
39#[derive(
41 Debug,
42 Clone,
43 Copy,
44 Default,
45 num_enum::TryFromPrimitive,
46 num_enum::IntoPrimitive,
47 PartialEq,
48 Eq,
49 PartialOrd,
50 Ord,
51 Hash,
52)]
53#[cfg_attr(
54 feature = "strum",
55 derive(strum::EnumIter, strum::EnumString, strum::Display)
56)]
57#[cfg_attr(feature = "strum", strum(serialize_all = "snake_case"))]
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
60#[cfg_attr(
61 feature = "anchor-lang",
62 derive(
63 anchor_lang::AnchorSerialize,
64 anchor_lang::AnchorDeserialize,
65 anchor_lang::InitSpace
66 )
67)]
68#[repr(u8)]
69#[non_exhaustive]
70pub enum DecreasePositionSwapType {
71 #[default]
73 NoSwap,
74 PnlTokenToCollateralToken,
76 CollateralToPnlToken,
78}
79
80#[derive(Debug, Clone, Copy)]
82pub struct DecreasePositionParams<T> {
83 prices: Prices<T>,
84 initial_size_delta_usd: T,
85 acceptable_price: Option<T>,
86 initial_collateral_withdrawal_amount: T,
87 flags: DecreasePositionFlags,
88 swap: DecreasePositionSwapType,
89}
90
91impl<T> DecreasePositionParams<T> {
92 pub fn prices(&self) -> &Prices<T> {
94 &self.prices
95 }
96
97 pub fn initial_size_delta_usd(&self) -> &T {
99 &self.initial_size_delta_usd
100 }
101
102 pub fn acceptable_price(&self) -> Option<&T> {
104 self.acceptable_price.as_ref()
105 }
106
107 pub fn initial_collateral_withdrawal_amount(&self) -> &T {
109 &self.initial_collateral_withdrawal_amount
110 }
111
112 pub fn is_insolvent_close_allowed(&self) -> bool {
114 self.flags.is_insolvent_close_allowed
115 }
116
117 pub fn is_liquidation_order(&self) -> bool {
119 self.flags.is_liquidation_order
120 }
121
122 pub fn is_cap_size_delta_usd_allowed(&self) -> bool {
124 self.flags.is_cap_size_delta_usd_allowed
125 }
126
127 pub fn swap(&self) -> DecreasePositionSwapType {
129 self.swap
130 }
131}
132
133#[derive(Debug, Clone, Copy, Default)]
135pub struct DecreasePositionFlags {
136 pub is_insolvent_close_allowed: bool,
138 pub is_liquidation_order: bool,
140 pub is_cap_size_delta_usd_allowed: bool,
142}
143
144impl DecreasePositionFlags {
145 fn init<T>(&mut self, size_in_usd: &T, size_delta_usd: &mut T) -> crate::Result<()>
146 where
147 T: Ord + Clone,
148 {
149 if *size_delta_usd > *size_in_usd {
150 if self.is_cap_size_delta_usd_allowed {
151 *size_delta_usd = size_in_usd.clone();
152 } else {
153 return Err(crate::Error::InvalidArgument("invalid decrease order size"));
154 }
155 }
156
157 let is_full_close = *size_in_usd == *size_delta_usd;
158 self.is_insolvent_close_allowed = is_full_close && self.is_insolvent_close_allowed;
159
160 Ok(())
161 }
162}
163
164struct ProcessCollateralResult<T: Unsigned> {
165 price_impact_value: T::Signed,
166 price_impact_diff: T,
167 execution_price: T,
168 size_delta_in_tokens: T,
169 is_output_token_long: bool,
170 is_secondary_output_token_long: bool,
171 collateral: ProcessResult<T>,
172 fees: PositionFees<T>,
173 pnl: Pnl<T::Signed>,
174}
175
176impl<const DECIMALS: u8, P: PositionMut<DECIMALS>> DecreasePosition<P, DECIMALS>
177where
178 P::Market: PerpMarketMut<DECIMALS, Num = P::Num, Signed = P::Signed>,
179{
180 pub fn try_new(
182 position: P,
183 prices: Prices<P::Num>,
184 mut size_delta_usd: P::Num,
185 acceptable_price: Option<P::Num>,
186 collateral_withdrawal_amount: P::Num,
187 mut flags: DecreasePositionFlags,
188 ) -> crate::Result<Self> {
189 if !prices.is_valid() {
190 return Err(crate::Error::InvalidArgument("invalid prices"));
191 }
192 if position.is_empty() {
193 return Err(crate::Error::InvalidPosition("empty position"));
194 }
195
196 let initial_size_delta_usd = size_delta_usd.clone();
197 flags.init(position.size_in_usd(), &mut size_delta_usd)?;
198
199 Ok(Self {
200 params: DecreasePositionParams {
201 prices,
202 initial_size_delta_usd,
203 acceptable_price,
204 initial_collateral_withdrawal_amount: collateral_withdrawal_amount.clone(),
205 flags,
206 swap: DecreasePositionSwapType::NoSwap,
207 },
208 withdrawable_collateral_amount: collateral_withdrawal_amount
209 .min(position.collateral_amount().clone()),
210 size_delta_usd,
211 position,
212 })
213 }
214
215 pub fn set_swap(mut self, kind: DecreasePositionSwapType) -> Self {
217 self.params.swap = kind;
218 self
219 }
220
221 fn check_partial_close(&mut self) -> crate::Result<()> {
223 use num_traits::CheckedMul;
224
225 if self.will_size_remain() {
226 let (estimated_pnl, _, _) = self
227 .position
228 .pnl_value(&self.params.prices, self.position.size_in_usd())?;
229 let estimated_realized_pnl = self
230 .size_delta_usd
231 .checked_mul_div_with_signed_numerator(&estimated_pnl, self.position.size_in_usd())
232 .ok_or(crate::Error::Computation("estimating realized pnl"))?;
233 let estimated_remaining_pnl = estimated_pnl
234 .checked_sub(&estimated_realized_pnl)
235 .ok_or(crate::Error::Computation("estimating remaining pnl"))?;
236
237 let delta = CollateralDelta::new(
238 self.position
239 .size_in_usd()
240 .checked_sub(&self.size_delta_usd)
241 .expect("should have been capped"),
242 self.position
243 .collateral_amount()
244 .checked_sub(&self.withdrawable_collateral_amount)
245 .expect("should have been capped"),
246 estimated_realized_pnl,
247 self.size_delta_usd.to_opposite_signed()?,
248 );
249
250 let mut will_be_sufficient = self
251 .position
252 .will_collateral_be_sufficient(&self.params.prices, &delta)?;
253
254 if let WillCollateralBeSufficient::Insufficient(remaining_collateral_value) =
255 &mut will_be_sufficient
256 {
257 if self.size_delta_usd.is_zero() {
258 return Err(crate::Error::InvalidArgument(
259 "unable to withdraw collateral: insufficient collateral",
260 ));
261 }
262
263 let collateral_token_price = if self.position.is_collateral_token_long() {
264 &self.params.prices.long_token_price
265 } else {
266 &self.params.prices.short_token_price
267 };
268 let add_back = self
270 .withdrawable_collateral_amount
271 .checked_mul(collateral_token_price.pick_price(false))
272 .ok_or(crate::Error::Computation("overflow calculating add back"))?
273 .to_signed()?;
274 *remaining_collateral_value = remaining_collateral_value
275 .checked_add(&add_back)
276 .ok_or(crate::Error::Computation("adding back"))?;
277 self.withdrawable_collateral_amount = Zero::zero();
278 }
279
280 let params = self.position.market().position_params()?;
283
284 let remaining_value = will_be_sufficient
285 .checked_add(&estimated_remaining_pnl)
286 .ok_or(crate::Error::Computation("calculating remaining value"))?;
287 if remaining_value < params.min_collateral_value().to_signed()? {
288 self.size_delta_usd = self.position.size_in_usd().clone();
289 }
290
291 if *self.position.size_in_usd() > self.size_delta_usd
292 && self
293 .position
294 .size_in_usd()
295 .checked_sub(&self.size_delta_usd)
296 .expect("must success")
297 < *params.min_position_size_usd()
298 {
299 self.size_delta_usd = self.position.size_in_usd().clone();
300 }
301 }
302 Ok(())
303 }
304
305 fn check_close(&mut self) -> crate::Result<()> {
306 if self.size_delta_usd == *self.position.size_in_usd()
307 && !self.withdrawable_collateral_amount.is_zero()
308 {
309 self.withdrawable_collateral_amount = Zero::zero();
311 }
312 Ok(())
313 }
314
315 fn check_liquidation(&self) -> crate::Result<()> {
316 if self.params.is_liquidation_order() {
317 let Some(_reason) =
318 self.position
319 .check_liquidatable(&self.params.prices, true, true)?
320 else {
321 return Err(crate::Error::NotLiquidatable);
322 };
323 Ok(())
324 } else {
325 Ok(())
326 }
327 }
328
329 fn will_size_remain(&self) -> bool {
330 self.size_delta_usd < *self.position.size_in_usd()
331 }
332
333 pub fn is_full_close(&self) -> bool {
335 self.size_delta_usd == *self.position.size_in_usd()
336 }
337
338 fn collateral_token_price(&self) -> &Price<P::Num> {
339 self.position.collateral_price(self.params.prices())
340 }
341
342 #[allow(clippy::type_complexity)]
343 fn process_collateral(&mut self) -> crate::Result<ProcessCollateralResult<P::Num>> {
344 debug_assert!(!self.params.is_insolvent_close_allowed() || self.is_full_close());
346
347 let ExecutionParams {
348 price_impact,
349 price_impact_diff,
350 execution_price,
351 } = self.get_execution_params()?;
352
353 let (base_pnl_usd, uncapped_base_pnl_usd, size_delta_in_tokens) = self
355 .position
356 .pnl_value(&self.params.prices, &self.size_delta_usd)?;
357
358 let is_output_token_long = self.position.is_collateral_token_long();
359 let is_pnl_token_long = self.position.is_long();
360 let are_pnl_and_collateral_tokens_the_same =
361 self.position.are_pnl_and_collateral_tokens_the_same();
362
363 let mut fees = self.position.position_fees(
364 self.params
365 .prices
366 .collateral_token_price(is_output_token_long),
367 &self.size_delta_usd,
368 price_impact.balance_change,
369 self.params.is_liquidation_order(),
370 )?;
371
372 let remaining_collateral_amount = self.position.collateral_amount().clone();
373
374 let processor = CollateralProcessor::new(
375 self.position.market_mut(),
376 is_output_token_long,
377 is_pnl_token_long,
378 are_pnl_and_collateral_tokens_the_same,
379 &self.params.prices,
380 remaining_collateral_amount,
381 self.params.is_insolvent_close_allowed(),
382 );
383
384 let mut result = {
385 let ty = self.params.swap;
386 let mut swap_result = None;
387
388 let price_impact_value = &price_impact.value;
389 let result = processor.process(|mut ctx| {
390 ctx.add_pnl_if_positive(&base_pnl_usd)?
391 .add_price_impact_if_positive(price_impact_value)?
392 .swap_profit_to_collateral_tokens(self.params.swap, |error| {
393 swap_result = Some(error);
394 Ok(())
395 })?
396 .pay_for_funding_fees(fees.funding_fees())?
397 .pay_for_pnl_if_negative(&base_pnl_usd)?
398 .pay_for_fees_excluding_funding(&mut fees)?
399 .pay_for_price_impact_if_negative(price_impact_value)?
400 .pay_for_price_impact_diff(&price_impact_diff)?;
401 Ok(())
402 })?;
403
404 if let Some(result) = swap_result {
405 match result {
406 Ok(report) => self.position.on_swapped(ty, &report)?,
407 Err(error) => self.position.on_swap_error(ty, error)?,
408 }
409 }
410
411 result
412 };
413
414 if !self.withdrawable_collateral_amount.is_zero() && !price_impact_diff.is_zero() {
423 debug_assert!(!self.collateral_token_price().has_zero());
425 let diff_amount = price_impact_diff
426 .checked_div(self.collateral_token_price().pick_price(false))
427 .ok_or(crate::Error::Computation("calculating diff amount"))?;
428 if self.withdrawable_collateral_amount > diff_amount {
429 self.withdrawable_collateral_amount = self
430 .withdrawable_collateral_amount
431 .checked_sub(&diff_amount)
432 .ok_or(crate::Error::Computation(
433 "calculating new withdrawable amount",
434 ))?;
435 } else {
436 self.withdrawable_collateral_amount = P::Num::zero();
437 }
438 }
439
440 if self.withdrawable_collateral_amount > result.remaining_collateral_amount {
442 self.withdrawable_collateral_amount = result.remaining_collateral_amount.clone();
443 }
444
445 if !self.withdrawable_collateral_amount.is_zero() {
446 result.remaining_collateral_amount = result
447 .remaining_collateral_amount
448 .checked_sub(&self.withdrawable_collateral_amount)
449 .expect("must be success");
450 result.output_amount = result
451 .output_amount
452 .checked_add(&self.withdrawable_collateral_amount)
453 .ok_or(crate::Error::Computation(
454 "overflow occurred while adding withdrawable amount",
455 ))?;
456 }
457
458 Ok(ProcessCollateralResult {
459 price_impact_value: price_impact.value,
460 price_impact_diff,
461 execution_price,
462 size_delta_in_tokens,
463 is_output_token_long,
464 is_secondary_output_token_long: is_pnl_token_long,
465 collateral: result,
466 fees,
467 pnl: Pnl::new(base_pnl_usd, uncapped_base_pnl_usd),
468 })
469 }
470
471 fn get_execution_params(&self) -> crate::Result<ExecutionParams<P::Num>> {
472 let index_token_price = &self.params.prices.index_token_price;
473 let size_delta_usd = &self.size_delta_usd;
474
475 if size_delta_usd.is_zero() {
476 return Ok(ExecutionParams {
477 price_impact: Default::default(),
478 price_impact_diff: Zero::zero(),
479 execution_price: index_token_price
480 .pick_price(!self.position.is_long())
481 .clone(),
482 });
483 }
484
485 let (price_impact, price_impact_diff_usd) = self.position.capped_position_price_impact(
486 index_token_price,
487 &self.size_delta_usd.to_opposite_signed()?,
488 true,
489 )?;
490
491 let execution_price = utils::get_execution_price_for_decrease(
492 index_token_price,
493 self.position.size_in_usd(),
494 self.position.size_in_tokens(),
495 size_delta_usd,
496 &price_impact.value,
497 self.params.acceptable_price.as_ref(),
498 self.position.is_long(),
499 )?;
500
501 Ok(ExecutionParams {
502 price_impact,
503 price_impact_diff: price_impact_diff_usd,
504 execution_price,
505 })
506 }
507
508 #[allow(clippy::type_complexity)]
510 fn swap_collateral_token_to_pnl_token(
511 market: &mut P::Market,
512 report: &mut DecreasePositionReport<P::Num, P::Signed>,
513 prices: &Prices<P::Num>,
514 swap: DecreasePositionSwapType,
515 ) -> crate::Result<Option<crate::Result<SwapReport<P::Num, <P::Num as Unsigned>::Signed>>>>
516 {
517 let is_token_in_long = report.is_output_token_long();
518 let is_secondary_output_token_long = report.is_secondary_output_token_long();
519 let (output_amount, secondary_output_amount) = report.output_amounts_mut();
520 if !output_amount.is_zero()
521 && matches!(swap, DecreasePositionSwapType::CollateralToPnlToken)
522 {
523 if is_token_in_long == is_secondary_output_token_long {
524 return Err(crate::Error::InvalidArgument(
525 "swap collateral: swap is not required",
526 ));
527 }
528
529 let token_in_amount = output_amount.clone();
530
531 match market
532 .swap(is_token_in_long, token_in_amount, prices.clone())
533 .and_then(|a| a.execute())
534 {
535 Ok(swap_report) => {
536 *secondary_output_amount = secondary_output_amount
537 .checked_add(swap_report.token_out_amount())
538 .ok_or(crate::Error::Computation(
539 "swap collateral: overflow occurred while adding token_out_amount",
540 ))?;
541 *output_amount = Zero::zero();
542 Ok(Some(Ok(swap_report)))
543 }
544 Err(err) => Ok(Some(Err(err))),
545 }
546 } else {
547 Ok(None)
548 }
549 }
550}
551
552impl<const DECIMALS: u8, P: PositionMut<DECIMALS>> MarketAction for DecreasePosition<P, DECIMALS>
553where
554 P::Market: PerpMarketMut<DECIMALS, Num = P::Num, Signed = P::Signed>,
555{
556 type Report = Box<DecreasePositionReport<P::Num, P::Signed>>;
557
558 fn execute(mut self) -> crate::Result<Self::Report> {
559 debug_assert!(
560 self.size_delta_usd <= *self.position.size_in_usd_mut(),
561 "must have been checked or capped by the position size"
562 );
563 debug_assert!(
564 self.withdrawable_collateral_amount <= *self.position.collateral_amount_mut(),
565 "must have been capped by the position collateral amount"
566 );
567
568 self.check_partial_close()?;
569 self.check_close()?;
570
571 if !matches!(self.params.swap, DecreasePositionSwapType::NoSwap)
572 && self.position.are_pnl_and_collateral_tokens_the_same()
573 {
574 self.params.swap = DecreasePositionSwapType::NoSwap;
575 }
576
577 self.check_liquidation()?;
578
579 let initial_collateral_amount = self.position.collateral_amount_mut().clone();
580
581 let mut execution = self.process_collateral()?;
582
583 let should_remove;
584 {
585 let is_long = self.position.is_long();
586 let is_collateral_long = self.position.is_collateral_token_long();
587
588 let next_position_size_in_usd = self
589 .position
590 .size_in_usd_mut()
591 .checked_sub(&self.size_delta_usd)
592 .ok_or(crate::Error::Computation(
593 "calculating next position size in usd",
594 ))?;
595 let next_position_borrowing_factor = self
596 .position
597 .market()
598 .cumulative_borrowing_factor(is_long)?;
599
600 self.position.update_total_borrowing(
602 &next_position_size_in_usd,
603 &next_position_borrowing_factor,
604 )?;
605
606 let next_position_size_in_tokens = self
607 .position
608 .size_in_tokens_mut()
609 .checked_sub(&execution.size_delta_in_tokens)
610 .ok_or(crate::Error::Computation("calculating next size in tokens"))?;
611 let next_position_collateral_amount =
612 execution.collateral.remaining_collateral_amount.clone();
613
614 should_remove =
615 next_position_size_in_usd.is_zero() || next_position_size_in_tokens.is_zero();
616
617 if should_remove {
618 *self.position.size_in_usd_mut() = Zero::zero();
619 *self.position.size_in_tokens_mut() = Zero::zero();
620 *self.position.collateral_amount_mut() = Zero::zero();
621 execution.collateral.output_amount = execution
622 .collateral
623 .output_amount
624 .checked_add(&next_position_collateral_amount)
625 .ok_or(crate::Error::Computation("calculating output amount"))?;
626 } else {
627 *self.position.size_in_usd_mut() = next_position_size_in_usd;
628 *self.position.size_in_tokens_mut() = next_position_size_in_tokens;
629 *self.position.collateral_amount_mut() = next_position_collateral_amount;
630 };
631
632 {
634 let collateral_delta_amount = initial_collateral_amount
635 .checked_sub(self.position.collateral_amount_mut())
636 .ok_or(crate::Error::Computation("collateral amount increased"))?;
637
638 self.position
639 .market_mut()
640 .collateral_sum_pool_mut(is_long)?
641 .apply_delta_amount(
642 is_collateral_long,
643 &collateral_delta_amount.to_opposite_signed()?,
644 )?;
645 }
646
647 *self.position.borrowing_factor_mut() = next_position_borrowing_factor;
649 *self.position.funding_fee_amount_per_size_mut() = self
650 .position
651 .market()
652 .funding_fee_amount_per_size(is_long, is_collateral_long)?;
653 for is_long_collateral in [true, false] {
654 *self
655 .position
656 .claimable_funding_fee_amount_per_size_mut(is_long_collateral) = self
657 .position
658 .market()
659 .claimable_funding_fee_amount_per_size(is_long, is_long_collateral)?;
660 }
661 }
662
663 self.position.update_open_interest(
665 &self.size_delta_usd.to_opposite_signed()?,
666 &execution.size_delta_in_tokens.to_opposite_signed()?,
667 )?;
668
669 if !should_remove {
670 self.position.validate(&self.params.prices, false, false)?;
671 }
672
673 self.position.on_decreased()?;
674
675 let mut report = Box::new(DecreasePositionReport::new(
676 &self.params,
677 execution,
678 self.withdrawable_collateral_amount,
679 self.size_delta_usd,
680 should_remove,
681 ));
682
683 {
685 let ty = self.params.swap;
686 let swap_result = Self::swap_collateral_token_to_pnl_token(
687 self.position.market_mut(),
688 &mut report,
689 self.params.prices(),
690 ty,
691 )?;
692
693 if let Some(result) = swap_result {
694 match result {
695 Ok(report) => {
696 self.position.on_swapped(ty, &report)?;
697 }
698 Err(err) => {
699 self.position.on_swap_error(ty, err)?;
700 }
701 }
702 }
703 }
704
705 let (output_amount, secondary_output_amount) = report.output_amounts_mut();
707 if self.position.are_pnl_and_collateral_tokens_the_same()
708 && !secondary_output_amount.is_zero()
709 {
710 *output_amount = output_amount.checked_add(secondary_output_amount).ok_or(
711 crate::Error::Computation(
712 "overflow occurred while merging the secondary output amount",
713 ),
714 )?;
715 *secondary_output_amount = Zero::zero();
716 }
717
718 Ok(report)
719 }
720}
721
722struct ExecutionParams<T: Unsigned> {
723 price_impact: PriceImpact<T::Signed>,
724 price_impact_diff: T,
725 execution_price: T,
726}
727
728#[cfg(test)]
729mod tests {
730 use crate::{
731 market::LiquidityMarketMutExt,
732 test::{TestMarket, TestPosition},
733 MarketAction,
734 };
735
736 use super::*;
737
738 #[test]
739 fn basic() -> crate::Result<()> {
740 let mut market = TestMarket::<u64, 9>::default();
741 let prices = Prices::new_for_test(120, 120, 1);
742 market.deposit(1_000_000_000, 0, prices)?.execute()?;
743 market.deposit(0, 1_000_000_000, prices)?.execute()?;
744 println!("{market:#?}");
745 let mut position = TestPosition::long(true);
746 let report = position
747 .ops(&mut market)
748 .increase(
749 Prices::new_for_test(123, 123, 1),
750 100_000_000,
751 80_000_000_000,
752 None,
753 )?
754 .execute()?;
755 println!("{report:#?}");
756 println!("{position:#?}");
757
758 let report = position
759 .ops(&mut market)
760 .decrease(
761 Prices::new_for_test(125, 125, 1),
762 40_000_000_000,
763 None,
764 100_000_000,
765 Default::default(),
766 )?
767 .execute()?;
768 println!("{report:#?}");
769 println!("{position:#?}");
770 println!("{market:#?}");
771
772 let report = position
773 .ops(&mut market)
774 .decrease(
775 Prices::new_for_test(118, 118, 1),
776 40_000_000_000,
777 None,
778 0,
779 Default::default(),
780 )?
781 .execute()?;
782 println!("{report:#?}");
783 println!("{position:#?}");
784 println!("{market:#?}");
785 Ok(())
786 }
787}