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