drift_rs/math/
liquidation.rs

1//!
2//! liquidation and margin helpers
3//!
4
5use 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/// Info on a position's liquidation price and unrealized PnL
28#[derive(Debug)]
29pub struct LiquidationAndPnlInfo {
30    // PRICE_PRECISION
31    pub liquidation_price: i64,
32    // PRICE_PRECISION
33    pub unrealized_pnl: i128,
34    // The oracle price used in calculations
35    // BASE_PRECISION
36    pub oracle_price: i64,
37}
38
39/// Calculate the liquidation price and unrealized PnL of a user's perp position (given by `market_index`)
40pub 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    // build a list of all user positions for margin calculations
55    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    // matching spot market e.g. sol-perp => SOL spot
72    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
91/// Calculate the unrealized pnl for user perp position, given by `market_index`
92pub 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
120/// Calculate the liquidation price of a user's perp position (given by `market_index`)
121///
122/// Returns the liquidation price (PRICE_PRECISION / 1e6)
123pub 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    // matching spot market e.g. sol-perp => SOL spot
140    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
155/// Calculate liquidation price of a users perp postion
156/// considers all of the users open positions
157///
158/// - `perp_market` Market info of the perp position
159/// - `spot_market` Corresponding spot market (e.g. SOL-perp => SOL spot)
160/// - `accounts` collection of all accounts (markets, oracles) to perform margin calculations
161///
162pub 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    // calculate perp free collateral delta
176    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    // user holding spot asset case
191    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    // calculate liquidation price
209    // what price delta causes free collateral == 0
210    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, // unused by Maintenance margin type, hence 0
271                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    /// initial margin requirement (PRICE_PRECISION)
294    pub initial: u128,
295    /// maintenance margin requirement (PRICE_PRECISION)
296    pub maintenance: u128,
297}
298
299/// Calculate the margin requirements of `user`
300pub 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
310/// Calculate the margin requirements of `user` (internal)
311fn 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    /// total collateral (QUOTE_PRECISION)
336    pub total: i128,
337    /// free collateral (QUOTE_PRECISION)
338    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, // 1_000/1_000_000 = .001
453            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, // 0.25btc
699            ..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        // entry at $80, upnl at $60
828        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        // entry at $80, upnl at $100
901        assert_eq!(unrealized_pnl, 20_i128 * QUOTE_PRECISION_I64 as i128);
902    }
903}