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#[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#[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 pub fn collateral_increment_amount(&self) -> &T {
43 &self.collateral_increment_amount
44 }
45
46 pub fn size_delta_usd(&self) -> &T {
48 &self.size_delta_usd
49 }
50
51 pub fn acceptable_price(&self) -> Option<&T> {
53 self.acceptable_price.as_ref()
54 }
55
56 pub fn prices(&self) -> &Prices<T> {
58 &self.prices
59 }
60}
61
62#[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 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 #[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 pub fn params(&self) -> &IncreasePositionParams<T> {
145 &self.params
146 }
147
148 pub fn execution(&self) -> &ExecutionParams<T, T::Signed> {
150 &self.execution
151 }
152
153 pub fn collateral_delta_amount(&self) -> &T::Signed {
155 &self.collateral_delta_amount
156 }
157
158 pub fn fees(&self) -> &PositionFees<T> {
160 &self.fees
161 }
162}
163
164#[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 pub fn price_impact_value(&self) -> &T::Signed {
189 &self.price_impact_value
190 }
191
192 pub fn price_impact_amount(&self) -> &T::Signed {
194 &self.price_impact_amount
195 }
196
197 pub fn size_delta_in_tokens(&self) -> &T {
199 &self.size_delta_in_tokens
200 }
201
202 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 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 *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 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 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 self.position
485 .update_total_borrowing(&next_position_size_in_usd, &next_position_borrowing_factor)?;
486
487 *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 *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 *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}