From 4d951febb047b8b5f7ac1f79bd2e1abdb78a504a Mon Sep 17 00:00:00 2001 From: jordy25519 Date: Thu, 24 Oct 2024 12:01:13 +0800 Subject: [PATCH] Fix/async calculations (#66) * Make the margin and PnL calcs async by default (makes things just work even when user is not subscribed) Make missing subscribed data error more descriptive * use async accountmap build * include oracle price in LiquidationAndPnlInfo --- crates/src/lib.rs | 10 +-- crates/src/math/account_map_builder.rs | 91 +++++++++++++++++++++++++- crates/src/math/liquidation.rs | 76 +++++++++++++-------- crates/src/types.rs | 6 +- 4 files changed, 147 insertions(+), 36 deletions(-) diff --git a/crates/src/lib.rs b/crates/src/lib.rs index 77aa6ec..19b2e35 100644 --- a/crates/src/lib.rs +++ b/crates/src/lib.rs @@ -367,7 +367,7 @@ impl DriftClient { { Some(market) => Ok(market.data), None => { - debug!(target: "rpc", "fetch spot market: {market_index}"); + debug!(target: "rpc", "fetch market: spot/{market_index}"); let market = derive_spot_market_account(market_index); self.backend.get_account(&market).await } @@ -384,7 +384,7 @@ impl DriftClient { { Some(market) => Ok(market.data), None => { - debug!(target: "rpc", "fetch perp market: {market_index}"); + debug!(target: "rpc", "fetch market: perp/{market_index}"); let market = derive_perp_market_account(market_index); self.backend.get_account(&market).await } @@ -401,7 +401,7 @@ impl DriftClient { { Ok(market.data) } else { - Err(SdkError::NoData) + Err(SdkError::NoMarketData(MarketId::spot(market_index))) } } @@ -415,7 +415,7 @@ impl DriftClient { { Ok(market.data) } else { - Err(SdkError::NoData) + Err(SdkError::NoMarketData(MarketId::perp(market_index))) } } @@ -744,7 +744,7 @@ impl DriftClientBackend { fn try_get_account(&self, account: &Pubkey) -> SdkResult { self.account_map .account_data(account) - .ok_or(SdkError::NoData) + .ok_or_else(|| SdkError::NoAccountData(account.clone())) } /// Returns latest blockhash diff --git a/crates/src/math/account_map_builder.rs b/crates/src/math/account_map_builder.rs index 4373a31..fa829f6 100644 --- a/crates/src/math/account_map_builder.rs +++ b/crates/src/math/account_map_builder.rs @@ -82,7 +82,96 @@ impl AccountsListBuilder { for (oracle_key, market) in oracle_markets.iter() { let oracle = client .try_get_oracle_price_data_and_slot(*market) - .ok_or(SdkError::NoData)?; + .ok_or(SdkError::NoMarketData(*market))?; + + latest_oracle_slot = oracle.slot.max(oracle.slot); + let oracle_owner = oracle_source_to_owner(client.context, oracle.source); + self.oracle_accounts.push( + ( + *oracle_key, + Account { + data: oracle.raw, + owner: oracle_owner, + ..Default::default() + }, + ) + .into(), + ); + } + + Ok(AccountsList { + perp_markets: self.perp_accounts.as_mut_slice(), + spot_markets: self.spot_accounts.as_mut_slice(), + oracles: self.oracle_accounts.as_mut_slice(), + oracle_guard_rails: Some(drift_state_account.oracle_guard_rails), + latest_slot: latest_oracle_slot, + }) + } + + /// Constructs an account map from `user` positions + /// + /// like `try_build` but will fall back to network queries to fetch market/oracle accounts as required + /// if the client is already subscribed to necessary market/oracles then no network requests are made. + pub async fn build(&mut self, client: &DriftClient, user: &User) -> SdkResult { + let mut oracle_markets = + HashMap::::with_capacity_and_hasher(16, Default::default()); + let mut spot_markets = Vec::::with_capacity(user.spot_positions.len()); + let mut perp_markets = Vec::::with_capacity(user.perp_positions.len()); + let drift_state_account = client.try_get_account::(state_account())?; + + for p in user.spot_positions.iter().filter(|p| !p.is_available()) { + let market = client.get_spot_market_account(p.market_index).await?; + oracle_markets.insert(market.oracle, MarketId::spot(market.market_index)); + spot_markets.push(market); + } + + let quote_market = client + .get_spot_market_account(MarketId::QUOTE_SPOT.index()) + .await?; + if oracle_markets + .insert(quote_market.oracle, MarketId::QUOTE_SPOT) + .is_none() + { + spot_markets.push(quote_market); + } + + for p in user.perp_positions.iter().filter(|p| !p.is_available()) { + let market = client.get_perp_market_account(p.market_index).await?; + oracle_markets.insert(market.amm.oracle, MarketId::perp(market.market_index)); + perp_markets.push(market); + } + + for market in spot_markets.iter() { + self.spot_accounts.push( + ( + market.pubkey, + Account { + data: zero_account_to_bytes(*market), + owner: constants::PROGRAM_ID, + ..Default::default() + }, + ) + .into(), + ); + } + + for market in perp_markets.iter() { + self.perp_accounts.push( + ( + market.pubkey, + Account { + data: zero_account_to_bytes(*market), + owner: constants::PROGRAM_ID, + ..Default::default() + }, + ) + .into(), + ); + } + + let mut latest_oracle_slot = 0; + for (oracle_key, market) in oracle_markets.iter() { + let oracle = client.get_oracle_price_data_and_slot(*market).await?; latest_oracle_slot = oracle.slot.max(oracle.slot); let oracle_owner = oracle_source_to_owner(client.context, oracle.source); diff --git a/crates/src/math/liquidation.rs b/crates/src/math/liquidation.rs index c224aec..dbc0bb8 100644 --- a/crates/src/math/liquidation.rs +++ b/crates/src/math/liquidation.rs @@ -5,7 +5,7 @@ use std::ops::Neg; use crate::{ ffi::{ - calculate_margin_requirement_and_total_collateral_and_liability_info, AccountsList, + self, calculate_margin_requirement_and_total_collateral_and_liability_info, AccountsList, MarginContextMode, }, math::{ @@ -22,65 +22,81 @@ use crate::{ DriftClient, MarketId, SdkError, SdkResult, SpotPosition, }; -/// Info on a positions liquidation price and unrealized PnL +/// Info on a position's liquidation price and unrealized PnL #[derive(Debug)] pub struct LiquidationAndPnlInfo { // PRICE_PRECISION pub liquidation_price: i64, // PRICE_PRECISION pub unrealized_pnl: i128, + // The oracle price used in calculations + // BASE_PRECISION + pub oracle_price: i64, } /// Calculate the liquidation price and unrealized PnL of a user's perp position (given by `market_index`) -pub fn calculate_liquidation_price_and_unrealized_pnl( +pub async fn calculate_liquidation_price_and_unrealized_pnl( client: &DriftClient, user: &User, market_index: u16, ) -> SdkResult { - let perp_market = client.try_get_perp_market_account(market_index)?; - let oracle = client - .try_get_oracle_price_data_and_slot(MarketId::perp(market_index)) - .ok_or(SdkError::NoData)?; + let perp_market = client + .program_data() + .perp_market_config_by_index(market_index) + .expect("market exists"); let position = user .get_perp_position(market_index) .map_err(|_| SdkError::NoPosiiton(market_index))?; - let unrealized_pnl = calculate_unrealized_pnl_inner(&position, oracle.data.price)?; + // build a list of all user positions for margin calculations + let mut builder = AccountsListBuilder::default(); + let mut accounts_list = builder.build(client, user).await?; + + let oracle = accounts_list + .oracles + .iter() + .find(|o| o.key == perp_market.amm.oracle) + .expect("oracle loaded"); + let oracle_source = perp_market.amm.oracle_source; + let oracle_price = ffi::get_oracle_price( + oracle_source, + &mut (oracle.key, oracle.account.clone()), + accounts_list.latest_slot, + )? + .price; // matching spot market e.g. sol-perp => SOL spot - let mut builder = AccountsListBuilder::default(); - let mut accounts = builder.try_build(client, user)?; let spot_market = client .program_data() .spot_market_configs() .iter() .find(|x| x.oracle == perp_market.amm.oracle); - let liquidation_price = calculate_liquidation_price_inner( - user, - &perp_market, - spot_market, - oracle.data.price, - &mut accounts, - )?; Ok(LiquidationAndPnlInfo { - unrealized_pnl, - liquidation_price, + unrealized_pnl: calculate_unrealized_pnl_inner(&position, oracle_price)?, + liquidation_price: calculate_liquidation_price_inner( + user, + &perp_market, + spot_market, + oracle_price, + &mut accounts_list, + )?, + oracle_price, }) } /// Calculate the unrealized pnl for user perp position, given by `market_index` -pub fn calculate_unrealized_pnl( +pub async fn calculate_unrealized_pnl( client: &DriftClient, user: &User, market_index: u16, ) -> SdkResult { if let Ok(position) = user.get_perp_position(market_index) { let oracle_price = client - .try_get_oracle_price_data_and_slot(MarketId::perp(market_index)) - .map(|x| x.data.price) - .ok_or(SdkError::NoData)?; + .get_oracle_price_data_and_slot(MarketId::perp(market_index)) + .await + .map(|x| x.data.price)?; calculate_unrealized_pnl_inner(&position, oracle_price) } else { @@ -100,19 +116,23 @@ pub fn calculate_unrealized_pnl_inner( } /// Calculate the liquidation price of a user's perp position (given by `market_index`) +/// /// Returns the liquidaton price (PRICE_PRECISION / 1e6) -pub fn calculate_liquidation_price( +pub async fn calculate_liquidation_price( client: &DriftClient, user: &User, market_index: u16, ) -> SdkResult { let mut accounts_builder = AccountsListBuilder::default(); - let mut account_maps = accounts_builder.try_build(client, user)?; - let perp_market = client.try_get_perp_market_account(market_index)?; + let mut account_maps = accounts_builder.build(client, user).await?; + let perp_market = client + .program_data() + .perp_market_config_by_index(market_index) + .expect("market exists"); let oracle = client - .try_get_oracle_price_data_and_slot(MarketId::perp(market_index)) - .ok_or(SdkError::NoData)?; + .get_oracle_price_data_and_slot(MarketId::perp(market_index)) + .await?; // matching spot market e.g. sol-perp => SOL spot let spot_market = client diff --git a/crates/src/types.rs b/crates/src/types.rs index 76dcd6e..aa5a507 100644 --- a/crates/src/types.rs +++ b/crates/src/types.rs @@ -313,8 +313,10 @@ pub enum SdkError { MaxReconnectionAttemptsReached, #[error("jit taker order not found")] JitOrderNotFound, - #[error("no data. client may be unsubscribed")] - NoData, + #[error("market data unavailable. subscribe market: {0:?}")] + NoMarketData(MarketId), + #[error("account data unavailable. subscribe account: {0:?}")] + NoAccountData(Pubkey), #[error("component is already subscribed")] AlreadySubscribed, #[error("invalid URL")]