Skip to main content

gmsol_model/action/decrease_position/
mod.rs

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/// Decrease the position.
31#[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/// Swap Type for the decrease position action.
40#[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    /// No swap.
72    #[default]
73    NoSwap,
74    /// Swap PnL token to collateral token.
75    PnlTokenToCollateralToken,
76    /// Swap collateral token to PnL token.
77    CollateralToPnlToken,
78}
79
80/// Decrease Position Params.
81#[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    /// Get prices.
93    pub fn prices(&self) -> &Prices<T> {
94        &self.prices
95    }
96
97    /// Get initial size delta usd.
98    pub fn initial_size_delta_usd(&self) -> &T {
99        &self.initial_size_delta_usd
100    }
101
102    /// Get acceptable price.
103    pub fn acceptable_price(&self) -> Option<&T> {
104        self.acceptable_price.as_ref()
105    }
106
107    /// Get initial collateral withdrawal amount.
108    pub fn initial_collateral_withdrawal_amount(&self) -> &T {
109        &self.initial_collateral_withdrawal_amount
110    }
111
112    /// Whether insolvent close is allowed.
113    pub fn is_insolvent_close_allowed(&self) -> bool {
114        self.flags.is_insolvent_close_allowed
115    }
116
117    /// Whether the order is a liquidation order.
118    pub fn is_liquidation_order(&self) -> bool {
119        self.flags.is_liquidation_order
120    }
121
122    /// Whether capping size_delta_usd is allowed.
123    pub fn is_cap_size_delta_usd_allowed(&self) -> bool {
124        self.flags.is_cap_size_delta_usd_allowed
125    }
126
127    /// Get the swap type.
128    pub fn swap(&self) -> DecreasePositionSwapType {
129        self.swap
130    }
131}
132
133/// Decrease Position Flags.
134#[derive(Debug, Clone, Copy, Default)]
135pub struct DecreasePositionFlags {
136    /// Whether insolvent close is allowed.
137    pub is_insolvent_close_allowed: bool,
138    /// Whether the order is a liquidation order.
139    pub is_liquidation_order: bool,
140    /// Whether capping size_delta_usd is allowed.
141    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    /// Create a new action to decrease the given position.
181    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    /// Set the swap type.
216    pub fn set_swap(mut self, kind: DecreasePositionSwapType) -> Self {
217        self.params.swap = kind;
218        self
219    }
220
221    /// Do a check when the position will be partially decreased.
222    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                // Add back to the estimated remaining collateral value && set withdrawable collateral amount to zero.
269                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            // Close all if collateral or position size too small.
281
282            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.is_remaining_size_too_small(params.min_position_size_usd())?
293            {
294                self.size_delta_usd = self.position.size_in_usd().clone();
295            }
296        }
297        Ok(())
298    }
299
300    fn is_remaining_size_too_small(&self, min_position_size_usd: &P::Num) -> crate::Result<bool> {
301        if self
302            .position
303            .size_in_usd()
304            .checked_sub(&self.size_delta_usd)
305            .ok_or(crate::Error::Computation(
306                "calculating remaining size_in_usd",
307            ))?
308            < *min_position_size_usd
309        {
310            return Ok(true);
311        }
312
313        // Check whether closing could drive size_in_tokens to 0; in that case
314        // we also consider the remaining size too small.
315        // In practice, size_in_tokens >= size_delta_in_tokens should hold here;
316        // this is only to avoid making too many assumptions.
317        Ok(*self.position.size_in_tokens()
318            <= self.position.size_delta_in_tokens(&self.size_delta_usd)?)
319    }
320
321    fn check_close(&mut self) -> crate::Result<()> {
322        if self.size_delta_usd == *self.position.size_in_usd()
323            && !self.withdrawable_collateral_amount.is_zero()
324        {
325            // Help ensure that the order can be executed.
326            self.withdrawable_collateral_amount = Zero::zero();
327        }
328        Ok(())
329    }
330
331    fn check_liquidation(&self) -> crate::Result<()> {
332        if self.params.is_liquidation_order() {
333            let Some(_reason) =
334                self.position
335                    .check_liquidatable(&self.params.prices, true, true)?
336            else {
337                return Err(crate::Error::NotLiquidatable);
338            };
339            Ok(())
340        } else {
341            Ok(())
342        }
343    }
344
345    fn will_size_remain(&self) -> bool {
346        self.size_delta_usd < *self.position.size_in_usd()
347    }
348
349    /// Whether the action is a full close.
350    pub fn is_full_close(&self) -> bool {
351        self.size_delta_usd == *self.position.size_in_usd()
352    }
353
354    fn collateral_token_price(&self) -> &Price<P::Num> {
355        self.position.collateral_price(self.params.prices())
356    }
357
358    #[allow(clippy::type_complexity)]
359    fn process_collateral(&mut self) -> crate::Result<ProcessCollateralResult<P::Num>> {
360        // is_insolvent_close_allowed => is_full_close
361        debug_assert!(!self.params.is_insolvent_close_allowed() || self.is_full_close());
362
363        let ExecutionParams {
364            price_impact,
365            price_impact_diff,
366            execution_price,
367        } = self.get_execution_params()?;
368
369        // Calculate position pnl usd.
370        let (base_pnl_usd, uncapped_base_pnl_usd, size_delta_in_tokens) = self
371            .position
372            .pnl_value(&self.params.prices, &self.size_delta_usd)?;
373
374        let is_output_token_long = self.position.is_collateral_token_long();
375        let is_pnl_token_long = self.position.is_long();
376        let are_pnl_and_collateral_tokens_the_same =
377            self.position.are_pnl_and_collateral_tokens_the_same();
378
379        let mut fees = self.position.position_fees(
380            self.params
381                .prices
382                .collateral_token_price(is_output_token_long),
383            &self.size_delta_usd,
384            price_impact.balance_change,
385            self.params.is_liquidation_order(),
386        )?;
387
388        let remaining_collateral_amount = self.position.collateral_amount().clone();
389
390        let processor = CollateralProcessor::new(
391            self.position.market_mut(),
392            is_output_token_long,
393            is_pnl_token_long,
394            are_pnl_and_collateral_tokens_the_same,
395            &self.params.prices,
396            remaining_collateral_amount,
397            self.params.is_insolvent_close_allowed(),
398        );
399
400        let mut result = {
401            let ty = self.params.swap;
402            let mut swap_result = None;
403
404            let price_impact_value = &price_impact.value;
405            let result = processor.process(|mut ctx| {
406                ctx.add_pnl_if_positive(&base_pnl_usd)?
407                    .add_price_impact_if_positive(price_impact_value)?
408                    .swap_profit_to_collateral_tokens(self.params.swap, |error| {
409                        swap_result = Some(error);
410                        Ok(())
411                    })?
412                    .pay_for_funding_fees(fees.funding_fees())?
413                    .pay_for_pnl_if_negative(&base_pnl_usd)?
414                    .pay_for_fees_excluding_funding(&mut fees)?
415                    .pay_for_price_impact_if_negative(price_impact_value)?
416                    .pay_for_price_impact_diff(&price_impact_diff)?;
417                Ok(())
418            })?;
419
420            if let Some(result) = swap_result {
421                match result {
422                    Ok(report) => self.position.on_swapped(ty, &report)?,
423                    Err(error) => self.position.on_swap_error(ty, error)?,
424                }
425            }
426
427            result
428        };
429
430        // Handle initial collateral delta amount with price impact diff.
431        // The price_impact_diff has been deducted from the output amount or the position's collateral
432        // to reduce the chance that the position's collateral is reduced by an unexpected amount, adjust the
433        // initial_collateral_delta_amount by the price_impact_diff_amount.
434        // This would also help to prevent the position's leverage from being unexpectedly increased
435        //
436        // note that this calculation may not be entirely accurate since it is possible that the price_impact_diff
437        // could have been paid with one of or a combination of collateral / output_amount / secondary_output_amount
438        if !self.withdrawable_collateral_amount.is_zero() && !price_impact_diff.is_zero() {
439            // The prices should have been validated to be non-zero.
440            debug_assert!(!self.collateral_token_price().has_zero());
441            let diff_amount = price_impact_diff
442                .checked_div(self.collateral_token_price().pick_price(false))
443                .ok_or(crate::Error::Computation("calculating diff amount"))?;
444            if self.withdrawable_collateral_amount > diff_amount {
445                self.withdrawable_collateral_amount = self
446                    .withdrawable_collateral_amount
447                    .checked_sub(&diff_amount)
448                    .ok_or(crate::Error::Computation(
449                        "calculating new withdrawable amount",
450                    ))?;
451            } else {
452                self.withdrawable_collateral_amount = P::Num::zero();
453            }
454        }
455
456        // Cap the withdrawal amount to the remaining collateral amount.
457        if self.withdrawable_collateral_amount > result.remaining_collateral_amount {
458            self.withdrawable_collateral_amount = result.remaining_collateral_amount.clone();
459        }
460
461        if !self.withdrawable_collateral_amount.is_zero() {
462            result.remaining_collateral_amount = result
463                .remaining_collateral_amount
464                .checked_sub(&self.withdrawable_collateral_amount)
465                .expect("must be success");
466            result.output_amount = result
467                .output_amount
468                .checked_add(&self.withdrawable_collateral_amount)
469                .ok_or(crate::Error::Computation(
470                    "overflow occurred while adding withdrawable amount",
471                ))?;
472        }
473
474        Ok(ProcessCollateralResult {
475            price_impact_value: price_impact.value,
476            price_impact_diff,
477            execution_price,
478            size_delta_in_tokens,
479            is_output_token_long,
480            is_secondary_output_token_long: is_pnl_token_long,
481            collateral: result,
482            fees,
483            pnl: Pnl::new(base_pnl_usd, uncapped_base_pnl_usd),
484        })
485    }
486
487    fn get_execution_params(&self) -> crate::Result<ExecutionParams<P::Num>> {
488        let index_token_price = &self.params.prices.index_token_price;
489        let size_delta_usd = &self.size_delta_usd;
490
491        if size_delta_usd.is_zero() {
492            return Ok(ExecutionParams {
493                price_impact: Default::default(),
494                price_impact_diff: Zero::zero(),
495                execution_price: index_token_price
496                    .pick_price(!self.position.is_long())
497                    .clone(),
498            });
499        }
500
501        let (price_impact, price_impact_diff_usd) = self.position.capped_position_price_impact(
502            index_token_price,
503            &self.size_delta_usd.to_opposite_signed()?,
504            true,
505        )?;
506
507        let execution_price = utils::get_execution_price_for_decrease(
508            index_token_price,
509            self.position.size_in_usd(),
510            self.position.size_in_tokens(),
511            size_delta_usd,
512            &price_impact.value,
513            self.params.acceptable_price.as_ref(),
514            self.position.is_long(),
515        )?;
516
517        Ok(ExecutionParams {
518            price_impact,
519            price_impact_diff: price_impact_diff_usd,
520            execution_price,
521        })
522    }
523
524    /// Swap the secondary output tokens to output tokens if needed.
525    #[allow(clippy::type_complexity)]
526    fn swap_collateral_token_to_pnl_token(
527        market: &mut P::Market,
528        report: &mut DecreasePositionReport<P::Num, P::Signed>,
529        prices: &Prices<P::Num>,
530        swap: DecreasePositionSwapType,
531    ) -> crate::Result<Option<crate::Result<SwapReport<P::Num, <P::Num as Unsigned>::Signed>>>>
532    {
533        let is_token_in_long = report.is_output_token_long();
534        let is_secondary_output_token_long = report.is_secondary_output_token_long();
535        let (output_amount, secondary_output_amount) = report.output_amounts_mut();
536        if !output_amount.is_zero()
537            && matches!(swap, DecreasePositionSwapType::CollateralToPnlToken)
538        {
539            if is_token_in_long == is_secondary_output_token_long {
540                return Err(crate::Error::InvalidArgument(
541                    "swap collateral: swap is not required",
542                ));
543            }
544
545            let token_in_amount = output_amount.clone();
546
547            match market
548                .swap(is_token_in_long, token_in_amount, prices.clone())
549                .and_then(|a| a.execute())
550            {
551                Ok(swap_report) => {
552                    *secondary_output_amount = secondary_output_amount
553                        .checked_add(swap_report.token_out_amount())
554                        .ok_or(crate::Error::Computation(
555                            "swap collateral: overflow occurred while adding token_out_amount",
556                        ))?;
557                    *output_amount = Zero::zero();
558                    Ok(Some(Ok(swap_report)))
559                }
560                Err(err) => Ok(Some(Err(err))),
561            }
562        } else {
563            Ok(None)
564        }
565    }
566}
567
568impl<const DECIMALS: u8, P: PositionMut<DECIMALS>> MarketAction for DecreasePosition<P, DECIMALS>
569where
570    P::Market: PerpMarketMut<DECIMALS, Num = P::Num, Signed = P::Signed>,
571{
572    type Report = Box<DecreasePositionReport<P::Num, P::Signed>>;
573
574    fn execute(mut self) -> crate::Result<Self::Report> {
575        debug_assert!(
576            self.size_delta_usd <= *self.position.size_in_usd_mut(),
577            "must have been checked or capped by the position size"
578        );
579        debug_assert!(
580            self.withdrawable_collateral_amount <= *self.position.collateral_amount_mut(),
581            "must have been capped by the position collateral amount"
582        );
583
584        self.check_partial_close()?;
585        self.check_close()?;
586
587        if !matches!(self.params.swap, DecreasePositionSwapType::NoSwap)
588            && self.position.are_pnl_and_collateral_tokens_the_same()
589        {
590            self.params.swap = DecreasePositionSwapType::NoSwap;
591        }
592
593        self.check_liquidation()?;
594
595        let initial_collateral_amount = self.position.collateral_amount_mut().clone();
596
597        let mut execution = self.process_collateral()?;
598
599        let should_remove;
600        {
601            let is_long = self.position.is_long();
602            let is_collateral_long = self.position.is_collateral_token_long();
603
604            let next_position_size_in_usd = self
605                .position
606                .size_in_usd_mut()
607                .checked_sub(&self.size_delta_usd)
608                .ok_or(crate::Error::Computation(
609                    "calculating next position size in usd",
610                ))?;
611            let next_position_borrowing_factor = self
612                .position
613                .market()
614                .cumulative_borrowing_factor(is_long)?;
615
616            // Update total borrowing before updating position size.
617            self.position.update_total_borrowing(
618                &next_position_size_in_usd,
619                &next_position_borrowing_factor,
620            )?;
621
622            let next_position_size_in_tokens = self
623                .position
624                .size_in_tokens_mut()
625                .checked_sub(&execution.size_delta_in_tokens)
626                .ok_or(crate::Error::Computation("calculating next size in tokens"))?;
627            let next_position_collateral_amount =
628                execution.collateral.remaining_collateral_amount.clone();
629
630            should_remove =
631                next_position_size_in_usd.is_zero() || next_position_size_in_tokens.is_zero();
632
633            if should_remove {
634                *self.position.size_in_usd_mut() = Zero::zero();
635                *self.position.size_in_tokens_mut() = Zero::zero();
636                *self.position.collateral_amount_mut() = Zero::zero();
637                execution.collateral.output_amount = execution
638                    .collateral
639                    .output_amount
640                    .checked_add(&next_position_collateral_amount)
641                    .ok_or(crate::Error::Computation("calculating output amount"))?;
642            } else {
643                *self.position.size_in_usd_mut() = next_position_size_in_usd;
644                *self.position.size_in_tokens_mut() = next_position_size_in_tokens;
645                *self.position.collateral_amount_mut() = next_position_collateral_amount;
646            };
647
648            // Update collateral sum.
649            {
650                let collateral_delta_amount = initial_collateral_amount
651                    .checked_sub(self.position.collateral_amount_mut())
652                    .ok_or(crate::Error::Computation("collateral amount increased"))?;
653
654                self.position
655                    .market_mut()
656                    .collateral_sum_pool_mut(is_long)?
657                    .apply_delta_amount(
658                        is_collateral_long,
659                        &collateral_delta_amount.to_opposite_signed()?,
660                    )?;
661            }
662
663            // The state of the position must be up-to-date, even if it is going to be removed.
664            *self.position.borrowing_factor_mut() = next_position_borrowing_factor;
665            *self.position.funding_fee_amount_per_size_mut() = self
666                .position
667                .market()
668                .funding_fee_amount_per_size(is_long, is_collateral_long)?;
669            for is_long_collateral in [true, false] {
670                *self
671                    .position
672                    .claimable_funding_fee_amount_per_size_mut(is_long_collateral) = self
673                    .position
674                    .market()
675                    .claimable_funding_fee_amount_per_size(is_long, is_long_collateral)?;
676            }
677        }
678
679        // Update open interest.
680        //
681        // Open interest is updated by exactly `-size_delta_usd` / `-size_delta_in_tokens`,
682        // which requires that the position's `size_in_usd` / `size_in_tokens` changed by
683        // exactly these amounts above. This holds because:
684        //   1. Partial decrease (the `!should_remove` branch): the sizes are set via
685        //      `checked_sub` to `size_in_usd - size_delta_usd` and
686        //      `size_in_tokens - size_delta_in_tokens`.
687        //   2. Full close (`should_remove`): both sizes are zeroed, which matches the
688        //      deltas iff the close is full on both dimensions. `check_partial_close`
689        //      (via `is_remaining_size_too_small`) promotes the order to a full close
690        //      (`size_delta_usd = size_in_usd`) whenever either the remaining
691        //      `size_in_usd` would fall below `min_position_size_usd` or
692        //      `size_delta_in_tokens(size_delta_usd) >= size_in_tokens` (i.e. the
693        //      decrease would zero out the tokens). Hence a non-full close leaves both
694        //      `size_in_usd` and `size_in_tokens` strictly positive, so `should_remove`
695        //      is reached only on a close that is full on both dimensions, and the same
696        //      `size_delta_in_tokens` helper guarantees `size_delta_in_tokens ==
697        //      size_in_tokens` there.
698        self.position.update_open_interest(
699            &self.size_delta_usd.to_opposite_signed()?,
700            &execution.size_delta_in_tokens.to_opposite_signed()?,
701        )?;
702
703        if !should_remove {
704            self.position.validate(&self.params.prices, false, false)?;
705        }
706
707        self.position.on_decreased()?;
708
709        let mut report = Box::new(DecreasePositionReport::new(
710            &self.params,
711            execution,
712            self.withdrawable_collateral_amount,
713            self.size_delta_usd,
714            should_remove,
715        ));
716
717        // Swap collateral tokens to pnl tokens.
718        {
719            let ty = self.params.swap;
720            let swap_result = Self::swap_collateral_token_to_pnl_token(
721                self.position.market_mut(),
722                &mut report,
723                self.params.prices(),
724                ty,
725            )?;
726
727            if let Some(result) = swap_result {
728                match result {
729                    Ok(report) => {
730                        self.position.on_swapped(ty, &report)?;
731                    }
732                    Err(err) => {
733                        self.position.on_swap_error(ty, err)?;
734                    }
735                }
736            }
737        }
738
739        // Merge amounts if needed.
740        let (output_amount, secondary_output_amount) = report.output_amounts_mut();
741        if self.position.are_pnl_and_collateral_tokens_the_same()
742            && !secondary_output_amount.is_zero()
743        {
744            *output_amount = output_amount.checked_add(secondary_output_amount).ok_or(
745                crate::Error::Computation(
746                    "overflow occurred while merging the secondary output amount",
747                ),
748            )?;
749            *secondary_output_amount = Zero::zero();
750        }
751
752        Ok(report)
753    }
754}
755
756struct ExecutionParams<T: Unsigned> {
757    price_impact: PriceImpact<T::Signed>,
758    price_impact_diff: T,
759    execution_price: T,
760}
761
762#[cfg(test)]
763mod tests {
764    use crate::{
765        market::LiquidityMarketMutExt,
766        test::{TestMarket, TestPosition},
767        MarketAction,
768    };
769
770    use super::*;
771
772    #[test]
773    fn basic() -> crate::Result<()> {
774        let mut market = TestMarket::<u64, 9>::default();
775        let prices = Prices::new_for_test(120, 120, 1);
776        market.deposit(1_000_000_000, 0, prices)?.execute()?;
777        market.deposit(0, 1_000_000_000, prices)?.execute()?;
778        println!("{market:#?}");
779        let mut position = TestPosition::long(true);
780        let report = position
781            .ops(&mut market)
782            .increase(
783                Prices::new_for_test(123, 123, 1),
784                100_000_000,
785                80_000_000_000,
786                None,
787            )?
788            .execute()?;
789        println!("{report:#?}");
790        println!("{position:#?}");
791
792        let report = position
793            .ops(&mut market)
794            .decrease(
795                Prices::new_for_test(125, 125, 1),
796                40_000_000_000,
797                None,
798                100_000_000,
799                Default::default(),
800            )?
801            .execute()?;
802        println!("{report:#?}");
803        println!("{position:#?}");
804        println!("{market:#?}");
805
806        let report = position
807            .ops(&mut market)
808            .decrease(
809                Prices::new_for_test(118, 118, 1),
810                40_000_000_000,
811                None,
812                0,
813                Default::default(),
814            )?
815            .execute()?;
816        println!("{report:#?}");
817        println!("{position:#?}");
818        println!("{market:#?}");
819        Ok(())
820    }
821}