2022-09-23 02:59:18 -07:00
|
|
|
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
|
2023-01-18 01:50:23 -08:00
|
|
|
use mango_v4::state::{
|
2023-07-11 23:38:38 -07:00
|
|
|
Bank, BookSide, MangoAccountValue, PerpPosition, PlaceOrderType, Side, TokenIndex,
|
|
|
|
TokenPosition, QUOTE_TOKEN_INDEX,
|
2023-01-18 01:50:23 -08:00
|
|
|
};
|
2023-02-20 05:08:38 -08:00
|
|
|
use mango_v4_client::{
|
2023-07-11 23:38:38 -07:00
|
|
|
chain_data, perp_pnl, AccountFetcher, AnyhowWrap, JupiterSwapMode, MangoClient,
|
|
|
|
PerpMarketContext, TokenContext,
|
2023-02-20 05:08:38 -08:00
|
|
|
};
|
2022-08-05 11:28:14 -07:00
|
|
|
|
|
|
|
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
use futures::{stream, StreamExt, TryStreamExt};
|
2023-01-18 01:50:23 -08:00
|
|
|
use solana_sdk::signature::Signature;
|
2023-02-13 06:22:16 -08:00
|
|
|
use std::sync::Arc;
|
2022-08-07 11:04:19 -07:00
|
|
|
use std::{collections::HashMap, time::Duration};
|
2023-07-11 23:38:38 -07:00
|
|
|
use tracing::*;
|
2022-08-05 11:28:14 -07:00
|
|
|
|
2022-12-20 05:14:20 -08:00
|
|
|
#[derive(Clone)]
|
2022-08-05 11:28:14 -07:00
|
|
|
pub struct Config {
|
2023-07-03 05:09:11 -07:00
|
|
|
pub enabled: bool,
|
2022-12-19 06:20:00 -08:00
|
|
|
/// Maximum slippage allowed in Jupiter
|
2023-02-03 02:34:06 -08:00
|
|
|
pub slippage_bps: u64,
|
2022-12-19 06:20:00 -08:00
|
|
|
/// When closing borrows, the rebalancer can't close token positions exactly.
|
|
|
|
/// Instead it purchases too much and then gets rid of the excess in a second step.
|
|
|
|
/// If this is 1.05, then it'll swap borrow_value * 1.05 quote token into borrow token.
|
|
|
|
pub borrow_settle_excess: f64,
|
2022-08-07 11:04:19 -07:00
|
|
|
pub refresh_timeout: Duration,
|
2022-08-05 11:28:14 -07:00
|
|
|
}
|
|
|
|
|
2022-08-07 11:04:19 -07:00
|
|
|
#[derive(Debug)]
|
2022-08-05 11:28:14 -07:00
|
|
|
struct TokenState {
|
2022-08-10 00:30:58 -07:00
|
|
|
price: I80F48,
|
2022-08-05 11:28:14 -07:00
|
|
|
native_position: I80F48,
|
2023-05-05 00:11:12 -07:00
|
|
|
in_use: bool,
|
2022-08-05 11:28:14 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
impl TokenState {
|
2022-12-16 04:10:46 -08:00
|
|
|
async fn new_position(
|
2022-08-05 11:28:14 -07:00
|
|
|
token: &TokenContext,
|
|
|
|
position: &TokenPosition,
|
|
|
|
account_fetcher: &chain_data::AccountFetcher,
|
|
|
|
) -> anyhow::Result<Self> {
|
2022-08-10 00:30:58 -07:00
|
|
|
let bank = Self::bank(token, account_fetcher)?;
|
2022-08-05 11:28:14 -07:00
|
|
|
Ok(Self {
|
2022-12-16 04:10:46 -08:00
|
|
|
price: Self::fetch_price(token, &bank, account_fetcher).await?,
|
2022-08-05 11:28:14 -07:00
|
|
|
native_position: position.native(&bank),
|
2023-05-05 00:11:12 -07:00
|
|
|
in_use: position.is_in_use(),
|
2022-08-05 11:28:14 -07:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-08-10 00:30:58 -07:00
|
|
|
fn bank(
|
|
|
|
token: &TokenContext,
|
|
|
|
account_fetcher: &chain_data::AccountFetcher,
|
|
|
|
) -> anyhow::Result<Bank> {
|
|
|
|
account_fetcher.fetch::<Bank>(&token.mint_info.first_bank())
|
|
|
|
}
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
async fn fetch_price(
|
2022-08-05 11:28:14 -07:00
|
|
|
token: &TokenContext,
|
|
|
|
bank: &Bank,
|
|
|
|
account_fetcher: &chain_data::AccountFetcher,
|
|
|
|
) -> anyhow::Result<I80F48> {
|
2022-12-16 04:10:46 -08:00
|
|
|
let oracle = account_fetcher
|
|
|
|
.fetch_raw_account(&token.mint_info.oracle)
|
|
|
|
.await?;
|
2022-11-10 06:47:11 -08:00
|
|
|
bank.oracle_price(
|
|
|
|
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
|
|
|
|
None,
|
|
|
|
)
|
2022-08-05 11:28:14 -07:00
|
|
|
.map_err_anyhow()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-13 06:22:16 -08:00
|
|
|
pub struct Rebalancer {
|
|
|
|
pub mango_client: Arc<MangoClient>,
|
|
|
|
pub account_fetcher: Arc<chain_data::AccountFetcher>,
|
2023-01-18 01:50:23 -08:00
|
|
|
pub mango_account_address: Pubkey,
|
|
|
|
pub config: Config,
|
|
|
|
}
|
|
|
|
|
2023-02-13 06:22:16 -08:00
|
|
|
impl Rebalancer {
|
2023-01-18 01:50:23 -08:00
|
|
|
pub async fn zero_all_non_quote(&self) -> anyhow::Result<()> {
|
2023-07-03 05:09:11 -07:00
|
|
|
if !self.config.enabled {
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
2023-07-11 23:38:38 -07:00
|
|
|
trace!(
|
|
|
|
pubkey = %self.mango_account_address,
|
|
|
|
"checking for rebalance"
|
|
|
|
);
|
2023-01-18 01:50:23 -08:00
|
|
|
|
|
|
|
self.rebalance_perps().await?;
|
2023-04-17 02:18:50 -07:00
|
|
|
self.rebalance_tokens().await?;
|
2023-01-18 01:50:23 -08:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Function to refresh the mango account after the txsig confirmed. Returns false on timeout.
|
|
|
|
async fn refresh_mango_account_after_tx(&self, txsig: Signature) -> anyhow::Result<bool> {
|
|
|
|
let max_slot = self.account_fetcher.transaction_max_slot(&[txsig]).await?;
|
|
|
|
if let Err(e) = self
|
|
|
|
.account_fetcher
|
|
|
|
.refresh_accounts_via_rpc_until_slot(
|
|
|
|
&[self.mango_account_address],
|
|
|
|
max_slot,
|
|
|
|
self.config.refresh_timeout,
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
{
|
|
|
|
// If we don't get fresh data, maybe the tx landed on a fork?
|
|
|
|
// Rebalance is technically still ok.
|
2023-07-11 23:38:38 -07:00
|
|
|
info!("could not refresh account data: {}", e);
|
2023-01-18 01:50:23 -08:00
|
|
|
return Ok(false);
|
2022-08-05 11:28:14 -07:00
|
|
|
}
|
2023-01-18 01:50:23 -08:00
|
|
|
Ok(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn rebalance_tokens(&self) -> anyhow::Result<()> {
|
|
|
|
let account = self
|
|
|
|
.account_fetcher
|
|
|
|
.fetch_mango_account(&self.mango_account_address)?;
|
|
|
|
|
|
|
|
// TODO: configurable?
|
|
|
|
let quote_token = self.mango_client.context.token(QUOTE_TOKEN_INDEX);
|
|
|
|
|
|
|
|
let tokens: anyhow::Result<HashMap<TokenIndex, TokenState>> =
|
|
|
|
stream::iter(account.active_token_positions())
|
|
|
|
.then(|token_position| async {
|
|
|
|
let token = self.mango_client.context.token(token_position.token_index);
|
|
|
|
Ok((
|
|
|
|
token.token_index,
|
2023-02-13 06:22:16 -08:00
|
|
|
TokenState::new_position(token, token_position, &self.account_fetcher)
|
2023-01-18 01:50:23 -08:00
|
|
|
.await?,
|
|
|
|
))
|
|
|
|
})
|
|
|
|
.try_collect()
|
|
|
|
.await;
|
|
|
|
let tokens = tokens?;
|
2023-07-11 23:38:38 -07:00
|
|
|
trace!(?tokens, "account tokens");
|
2023-01-18 01:50:23 -08:00
|
|
|
|
|
|
|
for (token_index, token_state) in tokens {
|
|
|
|
let token = self.mango_client.context.token(token_index);
|
|
|
|
if token_index == quote_token.token_index {
|
|
|
|
continue;
|
2022-08-10 00:30:58 -07:00
|
|
|
}
|
2023-01-18 01:50:23 -08:00
|
|
|
let token_mint = token.mint_info.mint;
|
|
|
|
let quote_mint = quote_token.mint_info.mint;
|
2022-12-19 06:20:00 -08:00
|
|
|
|
2023-01-18 01:50:23 -08:00
|
|
|
// It's not always possible to bring the native balance to 0 through swaps:
|
|
|
|
// Consider a price <1. You need to sell a bunch of tokens to get 1 USDC native and
|
|
|
|
// similarly will get multiple tokens when buying.
|
|
|
|
// Imagine SOL at 0.04 USDC-native per SOL-native: Any amounts below 25 SOL-native
|
|
|
|
// would not be worth a single USDC-native.
|
|
|
|
//
|
|
|
|
// To avoid errors, we consider all amounts below 2 * (1/oracle) dust and don't try
|
|
|
|
// to sell them. Instead they will be withdrawn at the end.
|
|
|
|
// Purchases will aim to purchase slightly more than is needed, such that we can
|
|
|
|
// again withdraw the dust at the end.
|
|
|
|
let dust_threshold = I80F48::from(2) / token_state.price;
|
|
|
|
|
|
|
|
let mut amount = token_state.native_position;
|
|
|
|
|
|
|
|
if amount < 0 {
|
|
|
|
// Buy
|
|
|
|
let buy_amount =
|
|
|
|
amount.abs().ceil() + (dust_threshold - I80F48::ONE).max(I80F48::ZERO);
|
|
|
|
let input_amount = buy_amount
|
|
|
|
* token_state.price
|
|
|
|
* I80F48::from_num(self.config.borrow_settle_excess);
|
|
|
|
let txsig = self
|
|
|
|
.mango_client
|
|
|
|
.jupiter_swap(
|
|
|
|
quote_mint,
|
|
|
|
token_mint,
|
|
|
|
input_amount.to_num::<u64>(),
|
2023-02-03 02:34:06 -08:00
|
|
|
self.config.slippage_bps,
|
2023-02-20 05:08:38 -08:00
|
|
|
JupiterSwapMode::ExactIn,
|
2023-07-10 01:40:48 -07:00
|
|
|
false,
|
2023-01-18 01:50:23 -08:00
|
|
|
)
|
|
|
|
.await?;
|
2023-07-11 23:38:38 -07:00
|
|
|
info!(
|
|
|
|
%txsig,
|
|
|
|
"bought {} {} for {}",
|
2023-01-18 01:50:23 -08:00
|
|
|
token.native_to_ui(buy_amount),
|
|
|
|
token.name,
|
|
|
|
quote_token.name,
|
|
|
|
);
|
|
|
|
if !self.refresh_mango_account_after_tx(txsig).await? {
|
|
|
|
return Ok(());
|
|
|
|
}
|
2023-02-13 06:22:16 -08:00
|
|
|
let bank = TokenState::bank(token, &self.account_fetcher)?;
|
2023-01-18 01:50:23 -08:00
|
|
|
amount = self
|
|
|
|
.mango_client
|
|
|
|
.mango_account()
|
|
|
|
.await?
|
|
|
|
.token_position_and_raw_index(token_index)
|
|
|
|
.map(|(position, _)| position.native(&bank))
|
|
|
|
.unwrap_or(I80F48::ZERO);
|
|
|
|
}
|
|
|
|
|
|
|
|
if amount > dust_threshold {
|
|
|
|
// Sell
|
|
|
|
let txsig = self
|
|
|
|
.mango_client
|
|
|
|
.jupiter_swap(
|
|
|
|
token_mint,
|
|
|
|
quote_mint,
|
|
|
|
amount.to_num::<u64>(),
|
2023-02-03 02:34:06 -08:00
|
|
|
self.config.slippage_bps,
|
2023-02-20 05:08:38 -08:00
|
|
|
JupiterSwapMode::ExactIn,
|
2023-07-10 01:40:48 -07:00
|
|
|
false,
|
2023-01-18 01:50:23 -08:00
|
|
|
)
|
|
|
|
.await?;
|
2023-07-11 23:38:38 -07:00
|
|
|
info!(
|
|
|
|
%txsig,
|
|
|
|
"sold {} {} for {}",
|
2023-01-18 01:50:23 -08:00
|
|
|
token.native_to_ui(amount),
|
|
|
|
token.name,
|
|
|
|
quote_token.name,
|
|
|
|
);
|
|
|
|
if !self.refresh_mango_account_after_tx(txsig).await? {
|
|
|
|
return Ok(());
|
|
|
|
}
|
2023-02-13 06:22:16 -08:00
|
|
|
let bank = TokenState::bank(token, &self.account_fetcher)?;
|
2023-01-18 01:50:23 -08:00
|
|
|
amount = self
|
|
|
|
.mango_client
|
|
|
|
.mango_account()
|
|
|
|
.await?
|
|
|
|
.token_position_and_raw_index(token_index)
|
|
|
|
.map(|(position, _)| position.native(&bank))
|
|
|
|
.unwrap_or(I80F48::ZERO);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Any remainder that could not be sold just gets withdrawn to ensure the
|
|
|
|
// TokenPosition is freed up
|
2023-05-05 00:11:12 -07:00
|
|
|
if amount > 0 && amount <= dust_threshold && !token_state.in_use {
|
2023-01-18 01:50:23 -08:00
|
|
|
let allow_borrow = false;
|
|
|
|
let txsig = self
|
|
|
|
.mango_client
|
2023-03-09 01:01:02 -08:00
|
|
|
.token_withdraw(token_mint, u64::MAX, allow_borrow)
|
2023-01-18 01:50:23 -08:00
|
|
|
.await?;
|
2023-07-11 23:38:38 -07:00
|
|
|
info!(
|
|
|
|
%txsig,
|
|
|
|
"withdrew {} {} to liqor wallet",
|
2023-01-18 01:50:23 -08:00
|
|
|
token.native_to_ui(amount),
|
|
|
|
token.name,
|
|
|
|
);
|
|
|
|
if !self.refresh_mango_account_after_tx(txsig).await? {
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
} else if amount > dust_threshold {
|
|
|
|
anyhow::bail!(
|
|
|
|
"unexpected {} position after rebalance swap: {} native",
|
|
|
|
token.name,
|
|
|
|
amount
|
|
|
|
);
|
2022-08-10 00:30:58 -07:00
|
|
|
}
|
|
|
|
}
|
2022-08-05 11:28:14 -07:00
|
|
|
|
2023-01-18 01:50:23 -08:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2023-07-11 23:38:38 -07:00
|
|
|
#[instrument(
|
|
|
|
skip_all,
|
|
|
|
fields(
|
|
|
|
perp_market_name = perp.market.name(),
|
|
|
|
base_lots = perp_position.base_position_lots(),
|
|
|
|
effective_lots = perp_position.effective_base_position_lots(),
|
|
|
|
quote_native = %perp_position.quote_position_native()
|
|
|
|
)
|
|
|
|
)]
|
|
|
|
async fn rebalance_perp(
|
|
|
|
&self,
|
|
|
|
account: &MangoAccountValue,
|
|
|
|
perp: &PerpMarketContext,
|
|
|
|
perp_position: &PerpPosition,
|
|
|
|
) -> anyhow::Result<bool> {
|
2023-01-18 01:50:23 -08:00
|
|
|
let now_ts: u64 = std::time::SystemTime::now()
|
|
|
|
.duration_since(std::time::UNIX_EPOCH)?
|
|
|
|
.as_secs()
|
|
|
|
.try_into()?;
|
|
|
|
|
2023-07-11 23:38:38 -07:00
|
|
|
let base_lots = perp_position.base_position_lots();
|
|
|
|
let effective_lots = perp_position.effective_base_position_lots();
|
|
|
|
let quote_native = perp_position.quote_position_native();
|
2023-01-18 01:50:23 -08:00
|
|
|
|
2023-07-11 23:38:38 -07:00
|
|
|
if effective_lots != 0 {
|
|
|
|
// send an ioc order to reduce the base position
|
|
|
|
let oracle_account_data = self.account_fetcher.fetch_raw(&perp.market.oracle)?;
|
|
|
|
let oracle_account =
|
|
|
|
KeyedAccountSharedData::new(perp.market.oracle, oracle_account_data);
|
|
|
|
let oracle_price = perp.market.oracle_price(&oracle_account, None)?;
|
|
|
|
let oracle_price_lots = perp.market.native_price_to_lot(oracle_price);
|
|
|
|
let (side, order_price, oo_lots) = if effective_lots > 0 {
|
|
|
|
(
|
|
|
|
Side::Ask,
|
|
|
|
oracle_price * (I80F48::ONE - perp.market.base_liquidation_fee),
|
|
|
|
perp_position.asks_base_lots,
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
(
|
|
|
|
Side::Bid,
|
|
|
|
oracle_price * (I80F48::ONE + perp.market.base_liquidation_fee),
|
|
|
|
perp_position.bids_base_lots,
|
|
|
|
)
|
|
|
|
};
|
|
|
|
let price_lots = perp.market.native_price_to_lot(order_price);
|
|
|
|
let max_base_lots = effective_lots.abs() - oo_lots;
|
|
|
|
if max_base_lots <= 0 {
|
|
|
|
warn!(?side, oo_lots, "cannot place reduce-only order",);
|
|
|
|
return Ok(true);
|
|
|
|
}
|
2023-01-18 01:50:23 -08:00
|
|
|
|
2023-07-11 23:38:38 -07:00
|
|
|
// Check the orderbook before sending the ioc order to see if we could
|
|
|
|
// even match anything. That way we don't need to pay the tx fee and
|
|
|
|
// ioc penalty fee unnecessarily.
|
|
|
|
let opposite_side_key = match side.invert_side() {
|
|
|
|
Side::Bid => perp.market.bids,
|
|
|
|
Side::Ask => perp.market.asks,
|
|
|
|
};
|
|
|
|
let bookside = Box::new(self.account_fetcher.fetch::<BookSide>(&opposite_side_key)?);
|
|
|
|
if bookside.quantity_at_price(price_lots, now_ts, oracle_price_lots) <= 0 {
|
|
|
|
warn!(
|
|
|
|
other_side = ?side.invert_side(),
|
|
|
|
%order_price,
|
|
|
|
%oracle_price,
|
|
|
|
"no liquidity",
|
2023-01-18 01:50:23 -08:00
|
|
|
);
|
2023-07-11 23:38:38 -07:00
|
|
|
return Ok(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
let txsig = self
|
|
|
|
.mango_client
|
|
|
|
.perp_place_order(
|
2023-01-18 01:50:23 -08:00
|
|
|
perp_position.market_index,
|
2023-07-11 23:38:38 -07:00
|
|
|
side,
|
|
|
|
price_lots,
|
|
|
|
max_base_lots,
|
|
|
|
i64::MAX,
|
|
|
|
0,
|
|
|
|
PlaceOrderType::ImmediateOrCancel,
|
|
|
|
true, // reduce only
|
|
|
|
0,
|
|
|
|
10,
|
|
|
|
mango_v4::state::SelfTradeBehavior::DecrementTake,
|
2023-01-18 01:50:23 -08:00
|
|
|
)
|
|
|
|
.await?;
|
2023-07-11 23:38:38 -07:00
|
|
|
info!(
|
|
|
|
%txsig,
|
|
|
|
%order_price,
|
|
|
|
"attempt to ioc reduce perp base position"
|
|
|
|
);
|
|
|
|
if !self.refresh_mango_account_after_tx(txsig).await? {
|
|
|
|
return Ok(false);
|
|
|
|
}
|
|
|
|
} else if base_lots == 0 && quote_native != 0 {
|
|
|
|
// settle pnl
|
|
|
|
let direction = if quote_native > 0 {
|
|
|
|
perp_pnl::Direction::MaxNegative
|
|
|
|
} else {
|
|
|
|
perp_pnl::Direction::MaxPositive
|
|
|
|
};
|
|
|
|
let counters = perp_pnl::fetch_top(
|
|
|
|
&self.mango_client.context,
|
|
|
|
self.account_fetcher.as_ref(),
|
|
|
|
perp_position.market_index,
|
|
|
|
direction,
|
|
|
|
2,
|
|
|
|
)
|
|
|
|
.await?;
|
|
|
|
if counters.is_empty() {
|
|
|
|
// If we can't settle some positive PNL because we're lacking a suitable counterparty,
|
|
|
|
// then liquidation should continue, even though this step produced no transaction
|
|
|
|
info!("could not settle perp pnl on perp market: no counterparty",);
|
|
|
|
return Ok(true);
|
|
|
|
}
|
|
|
|
let (counter_key, counter_acc, _counter_pnl) = counters.first().unwrap();
|
2023-01-18 01:50:23 -08:00
|
|
|
|
2023-07-11 23:38:38 -07:00
|
|
|
let (account_a, account_b) = if quote_native > 0 {
|
|
|
|
(
|
|
|
|
(&self.mango_account_address, account),
|
|
|
|
(counter_key, counter_acc),
|
|
|
|
)
|
2023-01-18 01:50:23 -08:00
|
|
|
} else {
|
2023-07-11 23:38:38 -07:00
|
|
|
(
|
|
|
|
(counter_key, counter_acc),
|
|
|
|
(&self.mango_account_address, account),
|
|
|
|
)
|
|
|
|
};
|
|
|
|
let txsig = self
|
|
|
|
.mango_client
|
|
|
|
.perp_settle_pnl(perp_position.market_index, account_a, account_b)
|
|
|
|
.await?;
|
|
|
|
info!(%txsig, "settled perp pnl");
|
|
|
|
if !self.refresh_mango_account_after_tx(txsig).await? {
|
|
|
|
return Ok(false);
|
|
|
|
}
|
|
|
|
} else if base_lots == 0 && quote_native == 0 {
|
|
|
|
// close perp position
|
|
|
|
let txsig = self
|
|
|
|
.mango_client
|
|
|
|
.perp_deactivate_position(perp_position.market_index)
|
|
|
|
.await?;
|
|
|
|
info!(
|
|
|
|
%txsig, "closed perp position"
|
|
|
|
);
|
|
|
|
if !self.refresh_mango_account_after_tx(txsig).await? {
|
|
|
|
return Ok(false);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// maybe we're still waiting for consume_events
|
|
|
|
info!("cannot deactivate perp position, waiting for consume events?");
|
|
|
|
}
|
|
|
|
Ok(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn rebalance_perps(&self) -> anyhow::Result<()> {
|
|
|
|
let account = Box::new(
|
|
|
|
self.account_fetcher
|
|
|
|
.fetch_mango_account(&self.mango_account_address)?,
|
|
|
|
);
|
|
|
|
|
|
|
|
for perp_position in account.active_perp_positions() {
|
|
|
|
let perp = self.mango_client.context.perp(perp_position.market_index);
|
|
|
|
if !self.rebalance_perp(&account, perp, perp_position).await? {
|
|
|
|
return Ok(());
|
2022-08-09 09:02:36 -07:00
|
|
|
}
|
|
|
|
}
|
2022-08-07 11:04:19 -07:00
|
|
|
|
2023-01-18 01:50:23 -08:00
|
|
|
Ok(())
|
|
|
|
}
|
2022-08-05 11:28:14 -07:00
|
|
|
}
|