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