Skip to content

Commit

Permalink
async calculations (#65)
Browse files Browse the repository at this point in the history
* Make the margin and PnL calcs async by default (makes it just work even when user is not subscribed to required markets/oracles)
* Make missing subscribed data error more descriptive
* use async accountmap build
* include oracle price in liq/pnl result
  • Loading branch information
jordy25519 authored Oct 24, 2024
1 parent fa69512 commit 09ac04f
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 39 deletions.
2 changes: 1 addition & 1 deletion crates/drift-ffi-sys
Submodule drift-ffi-sys updated 2 files
+6 −6 Cargo.lock
+3 −3 Cargo.toml
6 changes: 4 additions & 2 deletions crates/src/drift_idl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
#![doc = r""]
#![doc = r" Auto-generated IDL types, manual edits do not persist (see `crates/drift-idl-gen`)"]
#![doc = r""]
use self::traits::ToAccountMetas;
use anchor_lang::{
prelude::{
account,
Expand All @@ -12,6 +11,8 @@ use anchor_lang::{
Discriminator,
};
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};

use self::traits::ToAccountMetas;
pub mod traits {
use solana_sdk::instruction::AccountMeta;
#[doc = r" This is distinct from the anchor version of the trait"]
Expand Down Expand Up @@ -1801,8 +1802,9 @@ pub mod instructions {
impl anchor_lang::InstructionData for InitializePythPullOracle {}
}
pub mod types {
use super::*;
use std::ops::Mul;

use super::*;
#[doc = r" backwards compatible u128 deserializing data from rust <=1.76.0 when u/i128 was 8-byte aligned"]
#[doc = r" https://solana.stackexchange.com/questions/7720/using-u128-without-sacrificing-alignment-8"]
#[derive(
Expand Down
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 09ac04f

Please sign in to comment.