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) = 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    /// 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        )?;
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    /// Swap the secondary output tokens to output tokens if needed.
508    #[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            // Update total borrowing before updating position size.
600            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            // Update collateral sum.
632            {
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            // The state of the position must be up-to-date, even if it is going to be removed.
647            *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        // Update open interest.
663        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        // Swap collateral tokens to pnl tokens.
683        {
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        // Merge amounts if needed.
705        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}