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) = self
318 .position
319 .check_liquidatable(&self.params.prices, 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 )?;
489
490 let execution_price = utils::get_execution_price_for_decrease(
491 index_token_price,
492 self.position.size_in_usd(),
493 self.position.size_in_tokens(),
494 size_delta_usd,
495 &price_impact.value,
496 self.params.acceptable_price.as_ref(),
497 self.position.is_long(),
498 )?;
499
500 Ok(ExecutionParams {
501 price_impact,
502 price_impact_diff: price_impact_diff_usd,
503 execution_price,
504 })
505 }
506
507 #[allow(clippy::type_complexity)]
509 fn swap_collateral_token_to_pnl_token(
510 market: &mut P::Market,
511 report: &mut DecreasePositionReport<P::Num, P::Signed>,
512 prices: &Prices<P::Num>,
513 swap: DecreasePositionSwapType,
514 ) -> crate::Result<Option<crate::Result<SwapReport<P::Num, <P::Num as Unsigned>::Signed>>>>
515 {
516 let is_token_in_long = report.is_output_token_long();
517 let is_secondary_output_token_long = report.is_secondary_output_token_long();
518 let (output_amount, secondary_output_amount) = report.output_amounts_mut();
519 if !output_amount.is_zero()
520 && matches!(swap, DecreasePositionSwapType::CollateralToPnlToken)
521 {
522 if is_token_in_long == is_secondary_output_token_long {
523 return Err(crate::Error::InvalidArgument(
524 "swap collateral: swap is not required",
525 ));
526 }
527
528 let token_in_amount = output_amount.clone();
529
530 match market
531 .swap(is_token_in_long, token_in_amount, prices.clone())
532 .and_then(|a| a.execute())
533 {
534 Ok(swap_report) => {
535 *secondary_output_amount = secondary_output_amount
536 .checked_add(swap_report.token_out_amount())
537 .ok_or(crate::Error::Computation(
538 "swap collateral: overflow occurred while adding token_out_amount",
539 ))?;
540 *output_amount = Zero::zero();
541 Ok(Some(Ok(swap_report)))
542 }
543 Err(err) => Ok(Some(Err(err))),
544 }
545 } else {
546 Ok(None)
547 }
548 }
549}
550
551impl<const DECIMALS: u8, P: PositionMut<DECIMALS>> MarketAction for DecreasePosition<P, DECIMALS>
552where
553 P::Market: PerpMarketMut<DECIMALS, Num = P::Num, Signed = P::Signed>,
554{
555 type Report = Box<DecreasePositionReport<P::Num, P::Signed>>;
556
557 fn execute(mut self) -> crate::Result<Self::Report> {
558 debug_assert!(
559 self.size_delta_usd <= *self.position.size_in_usd_mut(),
560 "must have been checked or capped by the position size"
561 );
562 debug_assert!(
563 self.withdrawable_collateral_amount <= *self.position.collateral_amount_mut(),
564 "must have been capped by the position collateral amount"
565 );
566
567 self.check_partial_close()?;
568 self.check_close()?;
569
570 if !matches!(self.params.swap, DecreasePositionSwapType::NoSwap)
571 && self.position.are_pnl_and_collateral_tokens_the_same()
572 {
573 self.params.swap = DecreasePositionSwapType::NoSwap;
574 }
575
576 self.check_liquidation()?;
577
578 let initial_collateral_amount = self.position.collateral_amount_mut().clone();
579
580 let mut execution = self.process_collateral()?;
581
582 let should_remove;
583 {
584 let is_long = self.position.is_long();
585 let is_collateral_long = self.position.is_collateral_token_long();
586
587 let next_position_size_in_usd = self
588 .position
589 .size_in_usd_mut()
590 .checked_sub(&self.size_delta_usd)
591 .ok_or(crate::Error::Computation(
592 "calculating next position size in usd",
593 ))?;
594 let next_position_borrowing_factor = self
595 .position
596 .market()
597 .cumulative_borrowing_factor(is_long)?;
598
599 self.position.update_total_borrowing(
601 &next_position_size_in_usd,
602 &next_position_borrowing_factor,
603 )?;
604
605 let next_position_size_in_tokens = self
606 .position
607 .size_in_tokens_mut()
608 .checked_sub(&execution.size_delta_in_tokens)
609 .ok_or(crate::Error::Computation("calculating next size in tokens"))?;
610 let next_position_collateral_amount =
611 execution.collateral.remaining_collateral_amount.clone();
612
613 should_remove =
614 next_position_size_in_usd.is_zero() || next_position_size_in_tokens.is_zero();
615
616 if should_remove {
617 *self.position.size_in_usd_mut() = Zero::zero();
618 *self.position.size_in_tokens_mut() = Zero::zero();
619 *self.position.collateral_amount_mut() = Zero::zero();
620 execution.collateral.output_amount = execution
621 .collateral
622 .output_amount
623 .checked_add(&next_position_collateral_amount)
624 .ok_or(crate::Error::Computation("calculating output amount"))?;
625 } else {
626 *self.position.size_in_usd_mut() = next_position_size_in_usd;
627 *self.position.size_in_tokens_mut() = next_position_size_in_tokens;
628 *self.position.collateral_amount_mut() = next_position_collateral_amount;
629 };
630
631 {
633 let collateral_delta_amount = initial_collateral_amount
634 .checked_sub(self.position.collateral_amount_mut())
635 .ok_or(crate::Error::Computation("collateral amount increased"))?;
636
637 self.position
638 .market_mut()
639 .collateral_sum_pool_mut(is_long)?
640 .apply_delta_amount(
641 is_collateral_long,
642 &collateral_delta_amount.to_opposite_signed()?,
643 )?;
644 }
645
646 *self.position.borrowing_factor_mut() = next_position_borrowing_factor;
648 *self.position.funding_fee_amount_per_size_mut() = self
649 .position
650 .market()
651 .funding_fee_amount_per_size(is_long, is_collateral_long)?;
652 for is_long_collateral in [true, false] {
653 *self
654 .position
655 .claimable_funding_fee_amount_per_size_mut(is_long_collateral) = self
656 .position
657 .market()
658 .claimable_funding_fee_amount_per_size(is_long, is_long_collateral)?;
659 }
660 }
661
662 self.position.update_open_interest(
664 &self.size_delta_usd.to_opposite_signed()?,
665 &execution.size_delta_in_tokens.to_opposite_signed()?,
666 )?;
667
668 if !should_remove {
669 self.position.validate(&self.params.prices, false, false)?;
670 }
671
672 self.position.on_decreased()?;
673
674 let mut report = Box::new(DecreasePositionReport::new(
675 &self.params,
676 execution,
677 self.withdrawable_collateral_amount,
678 self.size_delta_usd,
679 should_remove,
680 ));
681
682 {
684 let ty = self.params.swap;
685 let swap_result = Self::swap_collateral_token_to_pnl_token(
686 self.position.market_mut(),
687 &mut report,
688 self.params.prices(),
689 ty,
690 )?;
691
692 if let Some(result) = swap_result {
693 match result {
694 Ok(report) => {
695 self.position.on_swapped(ty, &report)?;
696 }
697 Err(err) => {
698 self.position.on_swap_error(ty, err)?;
699 }
700 }
701 }
702 }
703
704 let (output_amount, secondary_output_amount) = report.output_amounts_mut();
706 if self.position.are_pnl_and_collateral_tokens_the_same()
707 && !secondary_output_amount.is_zero()
708 {
709 *output_amount = output_amount.checked_add(secondary_output_amount).ok_or(
710 crate::Error::Computation(
711 "overflow occurred while merging the secondary output amount",
712 ),
713 )?;
714 *secondary_output_amount = Zero::zero();
715 }
716
717 Ok(report)
718 }
719}
720
721struct ExecutionParams<T: Unsigned> {
722 price_impact: PriceImpact<T::Signed>,
723 price_impact_diff: T,
724 execution_price: T,
725}
726
727#[cfg(test)]
728mod tests {
729 use crate::{
730 market::LiquidityMarketMutExt,
731 test::{TestMarket, TestPosition},
732 MarketAction,
733 };
734
735 use super::*;
736
737 #[test]
738 fn basic() -> crate::Result<()> {
739 let mut market = TestMarket::<u64, 9>::default();
740 let prices = Prices::new_for_test(120, 120, 1);
741 market.deposit(1_000_000_000, 0, prices)?.execute()?;
742 market.deposit(0, 1_000_000_000, prices)?.execute()?;
743 println!("{market:#?}");
744 let mut position = TestPosition::long(true);
745 let report = position
746 .ops(&mut market)
747 .increase(
748 Prices::new_for_test(123, 123, 1),
749 100_000_000,
750 80_000_000_000,
751 None,
752 )?
753 .execute()?;
754 println!("{report:#?}");
755 println!("{position:#?}");
756
757 let report = position
758 .ops(&mut market)
759 .decrease(
760 Prices::new_for_test(125, 125, 1),
761 40_000_000_000,
762 None,
763 100_000_000,
764 Default::default(),
765 )?
766 .execute()?;
767 println!("{report:#?}");
768 println!("{position:#?}");
769 println!("{market:#?}");
770
771 let report = position
772 .ops(&mut market)
773 .decrease(
774 Prices::new_for_test(118, 118, 1),
775 40_000_000_000,
776 None,
777 0,
778 Default::default(),
779 )?
780 .execute()?;
781 println!("{report:#?}");
782 println!("{position:#?}");
783 println!("{market:#?}");
784 Ok(())
785 }
786}