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