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
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            // Help ensure that the order can be executed.
310            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    /// Whether the action is a full close.
334    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        // is_insolvent_close_allowed => is_full_close
345        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        // Calculate position pnl usd.
354        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        // Handle initial collateral delta amount with price impact diff.
415        // The price_impact_diff has been deducted from the output amount or the position's collateral
416        // to reduce the chance that the position's collateral is reduced by an unexpected amount, adjust the
417        // initial_collateral_delta_amount by the price_impact_diff_amount.
418        // This would also help to prevent the position's leverage from being unexpectedly increased
419        //
420        // note that this calculation may not be entirely accurate since it is possible that the price_impact_diff
421        // could have been paid with one of or a combination of collateral / output_amount / secondary_output_amount
422        if !self.withdrawable_collateral_amount.is_zero() && !price_impact_diff.is_zero() {
423            // The prices should have been validated to be non-zero.
424            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        // Cap the withdrawal amount to the remaining collateral amount.
441        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    /// Swap the secondary output tokens to output tokens if needed.
509    #[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            // Update total borrowing before updating position size.
601            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            // Update collateral sum.
633            {
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            // The state of the position must be up-to-date, even if it is going to be removed.
648            *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        // Update open interest.
664        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        // Swap collateral tokens to pnl tokens.
684        {
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        // Merge amounts if needed.
706        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}