1use std::ops::Neg;
6
7use super::get_oracle_normalization_factor;
8use crate::{
9 ffi::{
10 self, calculate_margin_requirement_and_total_collateral_and_liability_info, AccountsList,
11 MarginContextMode,
12 },
13 math::{
14 account_list_builder::AccountsListBuilder,
15 constants::{
16 AMM_RESERVE_PRECISION_I128, BASE_PRECISION_I128, MARGIN_PRECISION,
17 QUOTE_PRECISION_I128, QUOTE_PRECISION_I64, SPOT_WEIGHT_PRECISION,
18 },
19 },
20 types::{
21 accounts::{PerpMarket, SpotMarket, User},
22 MarginRequirementType, PerpPosition,
23 },
24 DriftClient, MarginMode, MarketId, SdkError, SdkResult, SpotPosition,
25};
26
27#[derive(Debug)]
29pub struct LiquidationAndPnlInfo {
30 pub liquidation_price: i64,
32 pub unrealized_pnl: i128,
34 pub oracle_price: i64,
37}
38
39pub async fn calculate_liquidation_price_and_unrealized_pnl(
41 client: &DriftClient,
42 user: &User,
43 market_index: u16,
44) -> SdkResult<LiquidationAndPnlInfo> {
45 let perp_market = client
46 .program_data()
47 .perp_market_config_by_index(market_index)
48 .expect("market exists");
49
50 let position = user
51 .get_perp_position(market_index)
52 .map_err(|_| SdkError::NoPosition(market_index))?;
53
54 let mut builder = AccountsListBuilder::default();
56 let mut accounts_list = builder.build(client, user, &[]).await?;
57
58 let oracle = accounts_list
59 .oracles
60 .iter()
61 .find(|o| o.key == perp_market.amm.oracle)
62 .expect("oracle loaded");
63 let oracle_source = perp_market.amm.oracle_source;
64 let oracle_price = ffi::get_oracle_price(
65 oracle_source,
66 &mut (oracle.key, oracle.account.clone()),
67 accounts_list.latest_slot,
68 )?
69 .price;
70
71 let spot_market = client
73 .program_data()
74 .spot_market_configs()
75 .iter()
76 .find(|x| x.oracle == perp_market.amm.oracle);
77
78 Ok(LiquidationAndPnlInfo {
79 unrealized_pnl: calculate_unrealized_pnl_inner(&position, oracle_price)?,
80 liquidation_price: calculate_liquidation_price_inner(
81 user,
82 perp_market,
83 spot_market,
84 oracle_price,
85 &mut accounts_list,
86 )?,
87 oracle_price,
88 })
89}
90
91pub async fn calculate_unrealized_pnl(
93 client: &DriftClient,
94 user: &User,
95 market_index: u16,
96) -> SdkResult<i128> {
97 if let Ok(position) = user.get_perp_position(market_index) {
98 let oracle_price = client
99 .get_oracle_price_data_and_slot(MarketId::perp(market_index))
100 .await
101 .map(|x| x.data.price)?;
102
103 calculate_unrealized_pnl_inner(&position, oracle_price)
104 } else {
105 Err(SdkError::NoPosition(market_index))
106 }
107}
108
109pub fn calculate_unrealized_pnl_inner(
110 position: &PerpPosition,
111 oracle_price: i64,
112) -> SdkResult<i128> {
113 let base_asset_value = (position.base_asset_amount as i128 * oracle_price.max(0) as i128)
114 / AMM_RESERVE_PRECISION_I128;
115 let pnl = base_asset_value + position.quote_entry_amount as i128;
116
117 Ok(pnl)
118}
119
120pub async fn calculate_liquidation_price(
124 client: &DriftClient,
125 user: &User,
126 market_index: u16,
127) -> SdkResult<i64> {
128 let mut accounts_builder = AccountsListBuilder::default();
129 let mut account_maps = accounts_builder.build(client, user, &[]).await?;
130 let perp_market = client
131 .program_data()
132 .perp_market_config_by_index(market_index)
133 .expect("market exists");
134
135 let oracle = client
136 .get_oracle_price_data_and_slot(MarketId::perp(market_index))
137 .await?;
138
139 let spot_market = client
141 .program_data()
142 .spot_market_configs()
143 .iter()
144 .find(|x| x.oracle == perp_market.amm.oracle);
145
146 calculate_liquidation_price_inner(
147 user,
148 perp_market,
149 spot_market,
150 oracle.data.price,
151 &mut account_maps,
152 )
153}
154
155pub fn calculate_liquidation_price_inner(
163 user: &User,
164 perp_market: &PerpMarket,
165 spot_market: Option<&SpotMarket>,
166 oracle_price: i64,
167 accounts: &mut AccountsList,
168) -> SdkResult<i64> {
169 let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info(
170 user,
171 accounts,
172 MarginContextMode::StandardMaintenance,
173 )?;
174
175 let perp_position = user
177 .get_perp_position(perp_market.market_index)
178 .map_err(|_| SdkError::NoPosition(perp_market.market_index))?;
179
180 let perp_position_with_lp =
181 perp_position.simulate_settled_lp_position(perp_market, oracle_price)?;
182
183 let perp_free_collateral_delta = calculate_perp_free_collateral_delta(
184 &perp_position_with_lp,
185 perp_market,
186 oracle_price,
187 user.margin_mode,
188 );
189
190 let mut spot_free_collateral_delta = 0;
192 if let Some(spot_market) = spot_market {
193 if let Ok(spot_position) = user.get_spot_position(spot_market.market_index) {
194 if !spot_position.is_available() {
195 spot_free_collateral_delta =
196 calculate_spot_free_collateral_delta(&spot_position, spot_market);
197 let (numerator, denominator) = get_oracle_normalization_factor(
198 perp_market.amm.oracle_source,
199 spot_market.oracle_source,
200 );
201 spot_free_collateral_delta = (((spot_free_collateral_delta as i128)
202 * numerator as i128)
203 / denominator as i128) as i64;
204 }
205 }
206 }
207
208 let free_collateral = margin_calculation.get_free_collateral();
211 let free_collateral_delta = perp_free_collateral_delta + spot_free_collateral_delta;
212 if free_collateral_delta == 0 {
213 return Ok(-1);
214 }
215 let liquidation_price_delta =
216 (free_collateral as i64 * QUOTE_PRECISION_I64) / free_collateral_delta;
217
218 let liquidation_price = (oracle_price - liquidation_price_delta).max(-1);
219 Ok(liquidation_price)
220}
221
222pub fn calculate_perp_free_collateral_delta(
223 position: &PerpPosition,
224 market: &PerpMarket,
225 oracle_price: i64,
226 margin_mode: MarginMode,
227) -> i64 {
228 let current_base_asset_amount = position.base_asset_amount;
229
230 let worst_case_base_amount = position
231 .worst_case_base_asset_amount(oracle_price, market.contract_type)
232 .unwrap();
233 let margin_ratio = market
234 .get_margin_ratio(
235 worst_case_base_amount.unsigned_abs(),
236 MarginRequirementType::Maintenance,
237 margin_mode == MarginMode::HighLeverage,
238 )
239 .unwrap();
240 let margin_ratio = (margin_ratio as i64 * QUOTE_PRECISION_I64) / MARGIN_PRECISION as i64;
241
242 if worst_case_base_amount == 0 {
243 return 0;
244 }
245
246 let mut fcd = if current_base_asset_amount > 0 {
247 ((QUOTE_PRECISION_I64 - margin_ratio) as i128 * current_base_asset_amount as i128)
248 / BASE_PRECISION_I128
249 } else {
250 ((QUOTE_PRECISION_I64.neg() - margin_ratio) as i128
251 * current_base_asset_amount.abs() as i128)
252 / BASE_PRECISION_I128
253 } as i64;
254
255 let order_base_amount = worst_case_base_amount - current_base_asset_amount as i128;
256 if order_base_amount != 0 {
257 fcd -= ((margin_ratio as i128 * order_base_amount.abs()) / BASE_PRECISION_I128) as i64;
258 }
259
260 fcd
261}
262
263pub fn calculate_spot_free_collateral_delta(position: &SpotPosition, market: &SpotMarket) -> i64 {
264 let market_precision = 10_i128.pow(market.decimals);
265 let signed_token_amount = position.get_signed_token_amount(market).unwrap();
266 let delta = if signed_token_amount > 0 {
267 let weight = market
268 .get_asset_weight(
269 signed_token_amount.unsigned_abs(),
270 0, MarginRequirementType::Maintenance,
272 )
273 .unwrap() as i128;
274 (((QUOTE_PRECISION_I128 * weight) / SPOT_WEIGHT_PRECISION as i128) * signed_token_amount)
275 / market_precision
276 } else {
277 let weight = market
278 .get_liability_weight(
279 signed_token_amount.unsigned_abs(),
280 MarginRequirementType::Maintenance,
281 )
282 .unwrap() as i128;
283 (((QUOTE_PRECISION_I128.neg() * weight) / SPOT_WEIGHT_PRECISION as i128)
284 * signed_token_amount.abs())
285 / market_precision
286 };
287
288 delta.try_into().expect("ftis i64")
289}
290
291#[derive(Copy, Clone, Debug, PartialEq)]
292pub struct MarginRequirementInfo {
293 pub initial: u128,
295 pub maintenance: u128,
297}
298
299pub fn calculate_margin_requirements(
301 client: &DriftClient,
302 user: &User,
303) -> SdkResult<MarginRequirementInfo> {
304 calculate_margin_requirements_inner(
305 user,
306 &mut AccountsListBuilder::default().try_build(client, user, &[])?,
307 )
308}
309
310fn calculate_margin_requirements_inner(
312 user: &User,
313 accounts: &mut AccountsList,
314) -> SdkResult<MarginRequirementInfo> {
315 let maintenance_result = calculate_margin_requirement_and_total_collateral_and_liability_info(
316 user,
317 accounts,
318 MarginContextMode::StandardMaintenance,
319 )?;
320
321 let initial_result = calculate_margin_requirement_and_total_collateral_and_liability_info(
322 user,
323 accounts,
324 MarginContextMode::StandardInitial,
325 )?;
326
327 Ok(MarginRequirementInfo {
328 maintenance: maintenance_result.margin_requirement,
329 initial: initial_result.margin_requirement,
330 })
331}
332
333#[derive(Copy, Clone, Debug, PartialEq)]
334pub struct CollateralInfo {
335 pub total: i128,
337 pub free: i128,
339}
340
341pub fn calculate_collateral(
342 client: &DriftClient,
343 user: &User,
344 margin_requirement_type: MarginRequirementType,
345) -> SdkResult<CollateralInfo> {
346 let mut accounts_builder = AccountsListBuilder::default();
347 calculate_collateral_inner(
348 user,
349 &mut accounts_builder.try_build(client, user, &[])?,
350 margin_requirement_type,
351 )
352}
353
354fn calculate_collateral_inner(
355 user: &User,
356 accounts: &mut AccountsList,
357 margin_requirement_type: MarginRequirementType,
358) -> SdkResult<CollateralInfo> {
359 let result = calculate_margin_requirement_and_total_collateral_and_liability_info(
360 user,
361 accounts,
362 MarginContextMode::StandardCustom(margin_requirement_type),
363 )?;
364
365 Ok(CollateralInfo {
366 total: result.total_collateral,
367 free: result.get_free_collateral() as i128,
368 })
369}
370
371#[cfg(test)]
372mod tests {
373 use solana_sdk::{account::Account, pubkey::Pubkey};
374
375 use super::*;
376 use crate::{
377 constants::{
378 ids::pyth_program,
379 {self},
380 },
381 drift_idl::types::{HistoricalOracleData, MarketStatus, OracleSource, SpotPosition, AMM},
382 math::constants::{
383 AMM_RESERVE_PRECISION, BASE_PRECISION_I64, LIQUIDATION_FEE_PRECISION, PEG_PRECISION,
384 PRICE_PRECISION_I64, SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64,
385 SPOT_CUMULATIVE_INTEREST_PRECISION,
386 },
387 utils::test_utils::*,
388 MarketId,
389 };
390
391 const SOL_ORACLE: Pubkey = solana_sdk::pubkey!("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix");
392 const BTC_ORACLE: Pubkey = solana_sdk::pubkey!("GVXRSBjFk6e6J3NbVPXohDJetcTjaeeuykUpbQF8UoMU");
393
394 fn sol_spot_market() -> SpotMarket {
395 SpotMarket {
396 market_index: 1,
397 oracle_source: OracleSource::Pyth,
398 oracle: SOL_ORACLE,
399 cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION.into(),
400 cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION.into(),
401 decimals: 9,
402 initial_asset_weight: 8 * SPOT_WEIGHT_PRECISION / 10,
403 maintenance_asset_weight: 9 * SPOT_WEIGHT_PRECISION / 10,
404 initial_liability_weight: 12 * SPOT_WEIGHT_PRECISION / 10,
405 maintenance_liability_weight: 11 * SPOT_WEIGHT_PRECISION / 10,
406 liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000,
407 deposit_balance: (1_000 * SPOT_BALANCE_PRECISION).into(),
408 ..SpotMarket::default()
409 }
410 }
411
412 fn sol_perp_market() -> PerpMarket {
413 PerpMarket {
414 amm: AMM {
415 base_asset_reserve: (100 * AMM_RESERVE_PRECISION).into(),
416 quote_asset_reserve: (100 * AMM_RESERVE_PRECISION).into(),
417 bid_base_asset_reserve: (101 * AMM_RESERVE_PRECISION).into(),
418 bid_quote_asset_reserve: (99 * AMM_RESERVE_PRECISION).into(),
419 ask_base_asset_reserve: (99 * AMM_RESERVE_PRECISION).into(),
420 ask_quote_asset_reserve: (101 * AMM_RESERVE_PRECISION).into(),
421 sqrt_k: (100 * AMM_RESERVE_PRECISION).into(),
422 peg_multiplier: (100 * PEG_PRECISION).into(),
423 order_step_size: 10_000_000,
424 oracle: SOL_ORACLE,
425 ..AMM::default()
426 },
427 market_index: 0,
428 margin_ratio_initial: 1000,
429 margin_ratio_maintenance: 500,
430 unrealized_pnl_maintenance_asset_weight: SPOT_WEIGHT_PRECISION,
431 status: MarketStatus::Initialized,
432 ..PerpMarket::default()
433 }
434 }
435
436 fn btc_perp_market() -> PerpMarket {
437 PerpMarket {
438 amm: AMM {
439 base_asset_reserve: (100 * AMM_RESERVE_PRECISION).into(),
440 quote_asset_reserve: (100 * AMM_RESERVE_PRECISION).into(),
441 bid_base_asset_reserve: (101 * AMM_RESERVE_PRECISION).into(),
442 bid_quote_asset_reserve: (99 * AMM_RESERVE_PRECISION).into(),
443 ask_base_asset_reserve: (99 * AMM_RESERVE_PRECISION).into(),
444 ask_quote_asset_reserve: (101 * AMM_RESERVE_PRECISION).into(),
445 sqrt_k: (100 * AMM_RESERVE_PRECISION).into(),
446 oracle: BTC_ORACLE,
447 ..AMM::default()
448 },
449 market_index: 1,
450 margin_ratio_initial: 1000,
451 margin_ratio_maintenance: 500,
452 imf_factor: 1000, unrealized_pnl_initial_asset_weight: SPOT_WEIGHT_PRECISION,
454 unrealized_pnl_maintenance_asset_weight: SPOT_WEIGHT_PRECISION,
455 status: MarketStatus::Initialized,
456 ..PerpMarket::default()
457 }
458 }
459
460 fn usdc_spot_market() -> SpotMarket {
461 SpotMarket {
462 market_index: 0,
463 oracle_source: OracleSource::QuoteAsset,
464 cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION.into(),
465 decimals: 6,
466 initial_asset_weight: SPOT_WEIGHT_PRECISION,
467 maintenance_asset_weight: SPOT_WEIGHT_PRECISION,
468 deposit_balance: (100_000 * SPOT_BALANCE_PRECISION).into(),
469 liquidator_fee: 0,
470 historical_oracle_data: HistoricalOracleData {
471 last_oracle_price: PRICE_PRECISION_I64,
472 last_oracle_conf: 0,
473 last_oracle_delay: 0,
474 last_oracle_price_twap: PRICE_PRECISION_I64,
475 last_oracle_price_twap5min: PRICE_PRECISION_I64,
476 ..HistoricalOracleData::default()
477 },
478 ..SpotMarket::default()
479 }
480 }
481
482 #[cfg(feature = "rpc_tests")]
483 #[tokio::test]
484 async fn calculate_liq_price() {
485 use solana_client::nonblocking::rpc_client::RpcClient;
486
487 use crate::{utils::test_envs::mainnet_endpoint, Wallet};
488
489 let wallet = Wallet::read_only(solana_sdk::pubkey!(
490 "DxoRJ4f5XRMvXU9SGuM4ZziBFUxbhB3ubur5sVZEvue2"
491 ));
492 let client = DriftClient::new(
493 crate::Context::MainNet,
494 RpcClient::new(mainnet_endpoint()),
495 wallet.clone(),
496 )
497 .await
498 .unwrap();
499 assert!(client.subscribe().await.is_ok());
500 let user = client
501 .get_user_account(&wallet.sub_account(0))
502 .await
503 .unwrap();
504
505 dbg!(calculate_liquidation_price_and_unrealized_pnl(&client, &user, 4).unwrap());
506 }
507
508 #[cfg(feature = "rpc_tests")]
509 #[tokio::test]
510 async fn calculate_margin_requirements_works() {
511 use solana_client::nonblocking::rpc_client::RpcClient;
512
513 use crate::{utils::test_envs::mainnet_endpoint, Wallet};
514
515 let wallet = Wallet::read_only(solana_sdk::pubkey!(
516 "DxoRJ4f5XRMvXU9SGuM4ZziBFUxbhB3ubur5sVZEvue2"
517 ));
518 let client = DriftClient::new(
519 crate::Context::MainNet,
520 RpcClient::new(mainnet_endpoint()),
521 wallet.clone(),
522 )
523 .await
524 .unwrap();
525 client.subscribe().await.unwrap();
526 let user = client
527 .get_user_account(&wallet.sub_account(0))
528 .await
529 .unwrap();
530
531 dbg!(calculate_margin_requirements(&client, &user).await.unwrap());
532 }
533
534 #[test]
535 fn calculate_margin_requirements_works() {
536 let sol_perp_index = 0;
537 let btc_perp_index = 1;
538 let mut user = User::default();
539 user.perp_positions[0] = PerpPosition {
540 market_index: sol_perp_index,
541 base_asset_amount: -2 * BASE_PRECISION_I64,
542 ..Default::default()
543 };
544 user.perp_positions[1] = PerpPosition {
545 market_index: btc_perp_index,
546 base_asset_amount: (1 * BASE_PRECISION_I64) / 20,
547 ..Default::default()
548 };
549 user.spot_positions[0] = SpotPosition {
550 market_index: MarketId::QUOTE_SPOT.index(),
551 scaled_balance: 1_000 * SPOT_BALANCE_PRECISION_U64,
552 ..Default::default()
553 };
554
555 let mut sol_oracle_price = get_pyth_price(100, 6);
556 crate::create_account_info!(sol_oracle_price, &SOL_ORACLE, pyth_program::ID, sol_oracle);
557 crate::create_anchor_account_info!(
558 sol_perp_market(),
559 Pubkey::new_unique(),
560 PerpMarket,
561 sol_perp
562 );
563 let mut btc_oracle_price = get_pyth_price(50_000, 6);
564 crate::create_account_info!(btc_oracle_price, &BTC_ORACLE, pyth_program::ID, btc_oracle);
565 crate::create_anchor_account_info!(
566 usdc_spot_market(),
567 Pubkey::new_unique(),
568 SpotMarket,
569 usdc_spot
570 );
571 crate::create_anchor_account_info!(
572 btc_perp_market(),
573 Pubkey::new_unique(),
574 PerpMarket,
575 btc_perp
576 );
577
578 let mut perps = [sol_perp, btc_perp];
579 let mut spot = [usdc_spot];
580 let mut oracles = [sol_oracle, btc_oracle];
581 let mut accounts_map = AccountsList::new(&mut perps, &mut spot, &mut oracles);
582
583 let margin_info = calculate_margin_requirements_inner(&user, &mut accounts_map).unwrap();
584
585 assert_eq!(
586 MarginRequirementInfo {
587 initial: 27_000_0000,
588 maintenance: 135_000_000
589 },
590 margin_info
591 );
592 }
593
594 #[test]
595 fn liquidation_price_short() {
596 let sol_perp_index = 0;
597 let mut user = User::default();
598 user.perp_positions[0] = PerpPosition {
599 market_index: sol_perp_index,
600 base_asset_amount: -2 * BASE_PRECISION_I64,
601 ..Default::default()
602 };
603 user.spot_positions[0] = SpotPosition {
604 market_index: MarketId::QUOTE_SPOT.index(),
605 scaled_balance: 250_u64 * SPOT_BALANCE_PRECISION_U64,
606 ..Default::default()
607 };
608
609 let sol_usdc_price = 100;
610
611 let mut sol_oracle_price = get_pyth_price(sol_usdc_price, 6);
612 crate::create_account_info!(sol_oracle_price, &SOL_ORACLE, pyth_program::ID, sol_oracle);
613 crate::create_anchor_account_info!(
614 usdc_spot_market(),
615 constants::PROGRAM_ID,
616 SpotMarket,
617 usdc_spot
618 );
619 crate::create_anchor_account_info!(
620 sol_perp_market(),
621 constants::PROGRAM_ID,
622 PerpMarket,
623 sol_perp
624 );
625 let mut perps = [sol_perp];
626 let mut spot = [usdc_spot];
627 let mut oracles = [sol_oracle];
628 let mut accounts_map = AccountsList::new(&mut perps, &mut spot, &mut oracles);
629
630 let sol_spot = sol_spot_market();
631 let sol_perp = sol_perp_market();
632 let liquidation_price = calculate_liquidation_price_inner(
633 &user,
634 &sol_perp,
635 Some(&sol_spot),
636 sol_usdc_price * QUOTE_PRECISION_I64,
637 &mut accounts_map,
638 )
639 .unwrap();
640
641 dbg!(liquidation_price);
642 assert_eq!(liquidation_price, 119_047_619);
643 }
644
645 #[test]
646 fn liquidation_price_long() {
647 let mut user = User::default();
648 user.perp_positions[0] = PerpPosition {
649 market_index: sol_perp_market().market_index,
650 base_asset_amount: 5 * BASE_PRECISION_I64,
651 quote_asset_amount: -5 * (100 * QUOTE_PRECISION_I64),
652 ..Default::default()
653 };
654 user.spot_positions[0] = SpotPosition {
655 market_index: MarketId::QUOTE_SPOT.index(),
656 scaled_balance: 250_u64 * SPOT_BALANCE_PRECISION_U64,
657 ..Default::default()
658 };
659 let sol_usdc_price = 100;
660 let mut sol_oracle_price = get_pyth_price(sol_usdc_price, 6);
661 crate::create_account_info!(sol_oracle_price, &SOL_ORACLE, pyth_program::ID, sol_oracle);
662 crate::create_anchor_account_info!(
663 usdc_spot_market(),
664 constants::PROGRAM_ID,
665 SpotMarket,
666 usdc_spot
667 );
668 crate::create_anchor_account_info!(
669 sol_perp_market(),
670 constants::PROGRAM_ID,
671 PerpMarket,
672 sol_perp
673 );
674
675 let mut perps = [sol_perp];
676 let mut spot = [usdc_spot];
677 let mut oracles = [sol_oracle];
678 let mut accounts_map = AccountsList::new(&mut perps, &mut spot, &mut oracles);
679
680 let liquidation_price = calculate_liquidation_price_inner(
681 &user,
682 &sol_perp_market(),
683 Some(&sol_spot_market()),
684 sol_usdc_price * QUOTE_PRECISION_I64,
685 &mut accounts_map,
686 )
687 .unwrap();
688
689 dbg!(liquidation_price);
690 assert_eq!(liquidation_price, 52_631_579);
691 }
692
693 #[test]
694 fn liquidation_price_short_with_spot_balance() {
695 let mut user = User::default();
696 user.perp_positions[0] = PerpPosition {
697 market_index: btc_perp_market().market_index,
698 base_asset_amount: -250_000_000, ..Default::default()
700 };
701 user.spot_positions[0] = SpotPosition {
702 market_index: 1,
703 scaled_balance: 200 * SPOT_BALANCE_PRECISION_U64,
704 ..Default::default()
705 };
706 let sol_usdc_price = 100;
707 let mut sol_oracle_price = get_pyth_price(sol_usdc_price, 6);
708 crate::create_account_info!(sol_oracle_price, &SOL_ORACLE, pyth_program::ID, sol_oracle);
709
710 let btc_usdc_price = 40_000;
711 let mut btc_oracle_price = get_pyth_price(btc_usdc_price, 6);
712 crate::create_account_info!(btc_oracle_price, &BTC_ORACLE, pyth_program::ID, btc_oracle);
713 crate::create_anchor_account_info!(
714 usdc_spot_market(),
715 constants::PROGRAM_ID,
716 SpotMarket,
717 usdc_spot
718 );
719 crate::create_anchor_account_info!(
720 sol_spot_market(),
721 constants::PROGRAM_ID,
722 SpotMarket,
723 sol_spot
724 );
725 crate::create_anchor_account_info!(
726 btc_perp_market(),
727 constants::PROGRAM_ID,
728 PerpMarket,
729 btc_perp
730 );
731 let mut perps = [btc_perp];
732 let mut spot = [usdc_spot, sol_spot];
733 let mut oracles = [sol_oracle, btc_oracle];
734 let mut accounts_map = AccountsList::new(&mut perps, &mut spot, &mut oracles);
735 let liquidation_price = calculate_liquidation_price_inner(
736 &user,
737 &btc_perp_market(),
738 None,
739 btc_usdc_price * QUOTE_PRECISION_I64,
740 &mut accounts_map,
741 )
742 .unwrap();
743 assert_eq!(liquidation_price, 68_571_428_571);
744 }
745
746 #[test]
747 fn liquidation_price_long_with_spot_balance() {
748 let sol_usdc_price = 100;
749 let mut user = User::default();
750 user.perp_positions[0] = PerpPosition {
751 market_index: sol_perp_market().market_index,
752 base_asset_amount: 5 * BASE_PRECISION_I64,
753 quote_asset_amount: -5 * (100 * QUOTE_PRECISION_I64),
754 ..Default::default()
755 };
756 user.spot_positions[0] = SpotPosition {
757 market_index: 1,
758 scaled_balance: 2 * SPOT_BALANCE_PRECISION_U64,
759 ..Default::default()
760 };
761
762 let mut sol_oracle_price = get_pyth_price(sol_usdc_price, 6);
763 crate::create_account_info!(sol_oracle_price, &SOL_ORACLE, pyth_program::ID, sol_oracle);
764 crate::create_anchor_account_info!(
765 usdc_spot_market(),
766 constants::PROGRAM_ID,
767 SpotMarket,
768 usdc_spot
769 );
770 crate::create_anchor_account_info!(
771 sol_spot_market(),
772 constants::PROGRAM_ID,
773 SpotMarket,
774 sol_spot
775 );
776 crate::create_anchor_account_info!(
777 sol_perp_market(),
778 constants::PROGRAM_ID,
779 PerpMarket,
780 sol_perp
781 );
782
783 let mut perps = [sol_perp];
784 let mut spot = [usdc_spot, sol_spot];
785 let mut sol_oracle = [sol_oracle];
786 let mut accounts_map = AccountsList::new(&mut perps, &mut spot, &mut sol_oracle);
787
788 let liquidation_price = calculate_liquidation_price_inner(
789 &user,
790 &sol_perp_market(),
791 Some(&sol_spot_market()),
792 sol_usdc_price * QUOTE_PRECISION_I64,
793 &mut accounts_map,
794 )
795 .unwrap();
796 dbg!(liquidation_price);
797 assert_eq!(liquidation_price, 76_335_878);
798 }
799
800 #[test]
801 fn liquidation_price_no_positions() {
802 let user = User::default();
803 let mut accounts_map = AccountsList::new(&mut [], &mut [], &mut []);
804 assert!(calculate_liquidation_price_inner(
805 &user,
806 &sol_perp_market(),
807 None,
808 100,
809 &mut accounts_map
810 )
811 .is_err());
812 }
813
814 #[test]
815 fn unrealized_pnl_short() {
816 let position = PerpPosition {
817 market_index: sol_perp_market().market_index,
818 base_asset_amount: -1 * BASE_PRECISION_I64,
819 quote_entry_amount: 80 * QUOTE_PRECISION_I64,
820 ..Default::default()
821 };
822 let sol_usdc_price = 60 * QUOTE_PRECISION_I64;
823
824 let unrealized_pnl = calculate_unrealized_pnl_inner(&position, sol_usdc_price).unwrap();
825
826 dbg!(unrealized_pnl);
827 assert_eq!(unrealized_pnl, 20_i128 * QUOTE_PRECISION_I64 as i128);
829 }
830
831 #[test]
832 fn liquidation_price_hedged_short() {
833 let mut user = User::default();
834 user.perp_positions[0] = PerpPosition {
835 market_index: sol_perp_market().market_index,
836 base_asset_amount: -10 * BASE_PRECISION_I64,
837 quote_entry_amount: 80 * QUOTE_PRECISION_I64,
838 ..Default::default()
839 };
840 user.spot_positions[0] = SpotPosition {
841 market_index: sol_spot_market().market_index,
842 scaled_balance: 10 * SPOT_BALANCE_PRECISION as u64,
843 ..Default::default()
844 };
845 let sol_usdc_price = 60;
846 let mut sol_oracle_price = get_pyth_price(sol_usdc_price, 6);
847
848 crate::create_account_info!(sol_oracle_price, &SOL_ORACLE, pyth_program::ID, sol_oracle);
849
850 crate::create_anchor_account_info!(
851 usdc_spot_market(),
852 Pubkey::new_unique(),
853 SpotMarket,
854 usdc_spot
855 );
856 crate::create_anchor_account_info!(
857 sol_perp_market(),
858 Pubkey::new_unique(),
859 PerpMarket,
860 sol_perp
861 );
862 crate::create_anchor_account_info!(
863 sol_spot_market(),
864 Pubkey::new_unique(),
865 SpotMarket,
866 sol_spot
867 );
868
869 let mut perps = [sol_perp];
870 let mut spot = [usdc_spot, sol_spot];
871 let mut sol_oracle = [sol_oracle];
872 let mut accounts_map = AccountsList::new(&mut perps, &mut spot, &mut sol_oracle);
873
874 let liq_price = calculate_liquidation_price_inner(
875 &user,
876 &sol_perp_market(),
877 Some(&sol_spot_market()),
878 sol_usdc_price * QUOTE_PRECISION_I64,
879 &mut accounts_map,
880 )
881 .expect("got price");
882 dbg!(liq_price);
883
884 assert_eq!(liq_price, 60 * QUOTE_PRECISION_I64);
885 }
886
887 #[test]
888 fn unrealized_pnl_long() {
889 let position = PerpPosition {
890 market_index: sol_perp_market().market_index,
891 base_asset_amount: 1 * BASE_PRECISION_I64,
892 quote_entry_amount: -80 * QUOTE_PRECISION_I64,
893 ..Default::default()
894 };
895 let sol_usdc_price = 100 * QUOTE_PRECISION_I64;
896
897 let unrealized_pnl = calculate_unrealized_pnl_inner(&position, sol_usdc_price).unwrap();
898
899 dbg!(unrealized_pnl);
900 assert_eq!(unrealized_pnl, 20_i128 * QUOTE_PRECISION_I64 as i128);
902 }
903}