gmsol_model/action/
increase_position.rs

1use num_traits::{CheckedAdd, CheckedDiv, CheckedNeg, Signed, Zero};
2use std::fmt;
3
4use crate::{
5    market::{BaseMarketExt, BaseMarketMutExt, PerpMarketExt, PositionImpactMarketMutExt},
6    num::Unsigned,
7    params::fee::PositionFees,
8    pool::delta::PriceImpact,
9    position::{CollateralDelta, Position, PositionExt},
10    price::{Price, Prices},
11    BorrowingFeeMarketExt, PerpMarketMut, PoolExt, PositionMut, PositionMutExt,
12};
13
14use super::MarketAction;
15
16/// Increase the position.
17#[must_use = "actions do nothing unless you `execute` them"]
18pub struct IncreasePosition<P: Position<DECIMALS>, const DECIMALS: u8> {
19    position: P,
20    params: IncreasePositionParams<P::Num>,
21}
22
23/// Increase Position Params.
24#[derive(Debug, Clone, Copy)]
25#[cfg_attr(
26    feature = "anchor-lang",
27    derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
28)]
29pub struct IncreasePositionParams<T> {
30    collateral_increment_amount: T,
31    size_delta_usd: T,
32    acceptable_price: Option<T>,
33    prices: Prices<T>,
34}
35
36#[cfg(feature = "gmsol-utils")]
37impl<T: gmsol_utils::InitSpace> gmsol_utils::InitSpace for IncreasePositionParams<T> {
38    const INIT_SPACE: usize = 2 * T::INIT_SPACE + 1 + T::INIT_SPACE + Prices::<T>::INIT_SPACE;
39}
40
41impl<T> IncreasePositionParams<T> {
42    /// Get collateral increment amount.
43    pub fn collateral_increment_amount(&self) -> &T {
44        &self.collateral_increment_amount
45    }
46
47    /// Get size delta USD.
48    pub fn size_delta_usd(&self) -> &T {
49        &self.size_delta_usd
50    }
51
52    /// Get acceptable price.
53    pub fn acceptable_price(&self) -> Option<&T> {
54        self.acceptable_price.as_ref()
55    }
56
57    /// Get prices.
58    pub fn prices(&self) -> &Prices<T> {
59        &self.prices
60    }
61}
62
63/// Report of the execution of position increasing.
64#[must_use = "`claimable_funding_amounts` must be used"]
65#[cfg_attr(
66    feature = "anchor-lang",
67    derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
68)]
69pub struct IncreasePositionReport<Unsigned, Signed> {
70    params: IncreasePositionParams<Unsigned>,
71    execution: ExecutionParams<Unsigned, Signed>,
72    collateral_delta_amount: Signed,
73    fees: PositionFees<Unsigned>,
74    /// Output amounts that must be processed.
75    claimable_funding_long_token_amount: Unsigned,
76    claimable_funding_short_token_amount: Unsigned,
77}
78
79#[cfg(feature = "gmsol-utils")]
80impl<Unsigned, Signed> gmsol_utils::InitSpace for IncreasePositionReport<Unsigned, Signed>
81where
82    Unsigned: gmsol_utils::InitSpace,
83    Signed: gmsol_utils::InitSpace,
84{
85    const INIT_SPACE: usize = IncreasePositionParams::<Unsigned>::INIT_SPACE
86        + ExecutionParams::<Unsigned, Signed>::INIT_SPACE
87        + Signed::INIT_SPACE
88        + PositionFees::<Unsigned>::INIT_SPACE
89        + 2 * Unsigned::INIT_SPACE;
90}
91
92impl<T: Unsigned + fmt::Debug> fmt::Debug for IncreasePositionReport<T, T::Signed>
93where
94    T::Signed: fmt::Debug,
95{
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        f.debug_struct("IncreasePositionReport")
98            .field("params", &self.params)
99            .field("execution", &self.execution)
100            .field("collateral_delta_amount", &self.collateral_delta_amount)
101            .field("fees", &self.fees)
102            .field(
103                "claimable_funding_long_token_amount",
104                &self.claimable_funding_long_token_amount,
105            )
106            .field(
107                "claimable_funding_short_token_amount",
108                &self.claimable_funding_short_token_amount,
109            )
110            .finish()
111    }
112}
113
114impl<T: Unsigned + Clone> IncreasePositionReport<T, T::Signed> {
115    fn new(
116        params: IncreasePositionParams<T>,
117        execution: ExecutionParams<T, T::Signed>,
118        collateral_delta_amount: T::Signed,
119        fees: PositionFees<T>,
120    ) -> Self {
121        let claimable_funding_long_token_amount =
122            fees.funding_fees().claimable_long_token_amount().clone();
123        let claimable_funding_short_token_amount =
124            fees.funding_fees().claimable_short_token_amount().clone();
125        Self {
126            params,
127            execution,
128            collateral_delta_amount,
129            fees,
130            claimable_funding_long_token_amount,
131            claimable_funding_short_token_amount,
132        }
133    }
134
135    /// Get claimable funding amounts, returns `(long_amount, short_amount)`.
136    #[must_use = "the returned amounts of tokens should be transferred out from the market vault"]
137    pub fn claimable_funding_amounts(&self) -> (&T, &T) {
138        (
139            &self.claimable_funding_long_token_amount,
140            &self.claimable_funding_short_token_amount,
141        )
142    }
143
144    /// Get params.
145    pub fn params(&self) -> &IncreasePositionParams<T> {
146        &self.params
147    }
148
149    /// Get execution params.
150    pub fn execution(&self) -> &ExecutionParams<T, T::Signed> {
151        &self.execution
152    }
153
154    /// Get collateral delta amount.
155    pub fn collateral_delta_amount(&self) -> &T::Signed {
156        &self.collateral_delta_amount
157    }
158
159    /// Get position fees.
160    pub fn fees(&self) -> &PositionFees<T> {
161        &self.fees
162    }
163}
164
165/// Execution Params for increasing position.
166#[derive(Debug, Clone, Copy)]
167#[cfg_attr(
168    feature = "anchor-lang",
169    derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
170)]
171pub struct ExecutionParams<Unsigned, Signed> {
172    price_impact_value: Signed,
173    price_impact_amount: Signed,
174    size_delta_in_tokens: Unsigned,
175    execution_price: Unsigned,
176}
177
178#[cfg(feature = "gmsol-utils")]
179impl<Unsigned, Signed> gmsol_utils::InitSpace for ExecutionParams<Unsigned, Signed>
180where
181    Unsigned: gmsol_utils::InitSpace,
182    Signed: gmsol_utils::InitSpace,
183{
184    const INIT_SPACE: usize = 2 * Signed::INIT_SPACE + 2 * Unsigned::INIT_SPACE;
185}
186
187impl<T: Unsigned> ExecutionParams<T, T::Signed> {
188    /// Get price impact value.
189    pub fn price_impact_value(&self) -> &T::Signed {
190        &self.price_impact_value
191    }
192
193    /// Get price impact amount.
194    pub fn price_impact_amount(&self) -> &T::Signed {
195        &self.price_impact_amount
196    }
197
198    /// Get size delta in tokens.
199    pub fn size_delta_in_tokens(&self) -> &T {
200        &self.size_delta_in_tokens
201    }
202
203    /// Get execution price.
204    pub fn execution_price(&self) -> &T {
205        &self.execution_price
206    }
207}
208
209impl<const DECIMALS: u8, P: PositionMut<DECIMALS>> IncreasePosition<P, DECIMALS>
210where
211    P::Market: PerpMarketMut<DECIMALS, Num = P::Num, Signed = P::Signed>,
212{
213    /// Create a new action to increase the given position.
214    pub fn try_new(
215        position: P,
216        prices: Prices<P::Num>,
217        collateral_increment_amount: P::Num,
218        size_delta_usd: P::Num,
219        acceptable_price: Option<P::Num>,
220    ) -> crate::Result<Self> {
221        if !prices.is_valid() {
222            return Err(crate::Error::InvalidArgument("invalid prices"));
223        }
224        Ok(Self {
225            position,
226            params: IncreasePositionParams {
227                collateral_increment_amount,
228                size_delta_usd,
229                acceptable_price,
230                prices,
231            },
232        })
233    }
234
235    fn initialize_position_if_empty(&mut self) -> crate::Result<()> {
236        if self.position.size_in_usd().is_zero() {
237            // Ensure that the size in tokens is initialized to zero.
238            *self.position.size_in_tokens_mut() = P::Num::zero();
239            let funding_fee_amount_per_size = self.position.market().funding_fee_amount_per_size(
240                self.position.is_long(),
241                self.position.is_collateral_token_long(),
242            )?;
243            *self.position.funding_fee_amount_per_size_mut() = funding_fee_amount_per_size;
244            for is_long_collateral in [true, false] {
245                let claimable_funding_fee_amount_per_size = self
246                    .position
247                    .market()
248                    .claimable_funding_fee_amount_per_size(
249                        self.position.is_long(),
250                        is_long_collateral,
251                    )?;
252                *self
253                    .position
254                    .claimable_funding_fee_amount_per_size_mut(is_long_collateral) =
255                    claimable_funding_fee_amount_per_size;
256            }
257        }
258        Ok(())
259    }
260
261    fn get_execution_params(&self) -> crate::Result<ExecutionParamsWithPriceImpact<P::Num>> {
262        let index_token_price = &self.params.prices.index_token_price;
263        if self.params.size_delta_usd.is_zero() {
264            return Ok(ExecutionParamsWithPriceImpact {
265                execution: ExecutionParams {
266                    price_impact_value: Zero::zero(),
267                    price_impact_amount: Zero::zero(),
268                    size_delta_in_tokens: Zero::zero(),
269                    execution_price: index_token_price
270                        .pick_price(self.position.is_long())
271                        .clone(),
272                },
273                price_impact: Default::default(),
274            });
275        }
276
277        let price_impact = self.position.capped_positive_position_price_impact(
278            index_token_price,
279            &self.params.size_delta_usd.to_signed()?,
280        )?;
281
282        let price_impact_value = &price_impact.value;
283        let price_impact_amount = if price_impact_value.is_positive() {
284            let price: P::Signed = self
285                .params
286                .prices
287                .index_token_price
288                .pick_price(true)
289                .clone()
290                .try_into()
291                .map_err(|_| crate::Error::Convert)?;
292            debug_assert!(
293                !price.is_zero(),
294                "price must have been checked to be non-zero"
295            );
296            price_impact_value
297                .checked_div(&price)
298                .ok_or(crate::Error::Computation("calculating price impact amount"))?
299        } else {
300            self.params
301                .prices
302                .index_token_price
303                .pick_price(false)
304                .as_divisor_to_round_up_magnitude_div(price_impact_value)
305                .ok_or(crate::Error::Computation("calculating price impact amount"))?
306        };
307
308        // Base size delta in tokens.
309        let mut size_delta_in_tokens = if self.position.is_long() {
310            let price = self.params.prices.index_token_price.pick_price(true);
311            debug_assert!(
312                !price.is_zero(),
313                "price must have been checked to be non-zero"
314            );
315            self.params
316                .size_delta_usd
317                .checked_div(price)
318                .ok_or(crate::Error::Computation(
319                    "calculating size delta in tokens",
320                ))?
321        } else {
322            let price = self.params.prices.index_token_price.pick_price(false);
323            self.params
324                .size_delta_usd
325                .checked_round_up_div(price)
326                .ok_or(crate::Error::Computation(
327                    "calculating size delta in tokens",
328                ))?
329        };
330
331        // Apply price impact.
332        size_delta_in_tokens = if self.position.is_long() {
333            size_delta_in_tokens.checked_add_with_signed(&price_impact_amount)
334        } else {
335            size_delta_in_tokens.checked_sub_with_signed(&price_impact_amount)
336        }
337        .ok_or(crate::Error::Computation(
338            "price impact larger than order size",
339        ))?;
340
341        let execution_price = get_execution_price_for_increase(
342            &self.params.size_delta_usd,
343            &size_delta_in_tokens,
344            self.params.acceptable_price.as_ref(),
345            self.position.is_long(),
346        )?;
347
348        Ok(ExecutionParamsWithPriceImpact {
349            execution: ExecutionParams {
350                price_impact_value: price_impact.value.clone(),
351                price_impact_amount,
352                size_delta_in_tokens,
353                execution_price,
354            },
355            price_impact,
356        })
357    }
358
359    #[inline]
360    fn collateral_price(&self) -> &Price<P::Num> {
361        self.position.collateral_price(&self.params.prices)
362    }
363
364    fn process_collateral(
365        &mut self,
366        price_impact: &PriceImpact<P::Signed>,
367    ) -> crate::Result<(P::Signed, PositionFees<P::Num>)> {
368        use num_traits::CheckedSub;
369
370        let mut collateral_delta_amount = self.params.collateral_increment_amount.to_signed()?;
371
372        let fees = self.position.position_fees(
373            self.collateral_price(),
374            &self.params.size_delta_usd,
375            price_impact.balance_change,
376            false,
377        )?;
378
379        collateral_delta_amount = collateral_delta_amount
380            .checked_sub(&fees.total_cost_amount()?.to_signed()?)
381            .ok_or(crate::Error::Computation(
382                "applying fees to collateral amount",
383            ))?;
384
385        let is_collateral_token_long = self.position.is_collateral_token_long();
386
387        self.position
388            .market_mut()
389            .apply_delta_to_claimable_fee_pool(
390                is_collateral_token_long,
391                &fees.for_receiver()?.to_signed()?,
392            )?;
393
394        self.position
395            .market_mut()
396            .apply_delta(is_collateral_token_long, &fees.for_pool()?.to_signed()?)?;
397
398        let is_long = self.position.is_long();
399        self.position
400            .market_mut()
401            .collateral_sum_pool_mut(is_long)?
402            .apply_delta_amount(is_collateral_token_long, &collateral_delta_amount)?;
403
404        Ok((collateral_delta_amount, fees))
405    }
406}
407
408fn get_execution_price_for_increase<T>(
409    size_delta_usd: &T,
410    size_delta_in_tokens: &T,
411    acceptable_price: Option<&T>,
412    is_long: bool,
413) -> crate::Result<T>
414where
415    T: num_traits::Num + Ord + Clone + CheckedDiv,
416{
417    if size_delta_usd.is_zero() {
418        return Err(crate::Error::Computation("empty size delta in tokens"));
419    }
420
421    let execution_price = size_delta_usd
422        .checked_div(size_delta_in_tokens)
423        .ok_or(crate::Error::Computation("calculating execution price"))?;
424
425    let Some(acceptable_price) = acceptable_price else {
426        return Ok(execution_price);
427    };
428
429    if (is_long && execution_price <= *acceptable_price)
430        || (!is_long && execution_price >= *acceptable_price)
431    {
432        Ok(execution_price)
433    } else {
434        Err(crate::Error::InvalidArgument(
435            "order not fulfillable at acceptable price",
436        ))
437    }
438}
439
440impl<const DECIMALS: u8, P: PositionMut<DECIMALS>> MarketAction for IncreasePosition<P, DECIMALS>
441where
442    P::Market: PerpMarketMut<DECIMALS, Num = P::Num, Signed = P::Signed>,
443{
444    type Report = IncreasePositionReport<P::Num, P::Signed>;
445
446    fn execute(mut self) -> crate::Result<Self::Report> {
447        self.initialize_position_if_empty()?;
448
449        let ExecutionParamsWithPriceImpact {
450            execution,
451            price_impact,
452        } = self.get_execution_params()?;
453
454        let (collateral_delta_amount, fees) = self.process_collateral(&price_impact)?;
455
456        let is_collateral_delta_positive = collateral_delta_amount.is_positive();
457        *self.position.collateral_amount_mut() = self
458            .position
459            .collateral_amount_mut()
460            .checked_add_with_signed(&collateral_delta_amount)
461            .ok_or({
462                if is_collateral_delta_positive {
463                    crate::Error::Computation("collateral amount overflow")
464                } else {
465                    crate::Error::InvalidArgument("insufficient collateral amount")
466                }
467            })?;
468
469        self.position
470            .market_mut()
471            .apply_delta_to_position_impact_pool(
472                &execution
473                    .price_impact_amount()
474                    .checked_neg()
475                    .ok_or(crate::Error::Computation(
476                        "calculating position impact pool delta amount",
477                    ))?,
478            )?;
479
480        let is_long = self.position.is_long();
481        let next_position_size_in_usd = self
482            .position
483            .size_in_usd_mut()
484            .checked_add(&self.params.size_delta_usd)
485            .ok_or(crate::Error::Computation("size in usd overflow"))?;
486        let next_position_borrowing_factor = self
487            .position
488            .market()
489            .cumulative_borrowing_factor(is_long)?;
490
491        // Update total borrowing before updating position size.
492        self.position
493            .update_total_borrowing(&next_position_size_in_usd, &next_position_borrowing_factor)?;
494
495        // Update sizes.
496        *self.position.size_in_usd_mut() = next_position_size_in_usd;
497        *self.position.size_in_tokens_mut() = self
498            .position
499            .size_in_tokens_mut()
500            .checked_add(&execution.size_delta_in_tokens)
501            .ok_or(crate::Error::Computation("size in tokens overflow"))?;
502
503        // Update funding fees state.
504        *self.position.funding_fee_amount_per_size_mut() = self
505            .position
506            .market()
507            .funding_fee_amount_per_size(is_long, self.position.is_collateral_token_long())?;
508        for is_long_collateral in [true, false] {
509            *self
510                .position
511                .claimable_funding_fee_amount_per_size_mut(is_long_collateral) = self
512                .position
513                .market()
514                .claimable_funding_fee_amount_per_size(is_long, is_long_collateral)?;
515        }
516
517        // Update borrowing fee state.
518        *self.position.borrowing_factor_mut() = next_position_borrowing_factor;
519
520        self.position.update_open_interest(
521            &self.params.size_delta_usd.to_signed()?,
522            &execution.size_delta_in_tokens.to_signed()?,
523        )?;
524
525        if !self.params.size_delta_usd.is_zero() {
526            let market = self.position.market();
527            market.validate_reserve(&self.params.prices, self.position.is_long())?;
528            market.validate_open_interest_reserve(&self.params.prices, self.position.is_long())?;
529
530            let delta = CollateralDelta::new(
531                self.position.size_in_usd().clone(),
532                self.position.collateral_amount().clone(),
533                Zero::zero(),
534                Zero::zero(),
535            );
536            let will_collateral_be_sufficient = self
537                .position
538                .will_collateral_be_sufficient(&self.params.prices, &delta)?;
539
540            if !will_collateral_be_sufficient.is_sufficient() {
541                return Err(crate::Error::InvalidArgument("insufficient collateral usd"));
542            }
543        }
544
545        self.position.validate(&self.params.prices, true, true)?;
546
547        self.position.on_increased()?;
548
549        Ok(IncreasePositionReport::new(
550            self.params,
551            execution,
552            collateral_delta_amount,
553            fees,
554        ))
555    }
556}
557
558struct ExecutionParamsWithPriceImpact<T: Unsigned> {
559    execution: ExecutionParams<T, T::Signed>,
560    price_impact: PriceImpact<T::Signed>,
561}
562
563#[cfg(test)]
564mod tests {
565    use crate::{
566        market::LiquidityMarketMutExt,
567        test::{TestMarket, TestPosition},
568        MarketAction,
569    };
570
571    use super::*;
572
573    #[test]
574    fn basic() -> crate::Result<()> {
575        let mut market = TestMarket::<u64, 9>::default();
576        let prices = Prices::new_for_test(120, 120, 1);
577        market.deposit(1_000_000_000, 0, prices)?.execute()?;
578        market.deposit(0, 1_000_000_000, prices)?.execute()?;
579        println!("{market:#?}");
580        let mut position = TestPosition::long(true);
581        let report = position
582            .ops(&mut market)
583            .increase(
584                Prices::new_for_test(123, 123, 1),
585                100_000_000,
586                8_000_000_000,
587                None,
588            )?
589            .execute()?;
590        println!("{report:#?}");
591        println!("{position:#?}");
592        Ok(())
593    }
594}