Skip to content

Commit

Permalink
Fix/async calculations (#66)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jordy25519 authored Oct 24, 2024
1 parent 3b49f57 commit 4d951fe
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 36 deletions.
10 changes: 5 additions & 5 deletions crates/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -401,7 +401,7 @@ impl DriftClient {
{
Ok(market.data)
} else {
Err(SdkError::NoData)
Err(SdkError::NoMarketData(MarketId::spot(market_index)))
}
}

Expand All @@ -415,7 +415,7 @@ impl DriftClient {
{
Ok(market.data)
} else {
Err(SdkError::NoData)
Err(SdkError::NoMarketData(MarketId::perp(market_index)))
}
}

Expand Down Expand Up @@ -744,7 +744,7 @@ impl DriftClientBackend {
fn try_get_account<T: AccountDeserialize>(&self, account: &Pubkey) -> SdkResult<T> {
self.account_map
.account_data(account)
.ok_or(SdkError::NoData)
.ok_or_else(|| SdkError::NoAccountData(account.clone()))
}

/// Returns latest blockhash
Expand Down
91 changes: 90 additions & 1 deletion crates/src/math/account_map_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AccountsList> {
let mut oracle_markets =
HashMap::<Pubkey, MarketId>::with_capacity_and_hasher(16, Default::default());
let mut spot_markets = Vec::<SpotMarket>::with_capacity(user.spot_positions.len());
let mut perp_markets = Vec::<PerpMarket>::with_capacity(user.perp_positions.len());
let drift_state_account = client.try_get_account::<State>(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);
Expand Down
76 changes: 48 additions & 28 deletions crates/src/math/liquidation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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<LiquidationAndPnlInfo> {
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<i128> {
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 {
Expand All @@ -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<i64> {
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
Expand Down
6 changes: 4 additions & 2 deletions crates/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down

0 comments on commit 4d951fe

Please sign in to comment.