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            true,
281        )?;
282
283        let price_impact_value = &price_impact.value;
284        let price_impact_amount = if price_impact_value.is_positive() {
285            let price: P::Signed = self
286                .params
287                .prices
288                .index_token_price
289                .pick_price(true)
290                .clone()
291                .try_into()
292                .map_err(|_| crate::Error::Convert)?;
293            debug_assert!(
294                !price.is_zero(),
295                "price must have been checked to be non-zero"
296            );
297            price_impact_value
298                .checked_div(&price)
299                .ok_or(crate::Error::Computation("calculating price impact amount"))?
300        } else {
301            self.params
302                .prices
303                .index_token_price
304                .pick_price(false)
305                .as_divisor_to_round_up_magnitude_div(price_impact_value)
306                .ok_or(crate::Error::Computation("calculating price impact amount"))?
307        };
308
309        // Base size delta in tokens.
310        let mut size_delta_in_tokens = if self.position.is_long() {
311            let price = self.params.prices.index_token_price.pick_price(true);
312            debug_assert!(
313                !price.is_zero(),
314                "price must have been checked to be non-zero"
315            );
316            self.params
317                .size_delta_usd
318                .checked_div(price)
319                .ok_or(crate::Error::Computation(
320                    "calculating size delta in tokens",
321                ))?
322        } else {
323            let price = self.params.prices.index_token_price.pick_price(false);
324            self.params
325                .size_delta_usd
326                .checked_round_up_div(price)
327                .ok_or(crate::Error::Computation(
328                    "calculating size delta in tokens",
329                ))?
330        };
331
332        // Apply price impact.
333        size_delta_in_tokens = if self.position.is_long() {
334            size_delta_in_tokens.checked_add_with_signed(&price_impact_amount)
335        } else {
336            size_delta_in_tokens.checked_sub_with_signed(&price_impact_amount)
337        }
338        .ok_or(crate::Error::Computation(
339            "price impact larger than order size",
340        ))?;
341
342        let execution_price = get_execution_price_for_increase(
343            &self.params.size_delta_usd,
344            &size_delta_in_tokens,
345            self.params.acceptable_price.as_ref(),
346            self.position.is_long(),
347        )?;
348
349        Ok(ExecutionParamsWithPriceImpact {
350            execution: ExecutionParams {
351                price_impact_value: price_impact.value.clone(),
352                price_impact_amount,
353                size_delta_in_tokens,
354                execution_price,
355            },
356            price_impact,
357        })
358    }
359
360    #[inline]
361    fn collateral_price(&self) -> &Price<P::Num> {
362        self.position.collateral_price(&self.params.prices)
363    }
364
365    fn process_collateral(
366        &mut self,
367        price_impact: &PriceImpact<P::Signed>,
368    ) -> crate::Result<(P::Signed, PositionFees<P::Num>)> {
369        use num_traits::CheckedSub;
370
371        let mut collateral_delta_amount = self.params.collateral_increment_amount.to_signed()?;
372
373        let fees = self.position.position_fees(
374            self.collateral_price(),
375            &self.params.size_delta_usd,
376            price_impact.balance_change,
377            false,
378        )?;
379
380        collateral_delta_amount = collateral_delta_amount
381            .checked_sub(&fees.total_cost_amount()?.to_signed()?)
382            .ok_or(crate::Error::Computation(
383                "applying fees to collateral amount",
384            ))?;
385
386        let is_collateral_token_long = self.position.is_collateral_token_long();
387
388        self.position
389            .market_mut()
390            .apply_delta_to_claimable_fee_pool(
391                is_collateral_token_long,
392                &fees.for_receiver()?.to_signed()?,
393            )?;
394
395        self.position
396            .market_mut()
397            .apply_delta(is_collateral_token_long, &fees.for_pool()?.to_signed()?)?;
398
399        let is_long = self.position.is_long();
400        self.position
401            .market_mut()
402            .collateral_sum_pool_mut(is_long)?
403            .apply_delta_amount(is_collateral_token_long, &collateral_delta_amount)?;
404
405        Ok((collateral_delta_amount, fees))
406    }
407}
408
409fn get_execution_price_for_increase<T>(
410    size_delta_usd: &T,
411    size_delta_in_tokens: &T,
412    acceptable_price: Option<&T>,
413    is_long: bool,
414) -> crate::Result<T>
415where
416    T: num_traits::Num + Ord + Clone + CheckedDiv,
417{
418    if size_delta_usd.is_zero() {
419        return Err(crate::Error::Computation("empty size delta in tokens"));
420    }
421
422    let execution_price = size_delta_usd
423        .checked_div(size_delta_in_tokens)
424        .ok_or(crate::Error::Computation("calculating execution price"))?;
425
426    let Some(acceptable_price) = acceptable_price else {
427        return Ok(execution_price);
428    };
429
430    if (is_long && execution_price <= *acceptable_price)
431        || (!is_long && execution_price >= *acceptable_price)
432    {
433        Ok(execution_price)
434    } else {
435        Err(crate::Error::InvalidArgument(
436            "order not fulfillable at acceptable price",
437        ))
438    }
439}
440
441impl<const DECIMALS: u8, P: PositionMut<DECIMALS>> MarketAction for IncreasePosition<P, DECIMALS>
442where
443    P::Market: PerpMarketMut<DECIMALS, Num = P::Num, Signed = P::Signed>,
444{
445    type Report = IncreasePositionReport<P::Num, P::Signed>;
446
447    fn execute(mut self) -> crate::Result<Self::Report> {
448        self.initialize_position_if_empty()?;
449
450        let ExecutionParamsWithPriceImpact {
451            execution,
452            price_impact,
453        } = self.get_execution_params()?;
454
455        let (collateral_delta_amount, fees) = self.process_collateral(&price_impact)?;
456
457        let is_collateral_delta_positive = collateral_delta_amount.is_positive();
458        *self.position.collateral_amount_mut() = self
459            .position
460            .collateral_amount_mut()
461            .checked_add_with_signed(&collateral_delta_amount)
462            .ok_or({
463                if is_collateral_delta_positive {
464                    crate::Error::Computation("collateral amount overflow")
465                } else {
466                    crate::Error::InvalidArgument("insufficient collateral amount")
467                }
468            })?;
469
470        self.position
471            .market_mut()
472            .apply_delta_to_position_impact_pool(
473                &execution
474                    .price_impact_amount()
475                    .checked_neg()
476                    .ok_or(crate::Error::Computation(
477                        "calculating position impact pool delta amount",
478                    ))?,
479            )?;
480
481        let is_long = self.position.is_long();
482        let next_position_size_in_usd = self
483            .position
484            .size_in_usd_mut()
485            .checked_add(&self.params.size_delta_usd)
486            .ok_or(crate::Error::Computation("size in usd overflow"))?;
487        let next_position_borrowing_factor = self
488            .position
489            .market()
490            .cumulative_borrowing_factor(is_long)?;
491
492        // Update total borrowing before updating position size.
493        self.position
494            .update_total_borrowing(&next_position_size_in_usd, &next_position_borrowing_factor)?;
495
496        // Update sizes.
497        *self.position.size_in_usd_mut() = next_position_size_in_usd;
498        *self.position.size_in_tokens_mut() = self
499            .position
500            .size_in_tokens_mut()
501            .checked_add(&execution.size_delta_in_tokens)
502            .ok_or(crate::Error::Computation("size in tokens overflow"))?;
503
504        // Update funding fees state.
505        *self.position.funding_fee_amount_per_size_mut() = self
506            .position
507            .market()
508            .funding_fee_amount_per_size(is_long, self.position.is_collateral_token_long())?;
509        for is_long_collateral in [true, false] {
510            *self
511                .position
512                .claimable_funding_fee_amount_per_size_mut(is_long_collateral) = self
513                .position
514                .market()
515                .claimable_funding_fee_amount_per_size(is_long, is_long_collateral)?;
516        }
517
518        // Update borrowing fee state.
519        *self.position.borrowing_factor_mut() = next_position_borrowing_factor;
520
521        self.position.update_open_interest(
522            &self.params.size_delta_usd.to_signed()?,
523            &execution.size_delta_in_tokens.to_signed()?,
524        )?;
525
526        if !self.params.size_delta_usd.is_zero() {
527            let market = self.position.market();
528            market.validate_reserve(&self.params.prices, self.position.is_long())?;
529            market.validate_open_interest_reserve(&self.params.prices, self.position.is_long())?;
530
531            let delta = CollateralDelta::new(
532                self.position.size_in_usd().clone(),
533                self.position.collateral_amount().clone(),
534                Zero::zero(),
535                Zero::zero(),
536            );
537            let will_collateral_be_sufficient = self
538                .position
539                .will_collateral_be_sufficient(&self.params.prices, &delta)?;
540
541            if !will_collateral_be_sufficient.is_sufficient() {
542                return Err(crate::Error::InvalidArgument("insufficient collateral usd"));
543            }
544        }
545
546        self.position.validate(&self.params.prices, true, true)?;
547
548        self.position.on_increased()?;
549
550        Ok(IncreasePositionReport::new(
551            self.params,
552            execution,
553            collateral_delta_amount,
554            fees,
555        ))
556    }
557}
558
559struct ExecutionParamsWithPriceImpact<T: Unsigned> {
560    execution: ExecutionParams<T, T::Signed>,
561    price_impact: PriceImpact<T::Signed>,
562}
563
564#[cfg(test)]
565mod tests {
566    use crate::{
567        market::LiquidityMarketMutExt,
568        test::{TestMarket, TestPosition},
569        MarketAction,
570    };
571
572    use super::*;
573
574    #[test]
575    fn basic() -> crate::Result<()> {
576        let mut market = TestMarket::<u64, 9>::default();
577        let prices = Prices::new_for_test(120, 120, 1);
578        market.deposit(1_000_000_000, 0, prices)?.execute()?;
579        market.deposit(0, 1_000_000_000, prices)?.execute()?;
580        println!("{market:#?}");
581        let mut position = TestPosition::long(true);
582        let report = position
583            .ops(&mut market)
584            .increase(
585                Prices::new_for_test(123, 123, 1),
586                100_000_000,
587                8_000_000_000,
588                None,
589            )?
590            .execute()?;
591        println!("{report:#?}");
592        println!("{position:#?}");
593        Ok(())
594    }
595}