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 )?;
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 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 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 self.position
493 .update_total_borrowing(&next_position_size_in_usd, &next_position_borrowing_factor)?;
494
495 *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 *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 *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}