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#[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#[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 pub fn collateral_increment_amount(&self) -> &T {
44 &self.collateral_increment_amount
45 }
46
47 pub fn size_delta_usd(&self) -> &T {
49 &self.size_delta_usd
50 }
51
52 pub fn acceptable_price(&self) -> Option<&T> {
54 self.acceptable_price.as_ref()
55 }
56
57 pub fn prices(&self) -> &Prices<T> {
59 &self.prices
60 }
61}
62
63#[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 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 #[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 pub fn params(&self) -> &IncreasePositionParams<T> {
146 &self.params
147 }
148
149 pub fn execution(&self) -> &ExecutionParams<T, T::Signed> {
151 &self.execution
152 }
153
154 pub fn collateral_delta_amount(&self) -> &T::Signed {
156 &self.collateral_delta_amount
157 }
158
159 pub fn fees(&self) -> &PositionFees<T> {
161 &self.fees
162 }
163}
164
165#[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 pub fn price_impact_value(&self) -> &T::Signed {
190 &self.price_impact_value
191 }
192
193 pub fn price_impact_amount(&self) -> &T::Signed {
195 &self.price_impact_amount
196 }
197
198 pub fn size_delta_in_tokens(&self) -> &T {
200 &self.size_delta_in_tokens
201 }
202
203 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 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 *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 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 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 self.position
494 .update_total_borrowing(&next_position_size_in_usd, &next_position_borrowing_factor)?;
495
496 *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 *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 *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}