Merge branch 'dev'
This commit is contained in:
commit
246b3a351d
|
@ -1118,10 +1118,12 @@ dependencies = [
|
|||
"anchor-lang",
|
||||
"anchor-spl",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.13.1",
|
||||
"bincode",
|
||||
"fixed",
|
||||
"fixed-macro",
|
||||
"futures 0.3.25",
|
||||
"itertools 0.10.5",
|
||||
"log 0.4.17",
|
||||
"mango-v4",
|
||||
|
@ -1132,6 +1134,7 @@ dependencies = [
|
|||
"serum_dex 0.5.6",
|
||||
"shellexpand",
|
||||
"solana-account-decoder",
|
||||
"solana-address-lookup-table-program",
|
||||
"solana-client",
|
||||
"solana-sdk",
|
||||
"spl-associated-token-account",
|
||||
|
|
|
@ -119,7 +119,8 @@ impl Rpc {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
env_logger::init_from_env(
|
||||
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
|
||||
);
|
||||
|
@ -137,7 +138,7 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
num
|
||||
} else {
|
||||
// find free account_num
|
||||
let accounts = MangoClient::find_accounts(&client, group, &owner)?;
|
||||
let accounts = MangoClient::find_accounts(&client, group, &owner).await?;
|
||||
if accounts.is_empty() {
|
||||
0
|
||||
} else {
|
||||
|
@ -149,14 +150,9 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
+ 1
|
||||
}
|
||||
};
|
||||
let (account, txsig) = MangoClient::create_account(
|
||||
&client,
|
||||
group,
|
||||
&owner,
|
||||
&owner,
|
||||
account_num,
|
||||
&cmd.name,
|
||||
)?;
|
||||
let (account, txsig) =
|
||||
MangoClient::create_account(&client, group, &owner, &owner, account_num, &cmd.name)
|
||||
.await?;
|
||||
println!("{}", account);
|
||||
println!("{}", txsig);
|
||||
}
|
||||
|
@ -165,8 +161,8 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
let account = client::pubkey_from_cli(&cmd.account);
|
||||
let owner = client::keypair_from_cli(&cmd.owner);
|
||||
let mint = client::pubkey_from_cli(&cmd.mint);
|
||||
let client = MangoClient::new_for_existing_account(client, account, owner)?;
|
||||
let txsig = client.token_deposit(mint, cmd.amount)?;
|
||||
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
|
||||
let txsig = client.token_deposit(mint, cmd.amount).await?;
|
||||
println!("{}", txsig);
|
||||
}
|
||||
Command::JupiterSwap(cmd) => {
|
||||
|
@ -175,14 +171,16 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
let owner = client::keypair_from_cli(&cmd.owner);
|
||||
let input_mint = client::pubkey_from_cli(&cmd.input_mint);
|
||||
let output_mint = client::pubkey_from_cli(&cmd.output_mint);
|
||||
let client = MangoClient::new_for_existing_account(client, account, owner)?;
|
||||
let txsig = client.jupiter_swap(
|
||||
input_mint,
|
||||
output_mint,
|
||||
cmd.amount,
|
||||
cmd.slippage,
|
||||
client::JupiterSwapMode::ExactIn,
|
||||
)?;
|
||||
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
|
||||
let txsig = client
|
||||
.jupiter_swap(
|
||||
input_mint,
|
||||
output_mint,
|
||||
cmd.amount,
|
||||
cmd.slippage,
|
||||
client::JupiterSwapMode::ExactIn,
|
||||
)
|
||||
.await?;
|
||||
println!("{}", txsig);
|
||||
}
|
||||
Command::GroupAddress { creator, num } => {
|
||||
|
|
|
@ -11,8 +11,10 @@ anchor-client = { path = "../anchor/client" }
|
|||
anchor-lang = { path = "../anchor/lang" }
|
||||
anchor-spl = { path = "../anchor/spl" }
|
||||
anyhow = "1.0"
|
||||
async-trait = "0.1.52"
|
||||
fixed = { version = "=1.11.0", features = ["serde", "borsh"] }
|
||||
fixed-macro = "^1.1.1"
|
||||
futures = "0.3.25"
|
||||
itertools = "0.10.3"
|
||||
mango-v4 = { path = "../programs/mango-v4", features = ["no-entrypoint", "client"] }
|
||||
pyth-sdk-solana = "0.1.0"
|
||||
|
@ -21,6 +23,7 @@ shellexpand = "2.1.0"
|
|||
solana-account-decoder = "~1.14.9"
|
||||
solana-client = "~1.14.9"
|
||||
solana-sdk = "~1.14.9"
|
||||
solana-address-lookup-table-program = "~1.14.9"
|
||||
spl-associated-token-account = "1.0.3"
|
||||
thiserror = "1.0.31"
|
||||
log = "0.4"
|
||||
|
@ -29,4 +32,4 @@ tokio = { version = "1", features = ["full"] }
|
|||
serde = "1.0.141"
|
||||
serde_json = "1.0.82"
|
||||
base64 = "0.13.0"
|
||||
bincode = "1.3.3"
|
||||
bincode = "1.3.3"
|
||||
|
|
|
@ -6,15 +6,22 @@ use anyhow::Context;
|
|||
use anchor_client::ClientError;
|
||||
use anchor_lang::AccountDeserialize;
|
||||
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||
use solana_sdk::account::{AccountSharedData, ReadableAccount};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
use mango_v4::state::MangoAccountValue;
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
pub trait AccountFetcher: Sync + Send {
|
||||
fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData>;
|
||||
fn fetch_program_accounts(
|
||||
async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData>;
|
||||
async fn fetch_raw_account_lookup_table(
|
||||
&self,
|
||||
address: &Pubkey,
|
||||
) -> anyhow::Result<AccountSharedData> {
|
||||
self.fetch_raw_account(address).await
|
||||
}
|
||||
async fn fetch_program_accounts(
|
||||
&self,
|
||||
program: &Pubkey,
|
||||
discriminator: [u8; 8],
|
||||
|
@ -22,35 +29,37 @@ pub trait AccountFetcher: Sync + Send {
|
|||
}
|
||||
|
||||
// Can't be in the trait, since then it would no longer be object-safe...
|
||||
pub fn account_fetcher_fetch_anchor_account<T: AccountDeserialize>(
|
||||
pub async fn account_fetcher_fetch_anchor_account<T: AccountDeserialize>(
|
||||
fetcher: &dyn AccountFetcher,
|
||||
address: &Pubkey,
|
||||
) -> anyhow::Result<T> {
|
||||
let account = fetcher.fetch_raw_account(address)?;
|
||||
let account = fetcher.fetch_raw_account(address).await?;
|
||||
let mut data: &[u8] = &account.data();
|
||||
T::try_deserialize(&mut data)
|
||||
.with_context(|| format!("deserializing anchor account {}", address))
|
||||
}
|
||||
|
||||
// Can't be in the trait, since then it would no longer be object-safe...
|
||||
pub fn account_fetcher_fetch_mango_account(
|
||||
pub async fn account_fetcher_fetch_mango_account(
|
||||
fetcher: &dyn AccountFetcher,
|
||||
address: &Pubkey,
|
||||
) -> anyhow::Result<MangoAccountValue> {
|
||||
let account = fetcher.fetch_raw_account(address)?;
|
||||
let account = fetcher.fetch_raw_account(address).await?;
|
||||
let data: &[u8] = &account.data();
|
||||
MangoAccountValue::from_bytes(&data[8..])
|
||||
.with_context(|| format!("deserializing mango account {}", address))
|
||||
}
|
||||
|
||||
pub struct RpcAccountFetcher {
|
||||
pub rpc: RpcClient,
|
||||
pub rpc: RpcClientAsync,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl AccountFetcher for RpcAccountFetcher {
|
||||
fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
|
||||
async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
|
||||
self.rpc
|
||||
.get_account_with_commitment(address, self.rpc.commitment())
|
||||
.await
|
||||
.with_context(|| format!("fetch account {}", *address))?
|
||||
.value
|
||||
.ok_or(ClientError::AccountNotFound)
|
||||
|
@ -58,7 +67,7 @@ impl AccountFetcher for RpcAccountFetcher {
|
|||
.map(Into::into)
|
||||
}
|
||||
|
||||
fn fetch_program_accounts(
|
||||
async fn fetch_program_accounts(
|
||||
&self,
|
||||
program: &Pubkey,
|
||||
discriminator: [u8; 8],
|
||||
|
@ -78,7 +87,10 @@ impl AccountFetcher for RpcAccountFetcher {
|
|||
},
|
||||
with_context: Some(true),
|
||||
};
|
||||
let accs = self.rpc.get_program_accounts_with_config(program, config)?;
|
||||
let accs = self
|
||||
.rpc
|
||||
.get_program_accounts_with_config(program, config)
|
||||
.await?;
|
||||
// convert Account -> AccountSharedData
|
||||
Ok(accs
|
||||
.into_iter()
|
||||
|
@ -121,18 +133,19 @@ impl<T: AccountFetcher> CachedAccountFetcher<T> {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl<T: AccountFetcher> AccountFetcher for CachedAccountFetcher<T> {
|
||||
fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
|
||||
async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
|
||||
let mut cache = self.cache.lock().unwrap();
|
||||
if let Some(account) = cache.accounts.get(address) {
|
||||
return Ok(account.clone());
|
||||
}
|
||||
let account = self.fetcher.fetch_raw_account(address)?;
|
||||
let account = self.fetcher.fetch_raw_account(address).await?;
|
||||
cache.accounts.insert(*address, account.clone());
|
||||
Ok(account)
|
||||
}
|
||||
|
||||
fn fetch_program_accounts(
|
||||
async fn fetch_program_accounts(
|
||||
&self,
|
||||
program: &Pubkey,
|
||||
discriminator: [u8; 8],
|
||||
|
@ -147,7 +160,8 @@ impl<T: AccountFetcher> AccountFetcher for CachedAccountFetcher<T> {
|
|||
}
|
||||
let accounts = self
|
||||
.fetcher
|
||||
.fetch_program_accounts(program, discriminator)?;
|
||||
.fetch_program_accounts(program, discriminator)
|
||||
.await?;
|
||||
cache
|
||||
.keys_for_program_and_discriminator
|
||||
.insert(cache_key, accounts.iter().map(|(pk, _)| *pk).collect());
|
||||
|
|
|
@ -11,7 +11,7 @@ use mango_v4::state::{MangoAccount, MangoAccountValue};
|
|||
|
||||
use anyhow::Context;
|
||||
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||
use solana_sdk::account::{AccountSharedData, ReadableAccount};
|
||||
use solana_sdk::clock::Slot;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
@ -19,7 +19,7 @@ use solana_sdk::signature::Signature;
|
|||
|
||||
pub struct AccountFetcher {
|
||||
pub chain_data: Arc<RwLock<ChainData>>,
|
||||
pub rpc: RpcClient,
|
||||
pub rpc: RpcClientAsync,
|
||||
}
|
||||
|
||||
impl AccountFetcher {
|
||||
|
@ -55,16 +55,19 @@ impl AccountFetcher {
|
|||
}
|
||||
|
||||
// fetches via RPC, stores in ChainData, returns new version
|
||||
pub fn fetch_fresh<T: anchor_lang::ZeroCopy + anchor_lang::Owner>(
|
||||
pub async fn fetch_fresh<T: anchor_lang::ZeroCopy + anchor_lang::Owner>(
|
||||
&self,
|
||||
address: &Pubkey,
|
||||
) -> anyhow::Result<T> {
|
||||
self.refresh_account_via_rpc(address)?;
|
||||
self.refresh_account_via_rpc(address).await?;
|
||||
self.fetch(address)
|
||||
}
|
||||
|
||||
pub fn fetch_fresh_mango_account(&self, address: &Pubkey) -> anyhow::Result<MangoAccountValue> {
|
||||
self.refresh_account_via_rpc(address)?;
|
||||
pub async fn fetch_fresh_mango_account(
|
||||
&self,
|
||||
address: &Pubkey,
|
||||
) -> anyhow::Result<MangoAccountValue> {
|
||||
self.refresh_account_via_rpc(address).await?;
|
||||
self.fetch_mango_account(address)
|
||||
}
|
||||
|
||||
|
@ -76,10 +79,11 @@ impl AccountFetcher {
|
|||
.clone())
|
||||
}
|
||||
|
||||
pub fn refresh_account_via_rpc(&self, address: &Pubkey) -> anyhow::Result<Slot> {
|
||||
pub async fn refresh_account_via_rpc(&self, address: &Pubkey) -> anyhow::Result<Slot> {
|
||||
let response = self
|
||||
.rpc
|
||||
.get_account_with_commitment(address, self.rpc.commitment())
|
||||
.await
|
||||
.with_context(|| format!("refresh account {} via rpc", address))?;
|
||||
let slot = response.context.slot;
|
||||
let account = response
|
||||
|
@ -100,8 +104,8 @@ impl AccountFetcher {
|
|||
}
|
||||
|
||||
/// Return the maximum slot reported for the processing of the signatures
|
||||
pub fn transaction_max_slot(&self, signatures: &[Signature]) -> anyhow::Result<Slot> {
|
||||
let statuses = self.rpc.get_signature_statuses(signatures)?.value;
|
||||
pub async fn transaction_max_slot(&self, signatures: &[Signature]) -> anyhow::Result<Slot> {
|
||||
let statuses = self.rpc.get_signature_statuses(signatures).await?.value;
|
||||
Ok(statuses
|
||||
.iter()
|
||||
.map(|status_opt| status_opt.as_ref().map(|status| status.slot).unwrap_or(0))
|
||||
|
@ -110,7 +114,7 @@ impl AccountFetcher {
|
|||
}
|
||||
|
||||
/// Return success once all addresses have data >= min_slot
|
||||
pub fn refresh_accounts_via_rpc_until_slot(
|
||||
pub async fn refresh_accounts_via_rpc_until_slot(
|
||||
&self,
|
||||
addresses: &[Pubkey],
|
||||
min_slot: Slot,
|
||||
|
@ -126,7 +130,7 @@ impl AccountFetcher {
|
|||
min_slot
|
||||
);
|
||||
}
|
||||
let data_slot = self.refresh_account_via_rpc(address)?;
|
||||
let data_slot = self.refresh_account_via_rpc(address).await?;
|
||||
if data_slot >= min_slot {
|
||||
break;
|
||||
}
|
||||
|
@ -137,15 +141,29 @@ impl AccountFetcher {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl crate::AccountFetcher for AccountFetcher {
|
||||
fn fetch_raw_account(
|
||||
async fn fetch_raw_account(
|
||||
&self,
|
||||
address: &Pubkey,
|
||||
) -> anyhow::Result<solana_sdk::account::AccountSharedData> {
|
||||
self.fetch_raw(&address)
|
||||
self.fetch_raw(address)
|
||||
}
|
||||
|
||||
fn fetch_program_accounts(
|
||||
async fn fetch_raw_account_lookup_table(
|
||||
&self,
|
||||
address: &Pubkey,
|
||||
) -> anyhow::Result<AccountSharedData> {
|
||||
// Fetch data via RPC if missing: the chain data updater doesn't know about all the
|
||||
// lookup talbes we may need.
|
||||
if let Ok(alt) = self.fetch_raw(address) {
|
||||
return Ok(alt);
|
||||
}
|
||||
self.refresh_account_via_rpc(address).await?;
|
||||
self.fetch_raw(address)
|
||||
}
|
||||
|
||||
async fn fetch_program_accounts(
|
||||
&self,
|
||||
program: &Pubkey,
|
||||
discriminator: [u8; 8],
|
||||
|
|
1204
client/src/client.rs
1204
client/src/client.rs
File diff suppressed because it is too large
Load Diff
|
@ -1,23 +1,24 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use anchor_client::{Client, ClientError, Cluster, Program};
|
||||
use anchor_client::ClientError;
|
||||
|
||||
use anchor_lang::__private::bytemuck;
|
||||
|
||||
use mango_v4::state::{
|
||||
MangoAccountValue, MintInfo, PerpMarket, PerpMarketIndex, Serum3Market, Serum3MarketIndex,
|
||||
TokenIndex,
|
||||
Group, MangoAccountValue, MintInfo, PerpMarket, PerpMarketIndex, Serum3Market,
|
||||
Serum3MarketIndex, TokenIndex,
|
||||
};
|
||||
|
||||
use fixed::types::I80F48;
|
||||
use futures::{stream, StreamExt, TryStreamExt};
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::gpa::*;
|
||||
|
||||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||
use solana_sdk::account::Account;
|
||||
use solana_sdk::instruction::AccountMeta;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TokenContext {
|
||||
|
@ -64,6 +65,8 @@ pub struct MangoGroupContext {
|
|||
|
||||
pub perp_markets: HashMap<PerpMarketIndex, PerpMarketContext>,
|
||||
pub perp_market_indexes_by_name: HashMap<String, PerpMarketIndex>,
|
||||
|
||||
pub address_lookup_tables: Vec<Pubkey>,
|
||||
}
|
||||
|
||||
impl MangoGroupContext {
|
||||
|
@ -94,17 +97,11 @@ impl MangoGroupContext {
|
|||
self.perp(perp_market_index).address
|
||||
}
|
||||
|
||||
pub fn new_from_rpc(
|
||||
group: Pubkey,
|
||||
cluster: Cluster,
|
||||
commitment: CommitmentConfig,
|
||||
) -> Result<Self, ClientError> {
|
||||
let program =
|
||||
Client::new_with_options(cluster, std::rc::Rc::new(Keypair::new()), commitment)
|
||||
.program(mango_v4::ID);
|
||||
pub async fn new_from_rpc(rpc: &RpcClientAsync, group: Pubkey) -> anyhow::Result<Self> {
|
||||
let program = mango_v4::ID;
|
||||
|
||||
// tokens
|
||||
let mint_info_tuples = fetch_mint_infos(&program, group)?;
|
||||
let mint_info_tuples = fetch_mint_infos(rpc, program, group).await?;
|
||||
let mut tokens = mint_info_tuples
|
||||
.iter()
|
||||
.map(|(pk, mi)| {
|
||||
|
@ -124,7 +121,7 @@ impl MangoGroupContext {
|
|||
// reading the banks is only needed for the token names and decimals
|
||||
// FUTURE: either store the names on MintInfo as well, or maybe don't store them at all
|
||||
// because they are in metaplex?
|
||||
let bank_tuples = fetch_banks(&program, group)?;
|
||||
let bank_tuples = fetch_banks(rpc, program, group).await?;
|
||||
for (_, bank) in bank_tuples {
|
||||
let token = tokens.get_mut(&bank.token_index).unwrap();
|
||||
token.name = bank.name().into();
|
||||
|
@ -133,11 +130,15 @@ impl MangoGroupContext {
|
|||
assert!(tokens.values().all(|t| t.decimals != u8::MAX));
|
||||
|
||||
// serum3 markets
|
||||
let serum3_market_tuples = fetch_serum3_markets(&program, group)?;
|
||||
let serum3_market_tuples = fetch_serum3_markets(rpc, program, group).await?;
|
||||
let serum3_markets_external = stream::iter(serum3_market_tuples.iter())
|
||||
.then(|(_, s)| fetch_raw_account(rpc, s.serum_market_external))
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
let serum3_markets = serum3_market_tuples
|
||||
.iter()
|
||||
.map(|(pk, s)| {
|
||||
let market_external_account = fetch_raw_account(&program, s.serum_market_external)?;
|
||||
.zip(serum3_markets_external.iter())
|
||||
.map(|((pk, s), market_external_account)| {
|
||||
let market_external: &serum_dex::state::MarketState = bytemuck::from_bytes(
|
||||
&market_external_account.data
|
||||
[5..5 + std::mem::size_of::<serum_dex::state::MarketState>()],
|
||||
|
@ -148,7 +149,7 @@ impl MangoGroupContext {
|
|||
&s.serum_program,
|
||||
)
|
||||
.unwrap();
|
||||
Ok((
|
||||
(
|
||||
s.market_index,
|
||||
Serum3MarketContext {
|
||||
address: *pk,
|
||||
|
@ -163,12 +164,12 @@ impl MangoGroupContext {
|
|||
coin_lot_size: market_external.coin_lot_size,
|
||||
pc_lot_size: market_external.pc_lot_size,
|
||||
},
|
||||
))
|
||||
)
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>, ClientError>>()?;
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
// perp markets
|
||||
let perp_market_tuples = fetch_perp_markets(&program, group)?;
|
||||
let perp_market_tuples = fetch_perp_markets(rpc, program, group).await?;
|
||||
let perp_markets = perp_market_tuples
|
||||
.iter()
|
||||
.map(|(pk, pm)| {
|
||||
|
@ -196,6 +197,14 @@ impl MangoGroupContext {
|
|||
.map(|(i, p)| (p.market.name().to_string(), *i))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let group_data = fetch_anchor_account::<Group>(rpc, &group).await?;
|
||||
let address_lookup_tables = group_data
|
||||
.address_lookup_tables
|
||||
.iter()
|
||||
.filter(|&&k| k != Pubkey::default())
|
||||
.cloned()
|
||||
.collect::<Vec<Pubkey>>();
|
||||
|
||||
Ok(MangoGroupContext {
|
||||
group,
|
||||
tokens,
|
||||
|
@ -204,6 +213,7 @@ impl MangoGroupContext {
|
|||
serum3_market_indexes_by_name,
|
||||
perp_markets,
|
||||
perp_market_indexes_by_name,
|
||||
address_lookup_tables,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -331,9 +341,9 @@ fn from_serum_style_pubkey(d: [u64; 4]) -> Pubkey {
|
|||
Pubkey::new(bytemuck::cast_slice(&d as &[_]))
|
||||
}
|
||||
|
||||
fn fetch_raw_account(program: &Program, address: Pubkey) -> Result<Account, ClientError> {
|
||||
let rpc = program.rpc();
|
||||
rpc.get_account_with_commitment(&address, rpc.commitment())?
|
||||
async fn fetch_raw_account(rpc: &RpcClientAsync, address: Pubkey) -> Result<Account, ClientError> {
|
||||
rpc.get_account_with_commitment(&address, rpc.commitment())
|
||||
.await?
|
||||
.value
|
||||
.ok_or(ClientError::AccountNotFound)
|
||||
}
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
use anchor_client::{ClientError, Program};
|
||||
use anchor_lang::Discriminator;
|
||||
use anchor_lang::{AccountDeserialize, Discriminator};
|
||||
|
||||
use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, MintInfo, PerpMarket, Serum3Market};
|
||||
|
||||
use solana_account_decoder::UiAccountEncoding;
|
||||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||
use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig};
|
||||
use solana_client::rpc_filter::{Memcmp, RpcFilterType};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
pub fn fetch_mango_accounts(
|
||||
program: &Program,
|
||||
pub async fn fetch_mango_accounts(
|
||||
rpc: &RpcClientAsync,
|
||||
program: Pubkey,
|
||||
group: Pubkey,
|
||||
owner: Pubkey,
|
||||
) -> Result<Vec<(Pubkey, MangoAccountValue)>, ClientError> {
|
||||
) -> anyhow::Result<Vec<(Pubkey, MangoAccountValue)>> {
|
||||
let config = RpcProgramAccountsConfig {
|
||||
filters: Some(vec![
|
||||
RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
|
@ -28,47 +29,103 @@ pub fn fetch_mango_accounts(
|
|||
},
|
||||
..RpcProgramAccountsConfig::default()
|
||||
};
|
||||
program
|
||||
.rpc()
|
||||
.get_program_accounts_with_config(&program.id(), config)?
|
||||
rpc.get_program_accounts_with_config(&program, config)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(key, account)| Ok((key, MangoAccountValue::from_bytes(&account.data[8..])?)))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
|
||||
pub fn fetch_banks(program: &Program, group: Pubkey) -> Result<Vec<(Pubkey, Bank)>, ClientError> {
|
||||
program.accounts::<Bank>(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
8,
|
||||
&group.to_bytes(),
|
||||
))])
|
||||
pub async fn fetch_anchor_account<T: AccountDeserialize>(
|
||||
rpc: &RpcClientAsync,
|
||||
address: &Pubkey,
|
||||
) -> anyhow::Result<T> {
|
||||
let account = rpc.get_account(address).await?;
|
||||
Ok(T::try_deserialize(&mut (&account.data as &[u8]))?)
|
||||
}
|
||||
|
||||
pub fn fetch_mint_infos(
|
||||
program: &Program,
|
||||
group: Pubkey,
|
||||
) -> Result<Vec<(Pubkey, MintInfo)>, ClientError> {
|
||||
program.accounts::<MintInfo>(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
8,
|
||||
&group.to_bytes(),
|
||||
))])
|
||||
async fn fetch_anchor_accounts<T: AccountDeserialize + Discriminator>(
|
||||
rpc: &RpcClientAsync,
|
||||
program: Pubkey,
|
||||
filters: Vec<RpcFilterType>,
|
||||
) -> anyhow::Result<Vec<(Pubkey, T)>> {
|
||||
let account_type_filter =
|
||||
RpcFilterType::Memcmp(Memcmp::new_base58_encoded(0, &T::discriminator()));
|
||||
let config = RpcProgramAccountsConfig {
|
||||
filters: Some([vec![account_type_filter], filters].concat()),
|
||||
account_config: RpcAccountInfoConfig {
|
||||
encoding: Some(UiAccountEncoding::Base64),
|
||||
..RpcAccountInfoConfig::default()
|
||||
},
|
||||
..RpcProgramAccountsConfig::default()
|
||||
};
|
||||
rpc.get_program_accounts_with_config(&program, config)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(key, account)| Ok((key, T::try_deserialize(&mut (&account.data as &[u8]))?)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn fetch_serum3_markets(
|
||||
program: &Program,
|
||||
pub async fn fetch_banks(
|
||||
rpc: &RpcClientAsync,
|
||||
program: Pubkey,
|
||||
group: Pubkey,
|
||||
) -> Result<Vec<(Pubkey, Serum3Market)>, ClientError> {
|
||||
program.accounts::<Serum3Market>(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
8,
|
||||
&group.to_bytes(),
|
||||
))])
|
||||
) -> anyhow::Result<Vec<(Pubkey, Bank)>> {
|
||||
fetch_anchor_accounts::<Bank>(
|
||||
rpc,
|
||||
program,
|
||||
vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
8,
|
||||
&group.to_bytes(),
|
||||
))],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn fetch_perp_markets(
|
||||
program: &Program,
|
||||
pub async fn fetch_mint_infos(
|
||||
rpc: &RpcClientAsync,
|
||||
program: Pubkey,
|
||||
group: Pubkey,
|
||||
) -> Result<Vec<(Pubkey, PerpMarket)>, ClientError> {
|
||||
program.accounts::<PerpMarket>(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
8,
|
||||
&group.to_bytes(),
|
||||
))])
|
||||
) -> anyhow::Result<Vec<(Pubkey, MintInfo)>> {
|
||||
fetch_anchor_accounts::<MintInfo>(
|
||||
rpc,
|
||||
program,
|
||||
vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
8,
|
||||
&group.to_bytes(),
|
||||
))],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn fetch_serum3_markets(
|
||||
rpc: &RpcClientAsync,
|
||||
program: Pubkey,
|
||||
group: Pubkey,
|
||||
) -> anyhow::Result<Vec<(Pubkey, Serum3Market)>> {
|
||||
fetch_anchor_accounts::<Serum3Market>(
|
||||
rpc,
|
||||
program,
|
||||
vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
8,
|
||||
&group.to_bytes(),
|
||||
))],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn fetch_perp_markets(
|
||||
rpc: &RpcClientAsync,
|
||||
program: Pubkey,
|
||||
group: Pubkey,
|
||||
) -> anyhow::Result<Vec<(Pubkey, PerpMarket)>> {
|
||||
fetch_anchor_accounts::<PerpMarket>(
|
||||
rpc,
|
||||
program,
|
||||
vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
8,
|
||||
&group.to_bytes(),
|
||||
))],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use crate::{AccountFetcher, MangoGroupContext};
|
||||
use anyhow::Context;
|
||||
use futures::{stream, StreamExt, TryStreamExt};
|
||||
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
|
||||
use mango_v4::health::{FixedOrderAccountRetriever, HealthCache};
|
||||
use mango_v4::state::MangoAccountValue;
|
||||
|
||||
pub fn new(
|
||||
pub async fn new(
|
||||
context: &MangoGroupContext,
|
||||
account_fetcher: &impl AccountFetcher,
|
||||
account: &MangoAccountValue,
|
||||
|
@ -13,18 +14,18 @@ pub fn new(
|
|||
let active_perp_len = account.active_perp_positions().count();
|
||||
|
||||
let metas = context.derive_health_check_remaining_account_metas(account, vec![], false)?;
|
||||
let accounts = metas
|
||||
.iter()
|
||||
.map(|meta| {
|
||||
let accounts: anyhow::Result<Vec<KeyedAccountSharedData>> = stream::iter(metas.iter())
|
||||
.then(|meta| async {
|
||||
Ok(KeyedAccountSharedData::new(
|
||||
meta.pubkey,
|
||||
account_fetcher.fetch_raw_account(&meta.pubkey)?,
|
||||
account_fetcher.fetch_raw_account(&meta.pubkey).await?,
|
||||
))
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
.try_collect()
|
||||
.await;
|
||||
|
||||
let retriever = FixedOrderAccountRetriever {
|
||||
ais: accounts,
|
||||
ais: accounts?,
|
||||
n_banks: active_token_len,
|
||||
n_perps: active_perp_len,
|
||||
begin_perp: active_token_len * 2,
|
||||
|
|
|
@ -5,20 +5,21 @@ use serde::{Deserialize, Serialize};
|
|||
pub struct QueryResult {
|
||||
pub data: Vec<QueryRoute>,
|
||||
pub time_taken: f64,
|
||||
pub context_slot: String,
|
||||
pub context_slot: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryRoute {
|
||||
pub in_amount: u64,
|
||||
pub out_amount: u64,
|
||||
pub amount: u64,
|
||||
pub other_amount_threshold: u64,
|
||||
pub out_amount_with_slippage: Option<u64>,
|
||||
pub swap_mode: String,
|
||||
pub in_amount: String,
|
||||
pub out_amount: String,
|
||||
pub price_impact_pct: f64,
|
||||
pub market_infos: Vec<QueryMarketInfo>,
|
||||
pub amount: String,
|
||||
pub slippage_bps: u64,
|
||||
pub other_amount_threshold: String,
|
||||
pub swap_mode: String,
|
||||
pub fees: Option<QueryRouteFees>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
|
@ -28,22 +29,35 @@ pub struct QueryMarketInfo {
|
|||
pub label: String,
|
||||
pub input_mint: String,
|
||||
pub output_mint: String,
|
||||
pub in_amount: u64,
|
||||
pub out_amount: u64,
|
||||
pub not_enough_liquidity: bool,
|
||||
pub in_amount: String,
|
||||
pub out_amount: String,
|
||||
pub min_in_amount: Option<String>,
|
||||
pub min_out_amount: Option<String>,
|
||||
pub price_impact_pct: Option<f64>,
|
||||
pub lp_fee: QueryFee,
|
||||
pub platform_fee: QueryFee,
|
||||
pub not_enough_liquidity: bool,
|
||||
pub price_impact_pct: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryFee {
|
||||
pub amount: u64,
|
||||
pub amount: String,
|
||||
pub mint: String,
|
||||
pub pct: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryRouteFees {
|
||||
pub signature_fee: f64,
|
||||
pub open_orders_deposits: Vec<f64>,
|
||||
pub ata_deposits: Vec<f64>,
|
||||
pub total_fee_and_deposits: f64,
|
||||
#[serde(rename = "minimalSOLForTransaction")]
|
||||
pub minimal_sol_for_transaction: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwapRequest {
|
||||
|
|
|
@ -15,7 +15,7 @@ pub enum Direction {
|
|||
|
||||
/// Returns up to `count` accounts with highest abs pnl (by `direction`) in descending order.
|
||||
/// Note: keep in sync with perp.ts:getSettlePnlCandidates
|
||||
pub fn fetch_top(
|
||||
pub async fn fetch_top(
|
||||
context: &crate::context::MangoGroupContext,
|
||||
account_fetcher: &impl AccountFetcher,
|
||||
perp_market_index: PerpMarketIndex,
|
||||
|
@ -25,20 +25,23 @@ pub fn fetch_top(
|
|||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let now_ts: u64 = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)?
|
||||
.as_millis()
|
||||
.as_secs()
|
||||
.try_into()?;
|
||||
|
||||
let perp = context.perp(perp_market_index);
|
||||
let perp_market =
|
||||
account_fetcher_fetch_anchor_account::<PerpMarket>(account_fetcher, &perp.address)?;
|
||||
let oracle_acc = account_fetcher.fetch_raw_account(&perp_market.oracle)?;
|
||||
account_fetcher_fetch_anchor_account::<PerpMarket>(account_fetcher, &perp.address).await?;
|
||||
let oracle_acc = account_fetcher
|
||||
.fetch_raw_account(&perp_market.oracle)
|
||||
.await?;
|
||||
let oracle_price = perp_market.oracle_price(
|
||||
&KeyedAccountSharedData::new(perp_market.oracle, oracle_acc),
|
||||
None,
|
||||
)?;
|
||||
|
||||
let accounts =
|
||||
account_fetcher.fetch_program_accounts(&mango_v4::id(), MangoAccount::discriminator())?;
|
||||
let accounts = account_fetcher
|
||||
.fetch_program_accounts(&mango_v4::id(), MangoAccount::discriminator())
|
||||
.await?;
|
||||
|
||||
let mut accounts_pnl = accounts
|
||||
.iter()
|
||||
|
@ -54,6 +57,7 @@ pub fn fetch_top(
|
|||
return None;
|
||||
}
|
||||
let mut perp_pos = perp_pos.unwrap().clone();
|
||||
perp_pos.settle_funding(&perp_market);
|
||||
perp_pos.update_settle_limit(&perp_market, now_ts);
|
||||
let pnl = perp_pos.pnl_for_price(&perp_market, oracle_price).unwrap();
|
||||
let limited_pnl = perp_pos.apply_pnl_settle_limit(pnl, &perp_market);
|
||||
|
@ -88,8 +92,9 @@ pub fn fetch_top(
|
|||
} else {
|
||||
I80F48::ZERO
|
||||
};
|
||||
let perp_settle_health =
|
||||
crate::health_cache::new(context, account_fetcher, &acc)?.perp_settle_health();
|
||||
let perp_settle_health = crate::health_cache::new(context, account_fetcher, &acc)
|
||||
.await?
|
||||
.perp_settle_health();
|
||||
let settleable_pnl = if perp_settle_health > 0 && !acc.being_liquidated() {
|
||||
(*pnl).max(-perp_settle_health)
|
||||
} else {
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
use std::{sync::Arc, time::Duration, time::Instant};
|
||||
use std::{collections::HashSet, sync::Arc, time::Duration, time::Instant};
|
||||
|
||||
use crate::MangoClient;
|
||||
use itertools::Itertools;
|
||||
|
||||
use anchor_lang::{__private::bytemuck::cast_ref, solana_program};
|
||||
use client::prettify_client_error;
|
||||
use futures::Future;
|
||||
use mango_v4::state::{EventQueue, EventType, FillEvent, OutEvent, PerpMarket, TokenIndex};
|
||||
use solana_sdk::{
|
||||
|
@ -93,79 +92,63 @@ pub async fn loop_update_index_and_rate(
|
|||
|
||||
let token_indices_clone = token_indices.clone();
|
||||
|
||||
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
|
||||
let token_names = token_indices_clone
|
||||
let token_names = token_indices_clone
|
||||
.iter()
|
||||
.map(|token_index| client.context.token(*token_index).name.to_owned())
|
||||
.join(",");
|
||||
|
||||
let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_price(1)];
|
||||
for token_index in token_indices_clone.iter() {
|
||||
let token = client.context.token(*token_index);
|
||||
let banks_for_a_token = token.mint_info.banks();
|
||||
let oracle = token.mint_info.oracle;
|
||||
|
||||
let mut ix = Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::TokenUpdateIndexAndRate {
|
||||
group: token.mint_info.group,
|
||||
mint_info: token.mint_info_address,
|
||||
oracle,
|
||||
instructions: solana_program::sysvar::instructions::id(),
|
||||
},
|
||||
None,
|
||||
),
|
||||
data: anchor_lang::InstructionData::data(
|
||||
&mango_v4::instruction::TokenUpdateIndexAndRate {},
|
||||
),
|
||||
};
|
||||
let mut banks = banks_for_a_token
|
||||
.iter()
|
||||
.map(|token_index| client.context.token(*token_index).name.to_owned())
|
||||
.join(",");
|
||||
.map(|bank_pubkey| AccountMeta {
|
||||
pubkey: *bank_pubkey,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
ix.accounts.append(&mut banks);
|
||||
instructions.push(ix);
|
||||
}
|
||||
let pre = Instant::now();
|
||||
let sig_result = client
|
||||
.send_and_confirm_permissionless_tx(instructions)
|
||||
.await;
|
||||
|
||||
let program = client.program();
|
||||
let mut req = program.request();
|
||||
req = req.instruction(ComputeBudgetInstruction::set_compute_unit_price(1));
|
||||
for token_index in token_indices_clone.iter() {
|
||||
let token = client.context.token(*token_index);
|
||||
let banks_for_a_token = token.mint_info.banks();
|
||||
let oracle = token.mint_info.oracle;
|
||||
|
||||
let mut ix = Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::TokenUpdateIndexAndRate {
|
||||
group: token.mint_info.group,
|
||||
mint_info: token.mint_info_address,
|
||||
oracle,
|
||||
instructions: solana_program::sysvar::instructions::id(),
|
||||
},
|
||||
None,
|
||||
),
|
||||
data: anchor_lang::InstructionData::data(
|
||||
&mango_v4::instruction::TokenUpdateIndexAndRate {},
|
||||
),
|
||||
};
|
||||
let mut banks = banks_for_a_token
|
||||
.iter()
|
||||
.map(|bank_pubkey| AccountMeta {
|
||||
pubkey: *bank_pubkey,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
ix.accounts.append(&mut banks);
|
||||
req = req.instruction(ix);
|
||||
}
|
||||
let pre = Instant::now();
|
||||
let sig_result = req.send().map_err(prettify_client_error);
|
||||
|
||||
if let Err(e) = sig_result {
|
||||
log::info!(
|
||||
"metricName=UpdateTokensV4Failure tokens={} durationMs={} error={}",
|
||||
token_names,
|
||||
pre.elapsed().as_millis(),
|
||||
e
|
||||
);
|
||||
log::error!("{:?}", e)
|
||||
} else {
|
||||
log::info!(
|
||||
"metricName=UpdateTokensV4Success tokens={} durationMs={}",
|
||||
token_names,
|
||||
pre.elapsed().as_millis(),
|
||||
);
|
||||
log::info!("{:?}", sig_result);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(inner_res) => {
|
||||
if inner_res.is_err() {
|
||||
log::error!("{}", inner_res.unwrap_err());
|
||||
}
|
||||
}
|
||||
Err(join_error) => {
|
||||
log::error!("{}", join_error);
|
||||
}
|
||||
if let Err(e) = sig_result {
|
||||
log::info!(
|
||||
"metricName=UpdateTokensV4Failure tokens={} durationMs={} error={}",
|
||||
token_names,
|
||||
pre.elapsed().as_millis(),
|
||||
e
|
||||
);
|
||||
log::error!("{:?}", e)
|
||||
} else {
|
||||
log::info!(
|
||||
"metricName=UpdateTokensV4Success tokens={} durationMs={}",
|
||||
token_names,
|
||||
pre.elapsed().as_millis(),
|
||||
);
|
||||
log::info!("{:?}", sig_result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -181,15 +164,17 @@ pub async fn loop_consume_events(
|
|||
interval.tick().await;
|
||||
|
||||
let client = mango_client.clone();
|
||||
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
|
||||
let mut event_queue: EventQueue =
|
||||
client.program().account(perp_market.event_queue).unwrap();
|
||||
|
||||
let mut ams_ = vec![];
|
||||
let find_accounts = || async {
|
||||
let mut num_of_events = 0;
|
||||
let mut event_queue: EventQueue = client
|
||||
.client
|
||||
.rpc_anchor_account(&perp_market.event_queue)
|
||||
.await?;
|
||||
|
||||
// TODO: future, choose better constant of how many max events to pack
|
||||
// TODO: future, choose better constant of how many max mango accounts to pack
|
||||
let mut set = HashSet::new();
|
||||
for _ in 0..10 {
|
||||
let event = match event_queue.peek_front() {
|
||||
None => break,
|
||||
|
@ -198,100 +183,87 @@ pub async fn loop_consume_events(
|
|||
match EventType::try_from(event.event_type)? {
|
||||
EventType::Fill => {
|
||||
let fill: &FillEvent = cast_ref(event);
|
||||
if fill.maker == fill.taker {
|
||||
ams_.push(AccountMeta {
|
||||
pubkey: fill.maker,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
});
|
||||
} else {
|
||||
ams_.push(AccountMeta {
|
||||
pubkey: fill.maker,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
});
|
||||
ams_.push(AccountMeta {
|
||||
pubkey: fill.taker,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
});
|
||||
}
|
||||
set.insert(fill.maker);
|
||||
set.insert(fill.taker);
|
||||
}
|
||||
EventType::Out => {
|
||||
let out: &OutEvent = cast_ref(event);
|
||||
ams_.push(AccountMeta {
|
||||
pubkey: out.owner,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
});
|
||||
set.insert(out.owner);
|
||||
}
|
||||
EventType::Liquidate => {}
|
||||
}
|
||||
event_queue.pop_front()?;
|
||||
num_of_events+=1;
|
||||
num_of_events += 1;
|
||||
}
|
||||
|
||||
if num_of_events == 0 {
|
||||
return Ok(());
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let pre = Instant::now();
|
||||
let sig_result = client
|
||||
.program()
|
||||
.request()
|
||||
.instruction(Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: {
|
||||
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::PerpConsumeEvents {
|
||||
group: perp_market.group,
|
||||
perp_market: pk,
|
||||
event_queue: perp_market.event_queue,
|
||||
},
|
||||
None,
|
||||
);
|
||||
ams.append(&mut ams_);
|
||||
ams
|
||||
},
|
||||
data: anchor_lang::InstructionData::data(
|
||||
&mango_v4::instruction::PerpConsumeEvents { limit: 10 },
|
||||
),
|
||||
})
|
||||
.send()
|
||||
.map_err(prettify_client_error);
|
||||
Ok(Some((set, num_of_events)))
|
||||
};
|
||||
|
||||
if let Err(e) = sig_result {
|
||||
log::info!(
|
||||
"metricName=ConsumeEventsV4Failure market={} durationMs={} consumed={} error={}",
|
||||
perp_market.name(),
|
||||
pre.elapsed().as_millis(),
|
||||
num_of_events,
|
||||
e.to_string()
|
||||
);
|
||||
log::error!("{:?}", e)
|
||||
} else {
|
||||
log::info!(
|
||||
"metricName=ConsumeEventsV4Success market={} durationMs={} consumed={}",
|
||||
perp_market.name(),
|
||||
pre.elapsed().as_millis(),
|
||||
num_of_events,
|
||||
);
|
||||
log::info!("{:?}", sig_result);
|
||||
let event_info: anyhow::Result<Option<(HashSet<Pubkey>, u32)>> = find_accounts().await;
|
||||
|
||||
let (event_accounts, num_of_events) = match event_info {
|
||||
Ok(Some(x)) => x,
|
||||
Ok(None) => continue,
|
||||
Err(err) => {
|
||||
log::error!("preparing consume_events ams: {err:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(inner_res) => {
|
||||
if inner_res.is_err() {
|
||||
log::error!("{}", inner_res.unwrap_err());
|
||||
let mut event_ams = event_accounts
|
||||
.iter()
|
||||
.map(|key| -> AccountMeta {
|
||||
AccountMeta {
|
||||
pubkey: *key,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
}
|
||||
}
|
||||
Err(join_error) => {
|
||||
log::error!("{}", join_error);
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let pre = Instant::now();
|
||||
let ix = Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: {
|
||||
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::PerpConsumeEvents {
|
||||
group: perp_market.group,
|
||||
perp_market: pk,
|
||||
event_queue: perp_market.event_queue,
|
||||
},
|
||||
None,
|
||||
);
|
||||
ams.append(&mut event_ams);
|
||||
ams
|
||||
},
|
||||
data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpConsumeEvents {
|
||||
limit: 10,
|
||||
}),
|
||||
};
|
||||
|
||||
let sig_result = client.send_and_confirm_permissionless_tx(vec![ix]).await;
|
||||
|
||||
if let Err(e) = sig_result {
|
||||
log::info!(
|
||||
"metricName=ConsumeEventsV4Failure market={} durationMs={} consumed={} error={}",
|
||||
perp_market.name(),
|
||||
pre.elapsed().as_millis(),
|
||||
num_of_events,
|
||||
e.to_string()
|
||||
);
|
||||
log::error!("{:?}", e)
|
||||
} else {
|
||||
log::info!(
|
||||
"metricName=ConsumeEventsV4Success market={} durationMs={} consumed={}",
|
||||
perp_market.name(),
|
||||
pre.elapsed().as_millis(),
|
||||
num_of_events,
|
||||
);
|
||||
log::info!("{:?}", sig_result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -307,59 +279,39 @@ pub async fn loop_update_funding(
|
|||
interval.tick().await;
|
||||
|
||||
let client = mango_client.clone();
|
||||
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
|
||||
let pre = Instant::now();
|
||||
let sig_result = client
|
||||
.program()
|
||||
.request()
|
||||
.instruction(Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::PerpUpdateFunding {
|
||||
group: perp_market.group,
|
||||
perp_market: pk,
|
||||
bids: perp_market.bids,
|
||||
asks: perp_market.asks,
|
||||
oracle: perp_market.oracle,
|
||||
},
|
||||
None,
|
||||
),
|
||||
data: anchor_lang::InstructionData::data(
|
||||
&mango_v4::instruction::PerpUpdateFunding {},
|
||||
),
|
||||
})
|
||||
.send()
|
||||
.map_err(prettify_client_error);
|
||||
if let Err(e) = sig_result {
|
||||
log::error!(
|
||||
"metricName=UpdateFundingV4Error market={} durationMs={} error={}",
|
||||
perp_market.name(),
|
||||
pre.elapsed().as_millis(),
|
||||
e.to_string()
|
||||
);
|
||||
log::error!("{:?}", e)
|
||||
} else {
|
||||
log::info!(
|
||||
"metricName=UpdateFundingV4Success market={} durationMs={}",
|
||||
perp_market.name(),
|
||||
pre.elapsed().as_millis(),
|
||||
);
|
||||
log::info!("{:?}", sig_result);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
let pre = Instant::now();
|
||||
let ix = Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::PerpUpdateFunding {
|
||||
group: perp_market.group,
|
||||
perp_market: pk,
|
||||
bids: perp_market.bids,
|
||||
asks: perp_market.asks,
|
||||
oracle: perp_market.oracle,
|
||||
},
|
||||
None,
|
||||
),
|
||||
data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpUpdateFunding {}),
|
||||
};
|
||||
let sig_result = client.send_and_confirm_permissionless_tx(vec![ix]).await;
|
||||
|
||||
match res {
|
||||
Ok(inner_res) => {
|
||||
if inner_res.is_err() {
|
||||
log::error!("{}", inner_res.unwrap_err());
|
||||
}
|
||||
}
|
||||
Err(join_error) => {
|
||||
log::error!("{}", join_error);
|
||||
}
|
||||
if let Err(e) = sig_result {
|
||||
log::error!(
|
||||
"metricName=UpdateFundingV4Error market={} durationMs={} error={}",
|
||||
perp_market.name(),
|
||||
pre.elapsed().as_millis(),
|
||||
e.to_string()
|
||||
);
|
||||
log::error!("{:?}", e)
|
||||
} else {
|
||||
log::info!(
|
||||
"metricName=UpdateFundingV4Success market={} durationMs={}",
|
||||
perp_market.name(),
|
||||
pre.elapsed().as_millis(),
|
||||
);
|
||||
log::info!("{:?}", sig_result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,7 +62,9 @@ enum Command {
|
|||
Crank {},
|
||||
Taker {},
|
||||
}
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
env_logger::init_from_env(
|
||||
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
|
||||
);
|
||||
|
@ -87,21 +89,19 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
Command::Taker { .. } => CommitmentConfig::confirmed(),
|
||||
};
|
||||
|
||||
let mango_client = Arc::new(MangoClient::new_for_existing_account(
|
||||
Client::new(
|
||||
cluster,
|
||||
commitment,
|
||||
&owner,
|
||||
Some(Duration::from_secs(cli.timeout)),
|
||||
),
|
||||
cli.mango_account,
|
||||
owner,
|
||||
)?);
|
||||
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mango_client = Arc::new(
|
||||
MangoClient::new_for_existing_account(
|
||||
Client::new(
|
||||
cluster,
|
||||
commitment,
|
||||
&owner,
|
||||
Some(Duration::from_secs(cli.timeout)),
|
||||
),
|
||||
cli.mango_account,
|
||||
owner,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
let debugging_handle = async {
|
||||
let mut interval = time::interval(time::Duration::from_secs(5));
|
||||
|
@ -120,17 +120,18 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
match cli.command {
|
||||
Command::Crank { .. } => {
|
||||
let client = mango_client.clone();
|
||||
rt.block_on(crank::runner(
|
||||
crank::runner(
|
||||
client,
|
||||
debugging_handle,
|
||||
cli.interval_update_banks,
|
||||
cli.interval_consume_events,
|
||||
cli.interval_update_funding,
|
||||
))
|
||||
)
|
||||
.await
|
||||
}
|
||||
Command::Taker { .. } => {
|
||||
let client = mango_client.clone();
|
||||
rt.block_on(taker::runner(client, debugging_handle))
|
||||
taker::runner(client, debugging_handle).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,8 @@ pub async fn runner(
|
|||
mango_client: Arc<MangoClient>,
|
||||
_debugging_handle: impl Future,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
ensure_deposit(&mango_client)?;
|
||||
|
||||
ensure_oo(&mango_client)?;
|
||||
ensure_deposit(&mango_client).await?;
|
||||
ensure_oo(&mango_client).await?;
|
||||
|
||||
let mut price_arcs = HashMap::new();
|
||||
for market_name in mango_client.context.serum3_market_indexes_by_name.keys() {
|
||||
|
@ -30,6 +29,7 @@ pub async fn runner(
|
|||
.first()
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
price_arcs.insert(
|
||||
market_name.to_owned(),
|
||||
|
@ -73,23 +73,25 @@ pub async fn runner(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_oo(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
|
||||
let account = mango_client.mango_account()?;
|
||||
async fn ensure_oo(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
|
||||
let account = mango_client.mango_account().await?;
|
||||
|
||||
for (market_index, serum3_market) in mango_client.context.serum3_markets.iter() {
|
||||
if account.serum3_orders(*market_index).is_err() {
|
||||
mango_client.serum3_create_open_orders(serum3_market.market.name())?;
|
||||
mango_client
|
||||
.serum3_create_open_orders(serum3_market.market.name())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_deposit(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
|
||||
let mango_account = mango_client.mango_account()?;
|
||||
async fn ensure_deposit(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
|
||||
let mango_account = mango_client.mango_account().await?;
|
||||
|
||||
for &token_index in mango_client.context.tokens.keys() {
|
||||
let bank = mango_client.first_bank(token_index)?;
|
||||
let bank = mango_client.first_bank(token_index).await?;
|
||||
let desired_balance = I80F48::from_num(10_000 * 10u64.pow(bank.mint_decimals as u32));
|
||||
|
||||
let token_account_opt = mango_account.token_position(token_index).ok();
|
||||
|
@ -114,7 +116,9 @@ fn ensure_deposit(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error>
|
|||
}
|
||||
|
||||
log::info!("Depositing {} {}", deposit_native, bank.name());
|
||||
mango_client.token_deposit(bank.mint, desired_balance.to_num())?;
|
||||
mango_client
|
||||
.token_deposit(bank.mint, desired_balance.to_num())
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -126,33 +130,15 @@ pub async fn loop_blocking_price_update(
|
|||
price: Arc<RwLock<I80F48>>,
|
||||
) {
|
||||
let mut interval = time::interval(Duration::from_secs(1));
|
||||
let token_name = market_name.split('/').collect::<Vec<&str>>()[0];
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
let client1 = mango_client.clone();
|
||||
let market_name1 = market_name.clone();
|
||||
let price = price.clone();
|
||||
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
|
||||
let token_name = market_name1.split('/').collect::<Vec<&str>>()[0];
|
||||
let fresh_price = client1.get_oracle_price(token_name).unwrap();
|
||||
log::info!("{} Updated price is {:?}", token_name, fresh_price.price);
|
||||
if let Ok(mut price) = price.write() {
|
||||
*price = I80F48::from_num(fresh_price.price)
|
||||
/ I80F48::from_num(10u64.pow(-fresh_price.expo as u32));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(inner_res) => {
|
||||
if inner_res.is_err() {
|
||||
log::error!("{}", inner_res.unwrap_err());
|
||||
}
|
||||
}
|
||||
Err(join_error) => {
|
||||
log::error!("{}", join_error);
|
||||
}
|
||||
let fresh_price = mango_client.get_oracle_price(token_name).await.unwrap();
|
||||
log::info!("{} Updated price is {:?}", token_name, fresh_price.price);
|
||||
if let Ok(mut price) = price.write() {
|
||||
*price = I80F48::from_num(fresh_price.price)
|
||||
/ I80F48::from_num(10u64.pow(-fresh_price.expo as u32));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -165,7 +151,10 @@ pub async fn loop_blocking_orders(
|
|||
let mut interval = time::interval(Duration::from_secs(5));
|
||||
|
||||
// Cancel existing orders
|
||||
let orders: Vec<u128> = mango_client.serum3_cancel_all_orders(&market_name).unwrap();
|
||||
let orders: Vec<u128> = mango_client
|
||||
.serum3_cancel_all_orders(&market_name)
|
||||
.await
|
||||
.unwrap();
|
||||
log::info!("Cancelled orders - {:?} for {}", orders, market_name);
|
||||
|
||||
loop {
|
||||
|
@ -175,8 +164,8 @@ pub async fn loop_blocking_orders(
|
|||
let market_name = market_name.clone();
|
||||
let price = price.clone();
|
||||
|
||||
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
|
||||
client.serum3_settle_funds(&market_name)?;
|
||||
let res = (|| async move {
|
||||
client.serum3_settle_funds(&market_name).await?;
|
||||
|
||||
let fresh_price = match price.read() {
|
||||
Ok(price) => *price,
|
||||
|
@ -188,16 +177,18 @@ pub async fn loop_blocking_orders(
|
|||
let fresh_price = fresh_price.to_num::<f64>();
|
||||
|
||||
let bid_price = fresh_price + fresh_price * 0.1;
|
||||
let res = client.serum3_place_order(
|
||||
&market_name,
|
||||
Serum3Side::Bid,
|
||||
bid_price,
|
||||
0.0001,
|
||||
Serum3SelfTradeBehavior::DecrementTake,
|
||||
Serum3OrderType::ImmediateOrCancel,
|
||||
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
|
||||
10,
|
||||
);
|
||||
let res = client
|
||||
.serum3_place_order(
|
||||
&market_name,
|
||||
Serum3Side::Bid,
|
||||
bid_price,
|
||||
0.0001,
|
||||
Serum3SelfTradeBehavior::DecrementTake,
|
||||
Serum3OrderType::ImmediateOrCancel,
|
||||
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
|
||||
10,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
log::error!("Error while placing taker bid {:#?}", e)
|
||||
} else {
|
||||
|
@ -205,16 +196,18 @@ pub async fn loop_blocking_orders(
|
|||
}
|
||||
|
||||
let ask_price = fresh_price - fresh_price * 0.1;
|
||||
let res = client.serum3_place_order(
|
||||
&market_name,
|
||||
Serum3Side::Ask,
|
||||
ask_price,
|
||||
0.0001,
|
||||
Serum3SelfTradeBehavior::DecrementTake,
|
||||
Serum3OrderType::ImmediateOrCancel,
|
||||
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
|
||||
10,
|
||||
);
|
||||
let res = client
|
||||
.serum3_place_order(
|
||||
&market_name,
|
||||
Serum3Side::Ask,
|
||||
ask_price,
|
||||
0.0001,
|
||||
Serum3SelfTradeBehavior::DecrementTake,
|
||||
Serum3OrderType::ImmediateOrCancel,
|
||||
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
|
||||
10,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
log::error!("Error while placing taker ask {:#?}", e)
|
||||
} else {
|
||||
|
@ -222,18 +215,11 @@ pub async fn loop_blocking_orders(
|
|||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})()
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(inner_res) => {
|
||||
if inner_res.is_err() {
|
||||
log::error!("{}", inner_res.unwrap_err());
|
||||
}
|
||||
}
|
||||
Err(join_error) => {
|
||||
log::error!("{}", join_error);
|
||||
}
|
||||
if let Err(err) = res {
|
||||
log::error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use client::{chain_data, health_cache, AccountFetcher, MangoClient, MangoClientError};
|
||||
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
|
||||
use mango_v4::health::{HealthCache, HealthType};
|
||||
use mango_v4::state::{
|
||||
Bank, MangoAccountValue, PerpMarketIndex, Serum3Orders, Side, TokenIndex, QUOTE_TOKEN_INDEX,
|
||||
Bank, MangoAccountValue, PerpMarketIndex, Side, TokenIndex, QUOTE_TOKEN_INDEX,
|
||||
};
|
||||
use solana_sdk::signature::Signature;
|
||||
|
||||
use itertools::Itertools;
|
||||
use futures::{stream, StreamExt, TryStreamExt};
|
||||
use rand::seq::SliceRandom;
|
||||
use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
||||
|
||||
|
@ -17,7 +18,7 @@ pub struct Config {
|
|||
pub refresh_timeout: Duration,
|
||||
}
|
||||
|
||||
pub fn jupiter_market_can_buy(
|
||||
pub async fn jupiter_market_can_buy(
|
||||
mango_client: &MangoClient,
|
||||
token: TokenIndex,
|
||||
quote_token: TokenIndex,
|
||||
|
@ -41,10 +42,11 @@ pub fn jupiter_market_can_buy(
|
|||
slippage,
|
||||
client::JupiterSwapMode::ExactIn,
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub fn jupiter_market_can_sell(
|
||||
pub async fn jupiter_market_can_sell(
|
||||
mango_client: &MangoClient,
|
||||
token: TokenIndex,
|
||||
quote_token: TokenIndex,
|
||||
|
@ -68,6 +70,7 @@ pub fn jupiter_market_can_sell(
|
|||
slippage,
|
||||
client::JupiterSwapMode::ExactOut,
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
|
@ -79,40 +82,50 @@ struct LiquidateHelper<'a> {
|
|||
health_cache: &'a HealthCache,
|
||||
maint_health: I80F48,
|
||||
liqor_min_health_ratio: I80F48,
|
||||
allowed_asset_tokens: HashSet<Pubkey>,
|
||||
allowed_liab_tokens: HashSet<Pubkey>,
|
||||
}
|
||||
|
||||
impl<'a> LiquidateHelper<'a> {
|
||||
fn serum3_close_orders(&self) -> anyhow::Result<Option<Signature>> {
|
||||
async fn serum3_close_orders(&self) -> anyhow::Result<Option<Signature>> {
|
||||
// look for any open serum orders or settleable balances
|
||||
let serum_force_cancels = self
|
||||
.liqee
|
||||
.active_serum3_orders()
|
||||
.map(|orders| {
|
||||
let serum_oos: anyhow::Result<Vec<_>> = stream::iter(self.liqee.active_serum3_orders())
|
||||
.then(|orders| async {
|
||||
let open_orders_account = self
|
||||
.account_fetcher
|
||||
.fetch_raw_account(&orders.open_orders)?;
|
||||
.fetch_raw_account(&orders.open_orders)
|
||||
.await?;
|
||||
let open_orders = mango_v4::serum3_cpi::load_open_orders(&open_orders_account)?;
|
||||
Ok((*orders, *open_orders))
|
||||
})
|
||||
.try_collect()
|
||||
.await;
|
||||
let serum_force_cancels = serum_oos?
|
||||
.into_iter()
|
||||
.filter_map(|(orders, open_orders)| {
|
||||
let can_force_cancel = open_orders.native_coin_total > 0
|
||||
|| open_orders.native_pc_total > 0
|
||||
|| open_orders.referrer_rebates_accrued > 0;
|
||||
if can_force_cancel {
|
||||
Ok(Some(*orders))
|
||||
Some(orders)
|
||||
} else {
|
||||
Ok(None)
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter_map_ok(|v| v)
|
||||
.collect::<anyhow::Result<Vec<Serum3Orders>>>()?;
|
||||
.collect::<Vec<_>>();
|
||||
if serum_force_cancels.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
// Cancel all orders on a random serum market
|
||||
let serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap();
|
||||
let sig = self.client.serum3_liq_force_cancel_orders(
|
||||
(self.pubkey, &self.liqee),
|
||||
serum_orders.market_index,
|
||||
&serum_orders.open_orders,
|
||||
)?;
|
||||
let sig = self
|
||||
.client
|
||||
.serum3_liq_force_cancel_orders(
|
||||
(self.pubkey, &self.liqee),
|
||||
serum_orders.market_index,
|
||||
&serum_orders.open_orders,
|
||||
)
|
||||
.await?;
|
||||
log::info!(
|
||||
"Force cancelled serum orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||
self.pubkey,
|
||||
|
@ -123,7 +136,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
Ok(Some(sig))
|
||||
}
|
||||
|
||||
fn perp_close_orders(&self) -> anyhow::Result<Option<Signature>> {
|
||||
async fn perp_close_orders(&self) -> anyhow::Result<Option<Signature>> {
|
||||
let perp_force_cancels = self
|
||||
.liqee
|
||||
.active_perp_positions()
|
||||
|
@ -137,7 +150,8 @@ impl<'a> LiquidateHelper<'a> {
|
|||
let perp_market_index = *perp_force_cancels.choose(&mut rand::thread_rng()).unwrap();
|
||||
let sig = self
|
||||
.client
|
||||
.perp_liq_force_cancel_orders((self.pubkey, &self.liqee), perp_market_index)?;
|
||||
.perp_liq_force_cancel_orders((self.pubkey, &self.liqee), perp_market_index)
|
||||
.await?;
|
||||
log::info!(
|
||||
"Force cancelled perp orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||
self.pubkey,
|
||||
|
@ -148,11 +162,11 @@ impl<'a> LiquidateHelper<'a> {
|
|||
Ok(Some(sig))
|
||||
}
|
||||
|
||||
fn perp_liq_base_position(&self) -> anyhow::Result<Option<Signature>> {
|
||||
let mut perp_base_positions = self
|
||||
.liqee
|
||||
.active_perp_positions()
|
||||
.map(|pp| {
|
||||
async fn perp_liq_base_position(&self) -> anyhow::Result<Option<Signature>> {
|
||||
let all_perp_base_positions: anyhow::Result<
|
||||
Vec<Option<(PerpMarketIndex, i64, I80F48, I80F48)>>,
|
||||
> = stream::iter(self.liqee.active_perp_positions())
|
||||
.then(|pp| async {
|
||||
let base_lots = pp.base_position_lots();
|
||||
if base_lots == 0 {
|
||||
return Ok(None);
|
||||
|
@ -160,7 +174,8 @@ impl<'a> LiquidateHelper<'a> {
|
|||
let perp = self.client.context.perp(pp.market_index);
|
||||
let oracle = self
|
||||
.account_fetcher
|
||||
.fetch_raw_account(&perp.market.oracle)?;
|
||||
.fetch_raw_account(&perp.market.oracle)
|
||||
.await?;
|
||||
let price = perp.market.oracle_price(
|
||||
&KeyedAccountSharedData::new(perp.market.oracle, oracle.into()),
|
||||
None,
|
||||
|
@ -172,8 +187,12 @@ impl<'a> LiquidateHelper<'a> {
|
|||
I80F48::from(base_lots.abs()) * price,
|
||||
)))
|
||||
})
|
||||
.filter_map_ok(|v| v)
|
||||
.collect::<anyhow::Result<Vec<(PerpMarketIndex, i64, I80F48, I80F48)>>>()?;
|
||||
.try_collect()
|
||||
.await;
|
||||
let mut perp_base_positions = all_perp_base_positions?
|
||||
.into_iter()
|
||||
.filter_map(|x| x)
|
||||
.collect::<Vec<_>>();
|
||||
perp_base_positions.sort_by(|a, b| a.3.cmp(&b.3));
|
||||
|
||||
if perp_base_positions.is_empty() {
|
||||
|
@ -194,10 +213,12 @@ impl<'a> LiquidateHelper<'a> {
|
|||
let mut liqor = self
|
||||
.account_fetcher
|
||||
.fetch_fresh_mango_account(&self.client.mango_account_address)
|
||||
.await
|
||||
.context("getting liquidator account")?;
|
||||
liqor.ensure_perp_position(*perp_market_index, QUOTE_TOKEN_INDEX)?;
|
||||
let health_cache =
|
||||
health_cache::new(&self.client.context, self.account_fetcher, &liqor)
|
||||
.await
|
||||
.expect("always ok");
|
||||
health_cache.max_perp_for_health_ratio(
|
||||
*perp_market_index,
|
||||
|
@ -208,11 +229,14 @@ impl<'a> LiquidateHelper<'a> {
|
|||
};
|
||||
log::info!("computed max_base_transfer to be {max_base_transfer_abs}");
|
||||
|
||||
let sig = self.client.perp_liq_base_position(
|
||||
(self.pubkey, &self.liqee),
|
||||
*perp_market_index,
|
||||
side_signum * max_base_transfer_abs,
|
||||
)?;
|
||||
let sig = self
|
||||
.client
|
||||
.perp_liq_base_position(
|
||||
(self.pubkey, &self.liqee),
|
||||
*perp_market_index,
|
||||
side_signum * max_base_transfer_abs,
|
||||
)
|
||||
.await?;
|
||||
log::info!(
|
||||
"Liquidated base position for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||
self.pubkey,
|
||||
|
@ -223,7 +247,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
Ok(Some(sig))
|
||||
}
|
||||
|
||||
fn perp_settle_pnl(&self) -> anyhow::Result<Option<Signature>> {
|
||||
async fn perp_settle_pnl(&self) -> anyhow::Result<Option<Signature>> {
|
||||
let perp_settle_health = self.health_cache.perp_settle_health();
|
||||
let mut perp_settleable_pnl = self
|
||||
.liqee
|
||||
|
@ -262,7 +286,8 @@ impl<'a> LiquidateHelper<'a> {
|
|||
perp_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
|
||||
|
@ -270,7 +295,9 @@ impl<'a> LiquidateHelper<'a> {
|
|||
self.pubkey);
|
||||
continue;
|
||||
}
|
||||
let (counter_key, counter_acc, _) = counters.first().unwrap();
|
||||
let (counter_key, counter_acc, counter_pnl) = counters.first().unwrap();
|
||||
|
||||
log::info!("Trying to settle perp pnl account: {} market_index: {perp_index} amount: {pnl} against {counter_key} with pnl: {counter_pnl}", self.pubkey);
|
||||
|
||||
let (account_a, account_b) = if pnl > 0 {
|
||||
((self.pubkey, self.liqee), (counter_key, counter_acc))
|
||||
|
@ -279,7 +306,8 @@ impl<'a> LiquidateHelper<'a> {
|
|||
};
|
||||
let sig = self
|
||||
.client
|
||||
.perp_settle_pnl(perp_index, account_a, account_b)?;
|
||||
.perp_settle_pnl(perp_index, account_a, account_b)
|
||||
.await?;
|
||||
log::info!(
|
||||
"Settled perp pnl for perp market on account {}, market index {perp_index}, maint_health was {}, tx sig {sig:?}",
|
||||
self.pubkey,
|
||||
|
@ -290,7 +318,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
return Ok(None);
|
||||
}
|
||||
|
||||
fn perp_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
|
||||
async fn perp_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
|
||||
if self.health_cache.has_liquidatable_assets() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
@ -312,12 +340,15 @@ impl<'a> LiquidateHelper<'a> {
|
|||
}
|
||||
let (perp_market_index, _) = perp_bankruptcies.first().unwrap();
|
||||
|
||||
let sig = self.client.perp_liq_bankruptcy(
|
||||
(self.pubkey, &self.liqee),
|
||||
*perp_market_index,
|
||||
// Always use the max amount, since the health effect is always positive
|
||||
u64::MAX,
|
||||
)?;
|
||||
let sig = self
|
||||
.client
|
||||
.perp_liq_bankruptcy(
|
||||
(self.pubkey, &self.liqee),
|
||||
*perp_market_index,
|
||||
// Always use the max amount, since the health effect is always positive
|
||||
u64::MAX,
|
||||
)
|
||||
.await?;
|
||||
log::info!(
|
||||
"Liquidated bankruptcy for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||
self.pubkey,
|
||||
|
@ -328,34 +359,36 @@ impl<'a> LiquidateHelper<'a> {
|
|||
Ok(Some(sig))
|
||||
}
|
||||
|
||||
fn tokens(&self) -> anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>> {
|
||||
let mut tokens = self
|
||||
.liqee
|
||||
.active_token_positions()
|
||||
.map(|token_position| {
|
||||
let token = self.client.context.token(token_position.token_index);
|
||||
let bank = self
|
||||
.account_fetcher
|
||||
.fetch::<Bank>(&token.mint_info.first_bank())?;
|
||||
let oracle = self
|
||||
.account_fetcher
|
||||
.fetch_raw_account(&token.mint_info.oracle)?;
|
||||
let price = bank.oracle_price(
|
||||
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
|
||||
None,
|
||||
)?;
|
||||
Ok((
|
||||
token_position.token_index,
|
||||
price,
|
||||
token_position.native(&bank) * price,
|
||||
))
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>>>()?;
|
||||
async fn tokens(&self) -> anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>> {
|
||||
let tokens_maybe: anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>> =
|
||||
stream::iter(self.liqee.active_token_positions())
|
||||
.then(|token_position| async {
|
||||
let token = self.client.context.token(token_position.token_index);
|
||||
let bank = self
|
||||
.account_fetcher
|
||||
.fetch::<Bank>(&token.mint_info.first_bank())?;
|
||||
let oracle = self
|
||||
.account_fetcher
|
||||
.fetch_raw_account(&token.mint_info.oracle)
|
||||
.await?;
|
||||
let price = bank.oracle_price(
|
||||
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
|
||||
None,
|
||||
)?;
|
||||
Ok((
|
||||
token_position.token_index,
|
||||
price,
|
||||
token_position.native(&bank) * price,
|
||||
))
|
||||
})
|
||||
.try_collect()
|
||||
.await;
|
||||
let mut tokens = tokens_maybe?;
|
||||
tokens.sort_by(|a, b| a.2.cmp(&b.2));
|
||||
Ok(tokens)
|
||||
}
|
||||
|
||||
fn max_token_liab_transfer(
|
||||
async fn max_token_liab_transfer(
|
||||
&self,
|
||||
source: TokenIndex,
|
||||
target: TokenIndex,
|
||||
|
@ -363,6 +396,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
let mut liqor = self
|
||||
.account_fetcher
|
||||
.fetch_fresh_mango_account(&self.client.mango_account_address)
|
||||
.await
|
||||
.context("getting liquidator account")?;
|
||||
|
||||
// Ensure the tokens are activated, so they appear in the health cache and
|
||||
|
@ -371,10 +405,11 @@ impl<'a> LiquidateHelper<'a> {
|
|||
liqor.ensure_token_position(target)?;
|
||||
|
||||
let health_cache = health_cache::new(&self.client.context, self.account_fetcher, &liqor)
|
||||
.await
|
||||
.expect("always ok");
|
||||
|
||||
let source_bank = self.client.first_bank(source)?;
|
||||
let target_bank = self.client.first_bank(target)?;
|
||||
let source_bank = self.client.first_bank(source).await?;
|
||||
let target_bank = self.client.first_bank(target).await?;
|
||||
|
||||
let source_price = health_cache.token_info(source).unwrap().prices.oracle;
|
||||
let target_price = health_cache.token_info(target).unwrap().prices.oracle;
|
||||
|
@ -394,19 +429,21 @@ impl<'a> LiquidateHelper<'a> {
|
|||
Ok(amount)
|
||||
}
|
||||
|
||||
fn token_liq(&self) -> anyhow::Result<Option<Signature>> {
|
||||
if !self.health_cache.has_borrows() || self.health_cache.can_call_spot_bankruptcy() {
|
||||
async fn token_liq(&self) -> anyhow::Result<Option<Signature>> {
|
||||
if !self.health_cache.has_spot_assets() || !self.health_cache.has_spot_borrows() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let tokens = self.tokens()?;
|
||||
let tokens = self.tokens().await?;
|
||||
|
||||
let asset_token_index = tokens
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|(asset_token_index, _asset_price, asset_usdc_equivalent)| {
|
||||
asset_usdc_equivalent.is_positive()
|
||||
&& jupiter_market_can_sell(self.client, *asset_token_index, QUOTE_TOKEN_INDEX)
|
||||
&& self
|
||||
.allowed_asset_tokens
|
||||
.contains(&self.client.context.token(*asset_token_index).mint_info.mint)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
|
@ -420,7 +457,9 @@ impl<'a> LiquidateHelper<'a> {
|
|||
.iter()
|
||||
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
|
||||
liab_usdc_equivalent.is_negative()
|
||||
&& jupiter_market_can_buy(self.client, *liab_token_index, QUOTE_TOKEN_INDEX)
|
||||
&& self
|
||||
.allowed_liab_tokens
|
||||
.contains(&self.client.context.token(*liab_token_index).mint_info.mint)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
|
@ -433,6 +472,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
|
||||
let max_liab_transfer = self
|
||||
.max_token_liab_transfer(liab_token_index, asset_token_index)
|
||||
.await
|
||||
.context("getting max_liab_transfer")?;
|
||||
|
||||
//
|
||||
|
@ -447,6 +487,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
liab_token_index,
|
||||
max_liab_transfer,
|
||||
)
|
||||
.await
|
||||
.context("sending liq_token_with_token")?;
|
||||
log::info!(
|
||||
"Liquidated token with token for {}, maint_health was {}, tx sig {:?}",
|
||||
|
@ -457,12 +498,12 @@ impl<'a> LiquidateHelper<'a> {
|
|||
Ok(Some(sig))
|
||||
}
|
||||
|
||||
fn token_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
|
||||
async fn token_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
|
||||
if !self.health_cache.can_call_spot_bankruptcy() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let tokens = self.tokens()?;
|
||||
let tokens = self.tokens().await?;
|
||||
|
||||
if tokens.is_empty() {
|
||||
anyhow::bail!(
|
||||
|
@ -474,7 +515,9 @@ impl<'a> LiquidateHelper<'a> {
|
|||
.iter()
|
||||
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
|
||||
liab_usdc_equivalent.is_negative()
|
||||
&& jupiter_market_can_buy(self.client, *liab_token_index, QUOTE_TOKEN_INDEX)
|
||||
&& self
|
||||
.allowed_liab_tokens
|
||||
.contains(&self.client.context.token(*liab_token_index).mint_info.mint)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
|
@ -486,8 +529,9 @@ impl<'a> LiquidateHelper<'a> {
|
|||
.0;
|
||||
|
||||
let quote_token_index = 0;
|
||||
let max_liab_transfer =
|
||||
self.max_token_liab_transfer(liab_token_index, quote_token_index)?;
|
||||
let max_liab_transfer = self
|
||||
.max_token_liab_transfer(liab_token_index, quote_token_index)
|
||||
.await?;
|
||||
|
||||
let sig = self
|
||||
.client
|
||||
|
@ -496,6 +540,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
liab_token_index,
|
||||
max_liab_transfer,
|
||||
)
|
||||
.await
|
||||
.context("sending liq_token_bankruptcy")?;
|
||||
log::info!(
|
||||
"Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}",
|
||||
|
@ -506,7 +551,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
Ok(Some(sig))
|
||||
}
|
||||
|
||||
fn send_liq_tx(&self) -> anyhow::Result<Signature> {
|
||||
async fn send_liq_tx(&self) -> anyhow::Result<Signature> {
|
||||
// TODO: Should we make an attempt to settle positive PNL first?
|
||||
// The problem with it is that small market movements can continuously create
|
||||
// small amounts of new positive PNL while base_position > 0.
|
||||
|
@ -517,14 +562,14 @@ impl<'a> LiquidateHelper<'a> {
|
|||
// }
|
||||
|
||||
// Try to close orders before touching the user's positions
|
||||
if let Some(txsig) = self.perp_close_orders()? {
|
||||
if let Some(txsig) = self.perp_close_orders().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
if let Some(txsig) = self.serum3_close_orders()? {
|
||||
if let Some(txsig) = self.serum3_close_orders().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
|
||||
if let Some(txsig) = self.perp_liq_base_position()? {
|
||||
if let Some(txsig) = self.perp_liq_base_position().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
|
||||
|
@ -533,21 +578,21 @@ impl<'a> LiquidateHelper<'a> {
|
|||
// It's possible that some positive pnl can't be settled (if there's
|
||||
// no liquid counterparty) and that some negative pnl can't be settled
|
||||
// (if the liqee isn't liquid enough).
|
||||
if let Some(txsig) = self.perp_settle_pnl()? {
|
||||
if let Some(txsig) = self.perp_settle_pnl().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
|
||||
if let Some(txsig) = self.token_liq()? {
|
||||
if let Some(txsig) = self.token_liq().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
|
||||
// Socialize/insurance fund unsettleable negative pnl
|
||||
if let Some(txsig) = self.perp_liq_bankruptcy()? {
|
||||
if let Some(txsig) = self.perp_liq_bankruptcy().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
|
||||
// Socialize/insurance fund unliquidatable borrows
|
||||
if let Some(txsig) = self.token_liq_bankruptcy()? {
|
||||
if let Some(txsig) = self.token_liq_bankruptcy().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
|
||||
|
@ -562,7 +607,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn maybe_liquidate_account(
|
||||
pub async fn maybe_liquidate_account(
|
||||
mango_client: &MangoClient,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
pubkey: &Pubkey,
|
||||
|
@ -571,8 +616,9 @@ pub fn maybe_liquidate_account(
|
|||
let liqor_min_health_ratio = I80F48::from_num(config.min_health_ratio);
|
||||
|
||||
let account = account_fetcher.fetch_mango_account(pubkey)?;
|
||||
let health_cache =
|
||||
health_cache::new(&mango_client.context, account_fetcher, &account).expect("always ok");
|
||||
let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account)
|
||||
.await
|
||||
.expect("always ok");
|
||||
let maint_health = health_cache.health(HealthType::Maint);
|
||||
if !health_cache.is_liquidatable() {
|
||||
return Ok(false);
|
||||
|
@ -588,15 +634,24 @@ pub fn maybe_liquidate_account(
|
|||
// Fetch a fresh account and re-compute
|
||||
// This is -- unfortunately -- needed because the websocket streams seem to not
|
||||
// be great at providing timely updates to the account data.
|
||||
let account = account_fetcher.fetch_fresh_mango_account(pubkey)?;
|
||||
let health_cache =
|
||||
health_cache::new(&mango_client.context, account_fetcher, &account).expect("always ok");
|
||||
let account = account_fetcher.fetch_fresh_mango_account(pubkey).await?;
|
||||
let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account)
|
||||
.await
|
||||
.expect("always ok");
|
||||
if !health_cache.is_liquidatable() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let maint_health = health_cache.health(HealthType::Maint);
|
||||
|
||||
let all_token_mints = HashSet::from_iter(
|
||||
mango_client
|
||||
.context
|
||||
.tokens
|
||||
.values()
|
||||
.map(|c| c.mint_info.mint),
|
||||
);
|
||||
|
||||
// try liquidating
|
||||
let txsig = LiquidateHelper {
|
||||
client: mango_client,
|
||||
|
@ -606,15 +661,21 @@ pub fn maybe_liquidate_account(
|
|||
health_cache: &health_cache,
|
||||
maint_health,
|
||||
liqor_min_health_ratio,
|
||||
allowed_asset_tokens: all_token_mints.clone(),
|
||||
allowed_liab_tokens: all_token_mints,
|
||||
}
|
||||
.send_liq_tx()?;
|
||||
.send_liq_tx()
|
||||
.await?;
|
||||
|
||||
let slot = account_fetcher.transaction_max_slot(&[txsig])?;
|
||||
if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot(
|
||||
&[*pubkey, mango_client.mango_account_address],
|
||||
slot,
|
||||
config.refresh_timeout,
|
||||
) {
|
||||
let slot = account_fetcher.transaction_max_slot(&[txsig]).await?;
|
||||
if let Err(e) = account_fetcher
|
||||
.refresh_accounts_via_rpc_until_slot(
|
||||
&[*pubkey, mango_client.mango_account_address],
|
||||
slot,
|
||||
config.refresh_timeout,
|
||||
)
|
||||
.await
|
||||
{
|
||||
log::info!("could not refresh after liquidation: {}", e);
|
||||
}
|
||||
|
||||
|
@ -622,14 +683,14 @@ pub fn maybe_liquidate_account(
|
|||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn maybe_liquidate_one<'a>(
|
||||
pub async fn maybe_liquidate_one<'a>(
|
||||
mango_client: &MangoClient,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
accounts: impl Iterator<Item = &'a Pubkey>,
|
||||
config: &Config,
|
||||
) -> bool {
|
||||
for pubkey in accounts {
|
||||
match maybe_liquidate_account(mango_client, account_fetcher, pubkey, config) {
|
||||
match maybe_liquidate_account(mango_client, account_fetcher, pubkey, config).await {
|
||||
Err(err) => {
|
||||
// Not all errors need to be raised to the user's attention.
|
||||
let mut log_level = log::Level::Error;
|
||||
|
|
|
@ -114,13 +114,15 @@ async fn main() -> anyhow::Result<()> {
|
|||
// Reading accounts from chain_data
|
||||
let account_fetcher = Arc::new(chain_data::AccountFetcher {
|
||||
chain_data: chain_data.clone(),
|
||||
rpc: client.rpc(),
|
||||
rpc: client.rpc_async(),
|
||||
});
|
||||
|
||||
let mango_account = account_fetcher.fetch_fresh_mango_account(&cli.liqor_mango_account)?;
|
||||
let mango_account = account_fetcher
|
||||
.fetch_fresh_mango_account(&cli.liqor_mango_account)
|
||||
.await?;
|
||||
let mango_group = mango_account.fixed.group;
|
||||
|
||||
let group_context = MangoGroupContext::new_from_rpc(mango_group, cluster.clone(), commitment)?;
|
||||
let group_context = MangoGroupContext::new_from_rpc(&client.rpc_async(), mango_group).await?;
|
||||
|
||||
let mango_oracles = group_context
|
||||
.tokens
|
||||
|
@ -258,7 +260,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
std::iter::once(&account_write.pubkey),
|
||||
&liq_config,
|
||||
&rebalance_config,
|
||||
)?;
|
||||
).await?;
|
||||
}
|
||||
|
||||
if is_mango_bank(&account_write.account, &mango_program, &mango_group).is_some() || oracles.contains(&account_write.pubkey) {
|
||||
|
@ -280,7 +282,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
mango_accounts.iter(),
|
||||
&liq_config,
|
||||
&rebalance_config,
|
||||
)?;
|
||||
).await?;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -313,12 +315,12 @@ async fn main() -> anyhow::Result<()> {
|
|||
mango_accounts.iter(),
|
||||
&liq_config,
|
||||
&rebalance_config,
|
||||
)?;
|
||||
).await?;
|
||||
},
|
||||
|
||||
_ = rebalance_interval.tick() => {
|
||||
if one_snapshot_done {
|
||||
if let Err(err) = rebalance::zero_all_non_quote(&mango_client, &account_fetcher, &cli.liqor_mango_account, &rebalance_config) {
|
||||
if let Err(err) = rebalance::zero_all_non_quote(&mango_client, &account_fetcher, &cli.liqor_mango_account, &rebalance_config).await {
|
||||
log::error!("failed to rebalance liqor: {:?}", err);
|
||||
|
||||
// Workaround: We really need a sequence enforcer in the liquidator since we don't want to
|
||||
|
@ -332,20 +334,20 @@ async fn main() -> anyhow::Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
fn liquidate<'a>(
|
||||
async fn liquidate<'a>(
|
||||
mango_client: &MangoClient,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
accounts: impl Iterator<Item = &'a Pubkey>,
|
||||
config: &liquidate::Config,
|
||||
rebalance_config: &rebalance::Config,
|
||||
) -> anyhow::Result<()> {
|
||||
if !liquidate::maybe_liquidate_one(mango_client, account_fetcher, accounts, config) {
|
||||
if !liquidate::maybe_liquidate_one(mango_client, account_fetcher, accounts, config).await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let liqor = &mango_client.mango_account_address;
|
||||
if let Err(err) =
|
||||
rebalance::zero_all_non_quote(mango_client, account_fetcher, liqor, rebalance_config)
|
||||
rebalance::zero_all_non_quote(mango_client, account_fetcher, liqor, rebalance_config).await
|
||||
{
|
||||
log::error!("failed to rebalance liqor: {:?}", err);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use mango_v4::state::{Bank, TokenIndex, TokenPosition, QUOTE_TOKEN_INDEX};
|
|||
|
||||
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
||||
|
||||
use futures::{stream, StreamExt, TryStreamExt};
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
pub struct Config {
|
||||
|
@ -20,14 +21,14 @@ struct TokenState {
|
|||
}
|
||||
|
||||
impl TokenState {
|
||||
fn new_position(
|
||||
async fn new_position(
|
||||
token: &TokenContext,
|
||||
position: &TokenPosition,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
) -> anyhow::Result<Self> {
|
||||
let bank = Self::bank(token, account_fetcher)?;
|
||||
Ok(Self {
|
||||
price: Self::fetch_price(token, &bank, account_fetcher)?,
|
||||
price: Self::fetch_price(token, &bank, account_fetcher).await?,
|
||||
native_position: position.native(&bank),
|
||||
})
|
||||
}
|
||||
|
@ -39,12 +40,14 @@ impl TokenState {
|
|||
account_fetcher.fetch::<Bank>(&token.mint_info.first_bank())
|
||||
}
|
||||
|
||||
fn fetch_price(
|
||||
async fn fetch_price(
|
||||
token: &TokenContext,
|
||||
bank: &Bank,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
) -> anyhow::Result<I80F48> {
|
||||
let oracle = account_fetcher.fetch_raw_account(&token.mint_info.oracle)?;
|
||||
let oracle = account_fetcher
|
||||
.fetch_raw_account(&token.mint_info.oracle)
|
||||
.await?;
|
||||
bank.oracle_price(
|
||||
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
|
||||
None,
|
||||
|
@ -54,7 +57,7 @@ impl TokenState {
|
|||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn zero_all_non_quote(
|
||||
pub async fn zero_all_non_quote(
|
||||
mango_client: &MangoClient,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
mango_account_address: &Pubkey,
|
||||
|
@ -67,27 +70,32 @@ pub fn zero_all_non_quote(
|
|||
|
||||
let account = account_fetcher.fetch_mango_account(mango_account_address)?;
|
||||
|
||||
let tokens = account
|
||||
.active_token_positions()
|
||||
.map(|token_position| {
|
||||
let token = mango_client.context.token(token_position.token_index);
|
||||
Ok((
|
||||
token.token_index,
|
||||
TokenState::new_position(token, token_position, account_fetcher)?,
|
||||
))
|
||||
})
|
||||
.collect::<anyhow::Result<HashMap<TokenIndex, TokenState>>>()?;
|
||||
let tokens: anyhow::Result<HashMap<TokenIndex, TokenState>> =
|
||||
stream::iter(account.active_token_positions())
|
||||
.then(|token_position| async {
|
||||
let token = mango_client.context.token(token_position.token_index);
|
||||
Ok((
|
||||
token.token_index,
|
||||
TokenState::new_position(token, token_position, account_fetcher).await?,
|
||||
))
|
||||
})
|
||||
.try_collect()
|
||||
.await;
|
||||
let tokens = tokens?;
|
||||
log::trace!("account tokens: {:?}", tokens);
|
||||
|
||||
// Function to refresh the mango account after the txsig confirmed. Returns false on timeout.
|
||||
let refresh_mango_account =
|
||||
|account_fetcher: &chain_data::AccountFetcher, txsig| -> anyhow::Result<bool> {
|
||||
let max_slot = account_fetcher.transaction_max_slot(&[txsig])?;
|
||||
if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot(
|
||||
&[*mango_account_address],
|
||||
max_slot,
|
||||
config.refresh_timeout,
|
||||
) {
|
||||
let refresh_mango_account = |txsig| async move {
|
||||
let res: anyhow::Result<bool> = {
|
||||
let max_slot = account_fetcher.transaction_max_slot(&[txsig]).await?;
|
||||
if let Err(e) = account_fetcher
|
||||
.refresh_accounts_via_rpc_until_slot(
|
||||
&[*mango_account_address],
|
||||
max_slot,
|
||||
config.refresh_timeout,
|
||||
)
|
||||
.await
|
||||
{
|
||||
// If we don't get fresh data, maybe the tx landed on a fork?
|
||||
// Rebalance is technically still ok.
|
||||
log::info!("could not refresh account data: {}", e);
|
||||
|
@ -95,6 +103,8 @@ pub fn zero_all_non_quote(
|
|||
}
|
||||
Ok(true)
|
||||
};
|
||||
res
|
||||
};
|
||||
|
||||
for (token_index, token_state) in tokens {
|
||||
let token = mango_client.context.token(token_index);
|
||||
|
@ -120,13 +130,15 @@ pub fn zero_all_non_quote(
|
|||
|
||||
if amount > dust_threshold {
|
||||
// Sell
|
||||
let txsig = mango_client.jupiter_swap(
|
||||
token_mint,
|
||||
quote_mint,
|
||||
amount.to_num::<u64>(),
|
||||
config.slippage,
|
||||
client::JupiterSwapMode::ExactIn,
|
||||
)?;
|
||||
let txsig = mango_client
|
||||
.jupiter_swap(
|
||||
token_mint,
|
||||
quote_mint,
|
||||
amount.to_num::<u64>(),
|
||||
config.slippage,
|
||||
client::JupiterSwapMode::ExactIn,
|
||||
)
|
||||
.await?;
|
||||
log::info!(
|
||||
"sold {} {} for {} in tx {}",
|
||||
token.native_to_ui(amount),
|
||||
|
@ -134,12 +146,13 @@ pub fn zero_all_non_quote(
|
|||
quote_token.name,
|
||||
txsig
|
||||
);
|
||||
if !refresh_mango_account(account_fetcher, txsig)? {
|
||||
if !refresh_mango_account(txsig).await? {
|
||||
return Ok(());
|
||||
}
|
||||
let bank = TokenState::bank(token, account_fetcher)?;
|
||||
amount = mango_client
|
||||
.mango_account()?
|
||||
.mango_account()
|
||||
.await?
|
||||
.token_position_and_raw_index(token_index)
|
||||
.map(|(position, _)| position.native(&bank))
|
||||
.unwrap_or(I80F48::ZERO);
|
||||
|
@ -147,13 +160,15 @@ pub fn zero_all_non_quote(
|
|||
// Buy
|
||||
let buy_amount = (-token_state.native_position).ceil()
|
||||
+ (dust_threshold - I80F48::ONE).max(I80F48::ZERO);
|
||||
let txsig = mango_client.jupiter_swap(
|
||||
quote_mint,
|
||||
token_mint,
|
||||
buy_amount.to_num::<u64>(),
|
||||
config.slippage,
|
||||
client::JupiterSwapMode::ExactOut,
|
||||
)?;
|
||||
let txsig = mango_client
|
||||
.jupiter_swap(
|
||||
quote_mint,
|
||||
token_mint,
|
||||
buy_amount.to_num::<u64>(),
|
||||
config.slippage,
|
||||
client::JupiterSwapMode::ExactOut,
|
||||
)
|
||||
.await?;
|
||||
log::info!(
|
||||
"bought {} {} for {} in tx {}",
|
||||
token.native_to_ui(buy_amount),
|
||||
|
@ -161,12 +176,13 @@ pub fn zero_all_non_quote(
|
|||
quote_token.name,
|
||||
txsig
|
||||
);
|
||||
if !refresh_mango_account(account_fetcher, txsig)? {
|
||||
if !refresh_mango_account(txsig).await? {
|
||||
return Ok(());
|
||||
}
|
||||
let bank = TokenState::bank(token, account_fetcher)?;
|
||||
amount = mango_client
|
||||
.mango_account()?
|
||||
.mango_account()
|
||||
.await?
|
||||
.token_position_and_raw_index(token_index)
|
||||
.map(|(position, _)| position.native(&bank))
|
||||
.unwrap_or(I80F48::ZERO);
|
||||
|
@ -176,15 +192,16 @@ pub fn zero_all_non_quote(
|
|||
// TokenPosition is freed up
|
||||
if amount > 0 && amount <= dust_threshold {
|
||||
let allow_borrow = false;
|
||||
let txsig =
|
||||
mango_client.token_withdraw(token_mint, amount.to_num::<u64>(), allow_borrow)?;
|
||||
let txsig = mango_client
|
||||
.token_withdraw(token_mint, amount.to_num::<u64>(), allow_borrow)
|
||||
.await?;
|
||||
log::info!(
|
||||
"withdrew {} {} to liqor wallet in {}",
|
||||
token.native_to_ui(amount),
|
||||
token.name,
|
||||
txsig
|
||||
);
|
||||
if !refresh_mango_account(account_fetcher, txsig)? {
|
||||
if !refresh_mango_account(txsig).await? {
|
||||
return Ok(());
|
||||
}
|
||||
} else if amount > dust_threshold {
|
||||
|
|
|
@ -175,7 +175,10 @@ pub fn start(config: Config, mango_oracles: Vec<Pubkey>, sender: async_channel::
|
|||
loop {
|
||||
info!("connecting to solana websocket streams");
|
||||
let out = feed_data(&config, mango_oracles.clone(), sender.clone());
|
||||
let _ = out.await;
|
||||
let result = out.await;
|
||||
if let Err(err) = result {
|
||||
warn!("websocket stream error: {err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
"build": "npm run build:esm; npm run build:cjs",
|
||||
"build:cjs": "tsc -p tsconfig.cjs.json",
|
||||
"build:esm": "tsc -p tsconfig.esm.json",
|
||||
"test": "ts-mocha ts/client/**/*.spec.ts --timeout 15000",
|
||||
"test": "ts-mocha ts/client/**/*.spec.ts --timeout 300000",
|
||||
"clean": "rm -rf dist",
|
||||
"example1-user": "ts-node ts/client/src/scripts/example1-user.ts",
|
||||
"example1-admin": "ts-node ts/client/src/scripts/example1-admin.ts",
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
use fixed::types::I80F48;
|
||||
use fixed_macro::types::I80F48;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::state::{
|
||||
|
@ -12,8 +11,6 @@ use crate::util::checked_math as cm;
|
|||
|
||||
use super::*;
|
||||
|
||||
const ONE_NATIVE_USDC_IN_USD: I80F48 = I80F48!(0.000001);
|
||||
|
||||
/// Information about prices for a bank or perp market.
|
||||
#[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)]
|
||||
pub struct Prices {
|
||||
|
@ -201,7 +198,7 @@ impl Serum3Info {
|
|||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Serum3Reserved {
|
||||
pub(crate) struct Serum3Reserved {
|
||||
/// base tokens when the serum3info.reserved_quote get converted to base and added to reserved_base
|
||||
all_reserved_as_base: I80F48,
|
||||
/// ditto the other way around
|
||||
|
@ -442,11 +439,15 @@ impl HealthCache {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn has_liquidatable_assets(&self) -> bool {
|
||||
let spot_liquidatable = self.token_infos.iter().any(|ti| {
|
||||
pub fn has_spot_assets(&self) -> bool {
|
||||
self.token_infos.iter().any(|ti| {
|
||||
// can use token_liq_with_token
|
||||
ti.balance_native.is_positive()
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_liquidatable_assets(&self) -> bool {
|
||||
let spot_liquidatable = self.has_spot_assets();
|
||||
// can use serum3_liq_force_cancel_orders
|
||||
let serum3_cancelable = self
|
||||
.serum3_infos
|
||||
|
@ -455,10 +456,11 @@ impl HealthCache {
|
|||
let perp_liquidatable = self.perp_infos.iter().any(|p| {
|
||||
// can use perp_liq_base_position
|
||||
p.base_lots != 0
|
||||
// can use perp_settle_pnl
|
||||
|| p.quote > ONE_NATIVE_USDC_IN_USD // TODO: we're not guaranteed to be able to settle positive perp pnl!
|
||||
// can use perp_liq_force_cancel_orders
|
||||
|| p.has_open_orders
|
||||
// A remaining quote position can be reduced with perp_settle_pnl and that can improve health.
|
||||
// However, since it's not guaranteed that there is a counterparty, a positive perp quote position
|
||||
// does not prevent bankruptcy.
|
||||
});
|
||||
spot_liquidatable || serum3_cancelable || perp_liquidatable
|
||||
}
|
||||
|
@ -477,7 +479,7 @@ impl HealthCache {
|
|||
self.has_spot_borrows() || perp_borrows
|
||||
}
|
||||
|
||||
fn compute_serum3_reservations(
|
||||
pub(crate) fn compute_serum3_reservations(
|
||||
&self,
|
||||
health_type: HealthType,
|
||||
) -> (Vec<I80F48>, Vec<Serum3Reserved>) {
|
||||
|
|
|
@ -79,6 +79,46 @@ impl HealthCache {
|
|||
Ok(resulting_cache)
|
||||
}
|
||||
|
||||
pub fn max_swap_source_for_health_ratio(
|
||||
&self,
|
||||
account: &MangoAccountValue,
|
||||
source_bank: &Bank,
|
||||
source_oracle_price: I80F48,
|
||||
target_bank: &Bank,
|
||||
price: I80F48,
|
||||
min_ratio: I80F48,
|
||||
) -> Result<I80F48> {
|
||||
self.max_swap_source_for_health_fn(
|
||||
account,
|
||||
source_bank,
|
||||
source_oracle_price,
|
||||
target_bank,
|
||||
price,
|
||||
min_ratio,
|
||||
|cache| cache.health_ratio(HealthType::Init),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn max_swap_source_for_health(
|
||||
&self,
|
||||
account: &MangoAccountValue,
|
||||
source_bank: &Bank,
|
||||
source_oracle_price: I80F48,
|
||||
target_bank: &Bank,
|
||||
price: I80F48,
|
||||
min_ratio: I80F48,
|
||||
) -> Result<I80F48> {
|
||||
self.max_swap_source_for_health_fn(
|
||||
account,
|
||||
source_bank,
|
||||
source_oracle_price,
|
||||
target_bank,
|
||||
price,
|
||||
min_ratio,
|
||||
|cache| cache.health(HealthType::Init),
|
||||
)
|
||||
}
|
||||
|
||||
/// How many source native tokens may be swapped for target tokens while staying
|
||||
/// above the min_ratio health ratio.
|
||||
///
|
||||
|
@ -90,29 +130,27 @@ impl HealthCache {
|
|||
/// Positions for the source and deposit token index must already exist in the account.
|
||||
///
|
||||
/// NOTE: keep getMaxSourceForTokenSwap in ts/client in sync with changes here
|
||||
pub fn max_swap_source_for_health_ratio(
|
||||
pub fn max_swap_source_for_health_fn(
|
||||
&self,
|
||||
account: &MangoAccountValue,
|
||||
source_bank: &Bank,
|
||||
source_oracle_price: I80F48,
|
||||
target_bank: &Bank,
|
||||
price: I80F48,
|
||||
min_ratio: I80F48,
|
||||
min_fn_value: I80F48,
|
||||
target_fn: fn(&HealthCache) -> I80F48,
|
||||
) -> Result<I80F48> {
|
||||
// The health_ratio is nonlinear based on swap amount.
|
||||
// The health and health_ratio are nonlinear based on swap amount.
|
||||
// For large swap amounts the slope is guaranteed to be negative (unless the price
|
||||
// is extremely good), but small amounts can have positive slope (e.g. using
|
||||
// source deposits to pay back target borrows).
|
||||
//
|
||||
// That means:
|
||||
// - even if the initial ratio is < min_ratio it can be useful to swap to *increase* health
|
||||
// - be careful about finding the min_ratio point: the function isn't convex
|
||||
// - even if the initial value is < min_fn_value it can be useful to swap to *increase* health
|
||||
// - even if initial value is < 0, swapping can increase health (maybe above 0)
|
||||
// - be careful about finding the min_fn_value: the function isn't convex
|
||||
|
||||
let health_type = HealthType::Init;
|
||||
let initial_ratio = self.health_ratio(health_type);
|
||||
if initial_ratio < 0 {
|
||||
return Ok(I80F48::ZERO);
|
||||
}
|
||||
|
||||
// Fail if the health cache (or consequently the account) don't have existing
|
||||
// positions for the source and target token index.
|
||||
|
@ -122,6 +160,10 @@ impl HealthCache {
|
|||
let source = &self.token_infos[source_index];
|
||||
let target = &self.token_infos[target_index];
|
||||
|
||||
let (tokens_max_reserved, _) = self.compute_serum3_reservations(health_type);
|
||||
let source_reserved = tokens_max_reserved[source_index];
|
||||
let target_reserved = tokens_max_reserved[target_index];
|
||||
|
||||
// If the price is sufficiently good, then health will just increase from swapping:
|
||||
// once we've swapped enough, swapping x reduces health by x * source_liab_weight and
|
||||
// increases it by x * target_asset_weight * price_factor.
|
||||
|
@ -157,106 +199,81 @@ impl HealthCache {
|
|||
Err(err) => Err(err),
|
||||
}
|
||||
};
|
||||
let health_ratio_after_swap = |amount| {
|
||||
let fn_value_after_swap = |amount| {
|
||||
Ok(cache_after_swap(amount)?
|
||||
.map(|c| c.health_ratio(HealthType::Init))
|
||||
.as_ref()
|
||||
.map(target_fn)
|
||||
.unwrap_or(I80F48::MIN))
|
||||
};
|
||||
|
||||
// There are two key slope changes: Assume source.balance > 0 and target.balance < 0.
|
||||
// The function we're looking at has a unique maximum.
|
||||
//
|
||||
// If we discount serum3 reservations, there are two key slope changes:
|
||||
// Assume source.balance > 0 and target.balance < 0.
|
||||
// When these values flip sign, the health slope decreases, but could still be positive.
|
||||
// After point1 it's definitely negative (due to final_health_slope check above).
|
||||
// The maximum health ratio will be at 0 or at one of these points (ignoring serum3 effects).
|
||||
let source_for_zero_target_balance = -target.balance_native / price;
|
||||
let point0_amount = source
|
||||
.balance_native
|
||||
.min(source_for_zero_target_balance)
|
||||
.max(I80F48::ZERO);
|
||||
let point1_amount = source
|
||||
.balance_native
|
||||
.max(source_for_zero_target_balance)
|
||||
.max(I80F48::ZERO);
|
||||
let point0_ratio = health_ratio_after_swap(point0_amount)?;
|
||||
let (point1_ratio, point1_health) = {
|
||||
let cache = cache_after_swap(point1_amount)?;
|
||||
cache
|
||||
.map(|c| (c.health_ratio(HealthType::Init), c.health(HealthType::Init)))
|
||||
.unwrap_or((I80F48::MIN, I80F48::MIN))
|
||||
//
|
||||
// The first thing we do is to find this maximum.
|
||||
let (amount_for_max_value, max_value) = {
|
||||
// The largest amount that the maximum could be at
|
||||
let rightmost = (source.balance_native.abs() + source_reserved)
|
||||
.max((target.balance_native.abs() + target_reserved) / price);
|
||||
find_maximum(
|
||||
I80F48::ZERO,
|
||||
rightmost,
|
||||
I80F48::from_num(0.1),
|
||||
fn_value_after_swap,
|
||||
)?
|
||||
};
|
||||
assert!(amount_for_max_value >= 0);
|
||||
|
||||
let amount =
|
||||
if initial_ratio <= min_ratio && point0_ratio < min_ratio && point1_ratio < min_ratio {
|
||||
// If we have to stay below the target ratio, pick the highest one
|
||||
if point0_ratio > initial_ratio {
|
||||
if point1_ratio > point0_ratio {
|
||||
point1_amount
|
||||
} else {
|
||||
point0_amount
|
||||
}
|
||||
} else if point1_ratio > initial_ratio {
|
||||
point1_amount
|
||||
} else {
|
||||
I80F48::ZERO
|
||||
}
|
||||
} else if point1_ratio >= min_ratio {
|
||||
// If point1_ratio is still bigger than min_ratio, the target amount must be >point1_amount
|
||||
// search to the right of point1_amount: but how far?
|
||||
// At point1, source.balance < 0 and target.balance > 0, so use a simple estimation for
|
||||
// zero health: health
|
||||
// - source_liab_weight * source_liab_price * a
|
||||
// + target_asset_weight * target_asset_price * price * a = 0.
|
||||
// where a is the source token native amount.
|
||||
// Note that this is just an estimate. Swapping can increase the amount that serum3
|
||||
// reserved contributions offset, moving the actual zero point further to the right.
|
||||
if point1_health <= 0 {
|
||||
return Ok(I80F48::ZERO);
|
||||
}
|
||||
let zero_health_estimate = point1_amount - point1_health / final_health_slope;
|
||||
let right_bound = scan_right_until_less_than(
|
||||
zero_health_estimate,
|
||||
min_ratio,
|
||||
health_ratio_after_swap,
|
||||
)?;
|
||||
if right_bound == zero_health_estimate {
|
||||
binary_search(
|
||||
point1_amount,
|
||||
point1_ratio,
|
||||
right_bound,
|
||||
min_ratio,
|
||||
I80F48::from_num(0.1),
|
||||
health_ratio_after_swap,
|
||||
)?
|
||||
} else {
|
||||
binary_search(
|
||||
zero_health_estimate,
|
||||
health_ratio_after_swap(zero_health_estimate)?,
|
||||
right_bound,
|
||||
min_ratio,
|
||||
I80F48::from_num(0.1),
|
||||
health_ratio_after_swap,
|
||||
)?
|
||||
}
|
||||
} else if point0_ratio >= min_ratio {
|
||||
// Must be between point0_amount and point1_amount.
|
||||
if max_value <= min_fn_value {
|
||||
// We cannot reach min_ratio, just return the max
|
||||
return Ok(amount_for_max_value);
|
||||
}
|
||||
|
||||
let amount = {
|
||||
// Now max_value is bigger than min_fn_value, the target amount must be >amount_for_max_value.
|
||||
// Search to the right of amount_for_max_value: but how far?
|
||||
// Use a simple estimation for the amount that would lead to zero health:
|
||||
// health
|
||||
// - source_liab_weight * source_liab_price * a
|
||||
// + target_asset_weight * target_asset_price * price * a = 0.
|
||||
// where a is the source token native amount.
|
||||
// Note that this is just an estimate. Swapping can increase the amount that serum3
|
||||
// reserved contributions offset, moving the actual zero point further to the right.
|
||||
let health_at_max_value = cache_after_swap(amount_for_max_value)?
|
||||
.map(|c| c.health(health_type))
|
||||
.unwrap_or(I80F48::MIN);
|
||||
if health_at_max_value <= 0 {
|
||||
return Ok(I80F48::ZERO);
|
||||
}
|
||||
let zero_health_estimate =
|
||||
amount_for_max_value - health_at_max_value / final_health_slope;
|
||||
let right_bound = scan_right_until_less_than(
|
||||
zero_health_estimate,
|
||||
min_fn_value,
|
||||
fn_value_after_swap,
|
||||
)?;
|
||||
if right_bound == zero_health_estimate {
|
||||
binary_search(
|
||||
point0_amount,
|
||||
point0_ratio,
|
||||
point1_amount,
|
||||
min_ratio,
|
||||
amount_for_max_value,
|
||||
max_value,
|
||||
right_bound,
|
||||
min_fn_value,
|
||||
I80F48::from_num(0.1),
|
||||
health_ratio_after_swap,
|
||||
fn_value_after_swap,
|
||||
)?
|
||||
} else {
|
||||
// Must be between 0 and point0_amount
|
||||
binary_search(
|
||||
I80F48::ZERO,
|
||||
initial_ratio,
|
||||
point0_amount,
|
||||
min_ratio,
|
||||
zero_health_estimate,
|
||||
fn_value_after_swap(zero_health_estimate)?,
|
||||
right_bound,
|
||||
min_fn_value,
|
||||
I80F48::from_num(0.1),
|
||||
health_ratio_after_swap,
|
||||
fn_value_after_swap,
|
||||
)?
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
assert!(amount >= 0);
|
||||
Ok(amount)
|
||||
|
@ -449,6 +466,77 @@ fn binary_search(
|
|||
Err(error_msg!("binary search iterations exhausted"))
|
||||
}
|
||||
|
||||
/// This is not a generic function. It assumes there is a unique maximum between left and right.
|
||||
fn find_maximum(
|
||||
mut left: I80F48,
|
||||
mut right: I80F48,
|
||||
min_step: I80F48,
|
||||
fun: impl Fn(I80F48) -> Result<I80F48>,
|
||||
) -> Result<(I80F48, I80F48)> {
|
||||
assert!(right >= left);
|
||||
let half = I80F48::from_num(0.5);
|
||||
let mut mid = half * (left + right);
|
||||
let mut left_value = fun(left)?;
|
||||
let mut right_value = fun(right)?;
|
||||
let mut mid_value = fun(mid)?;
|
||||
while (right - left) > min_step {
|
||||
if left_value >= mid_value {
|
||||
// max must be between left and mid
|
||||
assert!(mid_value >= right_value);
|
||||
right = mid;
|
||||
right_value = mid_value;
|
||||
mid = half * (left + mid);
|
||||
mid_value = fun(mid)?
|
||||
} else if mid_value <= right_value {
|
||||
// max must be between mid and right
|
||||
assert!(left_value <= mid_value);
|
||||
left = mid;
|
||||
left_value = mid_value;
|
||||
mid = half * (mid + right);
|
||||
mid_value = fun(mid)?;
|
||||
} else {
|
||||
// mid is larger than both left and right, max could be on either side
|
||||
let leftmid = half * (left + mid);
|
||||
let leftmid_value = fun(leftmid)?;
|
||||
assert!(leftmid_value >= left_value);
|
||||
if leftmid_value >= mid_value {
|
||||
// max between left and mid
|
||||
right = mid;
|
||||
right_value = mid_value;
|
||||
mid = leftmid;
|
||||
mid_value = leftmid_value;
|
||||
continue;
|
||||
}
|
||||
|
||||
let rightmid = half * (mid + right);
|
||||
let rightmid_value = fun(rightmid)?;
|
||||
assert!(rightmid_value >= right_value);
|
||||
if rightmid_value >= mid_value {
|
||||
// max between mid and right
|
||||
left = mid;
|
||||
left_value = mid_value;
|
||||
mid = rightmid;
|
||||
mid_value = rightmid_value;
|
||||
continue;
|
||||
}
|
||||
|
||||
// max between leftmid and rightmid
|
||||
left = leftmid;
|
||||
left_value = leftmid_value;
|
||||
right = rightmid;
|
||||
right_value = rightmid_value;
|
||||
}
|
||||
}
|
||||
|
||||
if left_value >= mid_value {
|
||||
Ok((left, left_value))
|
||||
} else if mid_value >= right_value {
|
||||
Ok((mid, mid_value))
|
||||
} else {
|
||||
Ok((right, right_value))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::test::*;
|
||||
|
@ -532,6 +620,8 @@ mod tests {
|
|||
I80F48::ZERO
|
||||
);
|
||||
|
||||
type MaxSwapFn = fn(&HealthCache) -> I80F48;
|
||||
|
||||
let adjust_by_usdc = |c: &mut HealthCache, ti: TokenIndex, usdc: f64| {
|
||||
let ti = &mut c.token_infos[ti as usize];
|
||||
ti.balance_native += I80F48::from_num(usdc) / ti.prices.oracle;
|
||||
|
@ -539,9 +629,10 @@ mod tests {
|
|||
let find_max_swap_actual = |c: &HealthCache,
|
||||
source: TokenIndex,
|
||||
target: TokenIndex,
|
||||
ratio: f64,
|
||||
min_value: f64,
|
||||
price_factor: f64,
|
||||
banks: [Bank; 3]| {
|
||||
banks: [Bank; 3],
|
||||
max_swap_fn: MaxSwapFn| {
|
||||
let source_price = &c.token_infos[source as usize].prices;
|
||||
let source_bank = &banks[source as usize];
|
||||
let target_price = &c.token_infos[target as usize].prices;
|
||||
|
@ -549,19 +640,20 @@ mod tests {
|
|||
let swap_price =
|
||||
I80F48::from_num(price_factor) * source_price.oracle / target_price.oracle;
|
||||
let source_amount = c
|
||||
.max_swap_source_for_health_ratio(
|
||||
.max_swap_source_for_health_fn(
|
||||
&account,
|
||||
source_bank,
|
||||
source_price.oracle,
|
||||
target_bank,
|
||||
swap_price,
|
||||
I80F48::from_num(ratio),
|
||||
I80F48::from_num(min_value),
|
||||
max_swap_fn,
|
||||
)
|
||||
.unwrap();
|
||||
if source_amount == I80F48::MAX {
|
||||
return (f64::MAX, f64::MAX, f64::MAX, f64::MAX);
|
||||
}
|
||||
let ratio_for_amount = |amount| {
|
||||
let value_for_amount = |amount| {
|
||||
c.cache_after_swap(
|
||||
&account,
|
||||
source_bank,
|
||||
|
@ -570,152 +662,262 @@ mod tests {
|
|||
I80F48::from(amount),
|
||||
swap_price,
|
||||
)
|
||||
.map(|c| c.health_ratio(HealthType::Init).to_num::<f64>())
|
||||
.map(|c| max_swap_fn(&c).to_num::<f64>())
|
||||
.unwrap_or(f64::MIN)
|
||||
};
|
||||
(
|
||||
source_amount.to_num(),
|
||||
ratio_for_amount(source_amount),
|
||||
ratio_for_amount(source_amount - I80F48::ONE),
|
||||
ratio_for_amount(source_amount + I80F48::ONE),
|
||||
value_for_amount(source_amount),
|
||||
value_for_amount(source_amount - I80F48::ONE),
|
||||
value_for_amount(source_amount + I80F48::ONE),
|
||||
)
|
||||
};
|
||||
let check_max_swap_result = |c: &HealthCache,
|
||||
source: TokenIndex,
|
||||
target: TokenIndex,
|
||||
ratio: f64,
|
||||
min_value: f64,
|
||||
price_factor: f64,
|
||||
banks: [Bank; 3]| {
|
||||
let (source_amount, actual_ratio, minus_ratio, plus_ratio) =
|
||||
find_max_swap_actual(c, source, target, ratio, price_factor, banks);
|
||||
banks: [Bank; 3],
|
||||
max_swap_fn: MaxSwapFn| {
|
||||
let (source_amount, actual_value, minus_value, plus_value) = find_max_swap_actual(
|
||||
c,
|
||||
source,
|
||||
target,
|
||||
min_value,
|
||||
price_factor,
|
||||
banks,
|
||||
max_swap_fn,
|
||||
);
|
||||
println!(
|
||||
"checking {source} to {target} for price_factor: {price_factor}, target ratio {ratio}: actual ratios: {minus_ratio}/{actual_ratio}/{plus_ratio}, amount: {source_amount}",
|
||||
"checking {source} to {target} for price_factor: {price_factor}, target {min_value}: actual: {minus_value}/{actual_value}/{plus_value}, amount: {source_amount}",
|
||||
);
|
||||
assert!(actual_ratio >= ratio);
|
||||
// either we're within tolerance of the target, or swapping 1 more would
|
||||
// bring us below the target
|
||||
assert!(actual_ratio < ratio + 1.0 || plus_ratio < ratio);
|
||||
if actual_value < min_value {
|
||||
// check that swapping more would decrease the ratio!
|
||||
assert!(plus_value < actual_value);
|
||||
} else {
|
||||
assert!(actual_value >= min_value);
|
||||
// either we're within tolerance of the target, or swapping 1 more would
|
||||
// bring us below the target
|
||||
assert!(actual_value < min_value + 1.0 || plus_value < min_value);
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
println!("test 0");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 1, 100.0);
|
||||
let health_fn: Box<MaxSwapFn> = Box::new(|c: &HealthCache| c.health(HealthType::Init));
|
||||
let health_ratio_fn: Box<MaxSwapFn> =
|
||||
Box::new(|c: &HealthCache| c.health_ratio(HealthType::Init));
|
||||
|
||||
for price_factor in [0.1, 0.9, 1.1] {
|
||||
for target in 1..100 {
|
||||
let target = target as f64;
|
||||
check_max_swap_result(&health_cache, 0, 1, target, price_factor, banks);
|
||||
check_max_swap_result(&health_cache, 1, 0, target, price_factor, banks);
|
||||
check_max_swap_result(&health_cache, 0, 2, target, price_factor, banks);
|
||||
for (test_name, max_swap_fn) in [("health", health_fn), ("health_ratio", health_ratio_fn)] {
|
||||
let check = |c: &HealthCache,
|
||||
source: TokenIndex,
|
||||
target: TokenIndex,
|
||||
min_value: f64,
|
||||
price_factor: f64,
|
||||
banks: [Bank; 3]| {
|
||||
check_max_swap_result(
|
||||
c,
|
||||
source,
|
||||
target,
|
||||
min_value,
|
||||
price_factor,
|
||||
banks,
|
||||
*max_swap_fn,
|
||||
)
|
||||
};
|
||||
|
||||
let find_max_swap = |c: &HealthCache,
|
||||
source: TokenIndex,
|
||||
target: TokenIndex,
|
||||
min_value: f64,
|
||||
price_factor: f64,
|
||||
banks: [Bank; 3]| {
|
||||
find_max_swap_actual(
|
||||
c,
|
||||
source,
|
||||
target,
|
||||
min_value,
|
||||
price_factor,
|
||||
banks,
|
||||
*max_swap_fn,
|
||||
)
|
||||
};
|
||||
|
||||
{
|
||||
println!("test 0 {test_name}");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 1, 100.0);
|
||||
|
||||
for price_factor in [0.1, 0.9, 1.1] {
|
||||
for target in 1..100 {
|
||||
let target = target as f64;
|
||||
check(&health_cache, 0, 1, target, price_factor, banks);
|
||||
check(&health_cache, 1, 0, target, price_factor, banks);
|
||||
check(&health_cache, 0, 2, target, price_factor, banks);
|
||||
}
|
||||
}
|
||||
|
||||
// At this unlikely price it's healthy to swap infinitely
|
||||
assert_eq!(
|
||||
find_max_swap(&health_cache, 0, 1, 50.0, 1.5, banks).0,
|
||||
f64::MAX
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
println!("test 1 {test_name}");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 0, -20.0);
|
||||
adjust_by_usdc(&mut health_cache, 1, 100.0);
|
||||
|
||||
for price_factor in [0.1, 0.9, 1.1] {
|
||||
for target in 1..100 {
|
||||
let target = target as f64;
|
||||
check(&health_cache, 0, 1, target, price_factor, banks);
|
||||
check(&health_cache, 1, 0, target, price_factor, banks);
|
||||
check(&health_cache, 0, 2, target, price_factor, banks);
|
||||
check(&health_cache, 2, 0, target, price_factor, banks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// At this unlikely price it's healthy to swap infinitely
|
||||
assert_eq!(
|
||||
find_max_swap_actual(&health_cache, 0, 1, 50.0, 1.5, banks).0,
|
||||
f64::MAX
|
||||
);
|
||||
}
|
||||
{
|
||||
println!("test 2 {test_name}");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 0, -50.0);
|
||||
adjust_by_usdc(&mut health_cache, 1, 100.0);
|
||||
// possible even though the init ratio is <100
|
||||
check(&health_cache, 1, 0, 100.0, 1.0, banks);
|
||||
}
|
||||
|
||||
{
|
||||
println!("test 1");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 0, -20.0);
|
||||
adjust_by_usdc(&mut health_cache, 1, 100.0);
|
||||
{
|
||||
println!("test 3 {test_name}");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 0, -30.0);
|
||||
adjust_by_usdc(&mut health_cache, 1, 100.0);
|
||||
adjust_by_usdc(&mut health_cache, 2, -30.0);
|
||||
|
||||
for price_factor in [0.1, 0.9, 1.1] {
|
||||
for target in 1..100 {
|
||||
let target = target as f64;
|
||||
check_max_swap_result(&health_cache, 0, 1, target, price_factor, banks);
|
||||
check_max_swap_result(&health_cache, 1, 0, target, price_factor, banks);
|
||||
check_max_swap_result(&health_cache, 0, 2, target, price_factor, banks);
|
||||
check_max_swap_result(&health_cache, 2, 0, target, price_factor, banks);
|
||||
// swapping with a high ratio advises paying back all liabs
|
||||
// and then swapping even more because increasing assets in 0 has better asset weight
|
||||
let init_ratio = health_cache.health_ratio(HealthType::Init);
|
||||
let (amount, actual_ratio, _, _) =
|
||||
find_max_swap(&health_cache, 1, 0, 100.0, 1.0, banks);
|
||||
println!(
|
||||
"init {}, after {}, amount {}",
|
||||
init_ratio, actual_ratio, amount
|
||||
);
|
||||
assert!(actual_ratio / 2.0 > init_ratio);
|
||||
assert!((amount as f64 - 100.0 / 3.0).abs() < 1.0);
|
||||
}
|
||||
|
||||
{
|
||||
println!("test 4 {test_name}");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 0, 100.0);
|
||||
adjust_by_usdc(&mut health_cache, 1, -2.0);
|
||||
adjust_by_usdc(&mut health_cache, 2, -65.0);
|
||||
|
||||
let init_ratio = health_cache.health_ratio(HealthType::Init);
|
||||
assert!(init_ratio > 3 && init_ratio < 4);
|
||||
|
||||
check(&health_cache, 0, 1, 1.0, 1.0, banks);
|
||||
check(&health_cache, 0, 1, 3.0, 1.0, banks);
|
||||
check(&health_cache, 0, 1, 4.0, 1.0, banks);
|
||||
}
|
||||
|
||||
{
|
||||
// check with net borrow limits
|
||||
println!("test 5 {test_name}");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 1, 100.0);
|
||||
let mut banks = banks.clone();
|
||||
banks[0].net_borrow_limit_per_window_quote = 50;
|
||||
|
||||
// The net borrow limit restricts the amount that can be swapped
|
||||
// (tracking happens without decimals)
|
||||
assert!(find_max_swap(&health_cache, 0, 1, 1.0, 1.0, banks).0 < 51.0);
|
||||
}
|
||||
|
||||
{
|
||||
// check with serum reserved
|
||||
println!("test 6 {test_name}");
|
||||
let mut health_cache = health_cache.clone();
|
||||
health_cache.serum3_infos = vec![Serum3Info {
|
||||
base_index: 1,
|
||||
quote_index: 0,
|
||||
market_index: 0,
|
||||
reserved_base: I80F48::from(30 / 3),
|
||||
reserved_quote: I80F48::from(30 / 2),
|
||||
}];
|
||||
adjust_by_usdc(&mut health_cache, 0, -20.0);
|
||||
adjust_by_usdc(&mut health_cache, 1, -40.0);
|
||||
adjust_by_usdc(&mut health_cache, 2, 120.0);
|
||||
|
||||
for price_factor in [0.9, 1.1] {
|
||||
for target in 1..100 {
|
||||
let target = target as f64;
|
||||
check(&health_cache, 0, 1, target, price_factor, banks);
|
||||
check(&health_cache, 1, 0, target, price_factor, banks);
|
||||
check(&health_cache, 0, 2, target, price_factor, banks);
|
||||
check(&health_cache, 1, 2, target, price_factor, banks);
|
||||
check(&health_cache, 2, 0, target, price_factor, banks);
|
||||
check(&health_cache, 2, 1, target, price_factor, banks);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
println!("test 2");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 0, -50.0);
|
||||
adjust_by_usdc(&mut health_cache, 1, 100.0);
|
||||
// possible even though the init ratio is <100
|
||||
check_max_swap_result(&health_cache, 1, 0, 100.0, 1.0, banks);
|
||||
}
|
||||
{
|
||||
// check starting with negative health but swapping can make it positive
|
||||
println!("test 7 {test_name}");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 0, -20.0);
|
||||
adjust_by_usdc(&mut health_cache, 1, 20.0);
|
||||
assert!(health_cache.health(HealthType::Init) < 0);
|
||||
|
||||
{
|
||||
println!("test 3");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 0, -30.0);
|
||||
adjust_by_usdc(&mut health_cache, 1, 100.0);
|
||||
adjust_by_usdc(&mut health_cache, 2, -30.0);
|
||||
if test_name == "health" {
|
||||
assert!(find_max_swap(&health_cache, 1, 0, 1.0, 1.0, banks).0 > 0.0);
|
||||
}
|
||||
|
||||
// swapping with a high ratio advises paying back all liabs
|
||||
// and then swapping even more because increasing assets in 0 has better asset weight
|
||||
let init_ratio = health_cache.health_ratio(HealthType::Init);
|
||||
let (amount, actual_ratio, _, _) =
|
||||
find_max_swap_actual(&health_cache, 1, 0, 100.0, 1.0, banks);
|
||||
println!(
|
||||
"init {}, after {}, amount {}",
|
||||
init_ratio, actual_ratio, amount
|
||||
);
|
||||
assert!(actual_ratio / 2.0 > init_ratio);
|
||||
assert!((amount as f64 - 100.0 / 3.0).abs() < 1.0);
|
||||
}
|
||||
for price_factor in [0.9, 1.1] {
|
||||
for target in 1..100 {
|
||||
let target = target as f64;
|
||||
check(&health_cache, 1, 0, target, price_factor, banks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
println!("test 4");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 0, 100.0);
|
||||
adjust_by_usdc(&mut health_cache, 1, -2.0);
|
||||
adjust_by_usdc(&mut health_cache, 2, -65.0);
|
||||
{
|
||||
// check starting with negative health but swapping can't make it positive
|
||||
println!("test 8 {test_name}");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 0, -20.0);
|
||||
adjust_by_usdc(&mut health_cache, 1, 10.0);
|
||||
assert!(health_cache.health(HealthType::Init) < 0);
|
||||
|
||||
let init_ratio = health_cache.health_ratio(HealthType::Init);
|
||||
assert!(init_ratio > 3 && init_ratio < 4);
|
||||
if test_name == "health" {
|
||||
assert!(find_max_swap(&health_cache, 1, 0, 1.0, 1.0, banks).0 > 0.0);
|
||||
}
|
||||
|
||||
check_max_swap_result(&health_cache, 0, 1, 1.0, 1.0, banks);
|
||||
check_max_swap_result(&health_cache, 0, 1, 3.0, 1.0, banks);
|
||||
check_max_swap_result(&health_cache, 0, 1, 4.0, 1.0, banks);
|
||||
}
|
||||
for price_factor in [0.9, 1.1] {
|
||||
for target in 1..100 {
|
||||
let target = target as f64;
|
||||
check(&health_cache, 1, 0, target, price_factor, banks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// check with net borrow limits
|
||||
println!("test 5");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 1, 100.0);
|
||||
let mut banks = banks.clone();
|
||||
banks[0].net_borrow_limit_per_window_quote = 50;
|
||||
{
|
||||
// swap some assets into a zero-asset-weight token
|
||||
println!("test 9 {test_name}");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 0, 10.0);
|
||||
health_cache.token_infos[1].init_asset_weight = I80F48::from(0);
|
||||
|
||||
// The net borrow limit restricts the amount that can be swapped
|
||||
// (tracking happens without decimals)
|
||||
assert!(find_max_swap_actual(&health_cache, 0, 1, 1.0, 1.0, banks).0 < 51.0);
|
||||
}
|
||||
assert!(find_max_swap(&health_cache, 0, 1, 1.0, 1.0, banks).0 > 0.0);
|
||||
|
||||
{
|
||||
// check with serum reserved
|
||||
println!("test 6");
|
||||
let mut health_cache = health_cache.clone();
|
||||
health_cache.serum3_infos = vec![Serum3Info {
|
||||
base_index: 1,
|
||||
quote_index: 0,
|
||||
market_index: 0,
|
||||
reserved_base: I80F48::from(30 / 3),
|
||||
reserved_quote: I80F48::from(30 / 2),
|
||||
}];
|
||||
adjust_by_usdc(&mut health_cache, 0, -20.0);
|
||||
adjust_by_usdc(&mut health_cache, 1, -40.0);
|
||||
adjust_by_usdc(&mut health_cache, 2, 120.0);
|
||||
|
||||
for price_factor in [0.9, 1.1] {
|
||||
for target in 1..100 {
|
||||
let target = target as f64;
|
||||
check_max_swap_result(&health_cache, 0, 1, target, price_factor, banks);
|
||||
check_max_swap_result(&health_cache, 1, 0, target, price_factor, banks);
|
||||
check_max_swap_result(&health_cache, 0, 2, target, price_factor, banks);
|
||||
check_max_swap_result(&health_cache, 1, 2, target, price_factor, banks);
|
||||
check_max_swap_result(&health_cache, 2, 0, target, price_factor, banks);
|
||||
check_max_swap_result(&health_cache, 2, 1, target, price_factor, banks);
|
||||
for price_factor in [0.9, 1.1] {
|
||||
for target in 1..100 {
|
||||
let target = target as f64;
|
||||
check(&health_cache, 0, 1, target, price_factor, banks);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,13 +24,14 @@ pub struct AccountClose<'info> {
|
|||
pub token_program: Program<'info, Token>,
|
||||
}
|
||||
|
||||
pub fn account_close(ctx: Context<AccountClose>) -> Result<()> {
|
||||
let group = ctx.accounts.group.load()?;
|
||||
|
||||
pub fn account_close(ctx: Context<AccountClose>, force_close: bool) -> Result<()> {
|
||||
let account = ctx.accounts.account.load_mut()?;
|
||||
|
||||
// don't perform checks if group is just testing
|
||||
if !group.is_testing() {
|
||||
if !ctx.accounts.group.load()?.is_testing() {
|
||||
require!(!force_close, MangoError::SomeError);
|
||||
}
|
||||
|
||||
if !force_close {
|
||||
require!(!account.fixed.being_liquidated(), MangoError::SomeError);
|
||||
for ele in account.all_token_positions() {
|
||||
require_eq!(ele.is_active(), false);
|
||||
|
|
|
@ -387,6 +387,15 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
|||
approved_amount
|
||||
};
|
||||
|
||||
// Enforce min vault to deposits ratio
|
||||
if loan > 0 {
|
||||
let vault_ai = vaults
|
||||
.iter()
|
||||
.find(|vault_ai| vault_ai.key == &bank.vault)
|
||||
.unwrap();
|
||||
bank.enforce_min_vault_to_deposits_ratio(vault_ai)?;
|
||||
}
|
||||
|
||||
let loan_origination_fee = cm!(loan * bank.loan_origination_fee_rate);
|
||||
cm!(bank.collected_fees_native += loan_origination_fee);
|
||||
|
||||
|
|
|
@ -344,8 +344,19 @@ pub fn serum3_place_order(
|
|||
// Placing an order cannot increase vault balance
|
||||
require_gte!(before_vault, after_vault);
|
||||
|
||||
// Charge the difference in vault balance to the user's account
|
||||
let mut payer_bank = ctx.accounts.payer_bank.load_mut()?;
|
||||
|
||||
// Enforce min vault to deposits ratio
|
||||
let withdrawn_from_vault = I80F48::from(cm!(before_vault - after_vault));
|
||||
let position_native = account
|
||||
.token_position_mut(payer_bank.token_index)?
|
||||
.0
|
||||
.native(&payer_bank);
|
||||
if withdrawn_from_vault > position_native {
|
||||
payer_bank.enforce_min_vault_to_deposits_ratio((*ctx.accounts.payer_vault).as_ref())?;
|
||||
}
|
||||
|
||||
// Charge the difference in vault balance to the user's account
|
||||
let vault_difference = {
|
||||
let oracle_price =
|
||||
payer_bank.oracle_price(&AccountInfoRef::borrow(&ctx.accounts.payer_oracle)?, None)?;
|
||||
|
|
|
@ -181,20 +181,10 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
|||
});
|
||||
}
|
||||
|
||||
// Prevent borrowing away the full bank vault. Keep some in reserve to satisfy non-borrow withdraws
|
||||
let bank_native_deposits = bank.native_deposits();
|
||||
if bank_native_deposits != I80F48::ZERO && is_borrow {
|
||||
// Enforce min vault to deposits ratio
|
||||
if is_borrow {
|
||||
ctx.accounts.vault.reload()?;
|
||||
let bank_native_deposits: f64 = bank_native_deposits.checked_to_num().unwrap();
|
||||
let vault_amount = ctx.accounts.vault.amount as f64;
|
||||
if vault_amount < bank.min_vault_to_deposits_ratio * bank_native_deposits {
|
||||
return err!(MangoError::BankBorrowLimitReached).with_context(|| {
|
||||
format!(
|
||||
"vault_amount ({:?}) below min_vault_to_deposits_ratio * bank_native_deposits ({:?})",
|
||||
vault_amount, bank.min_vault_to_deposits_ratio * bank_native_deposits,
|
||||
)
|
||||
});
|
||||
}
|
||||
bank.enforce_min_vault_to_deposits_ratio(ctx.accounts.vault.as_ref())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -214,8 +214,8 @@ pub mod mango_v4 {
|
|||
instructions::account_edit(ctx, name_opt, delegate_opt)
|
||||
}
|
||||
|
||||
pub fn account_close(ctx: Context<AccountClose>) -> Result<()> {
|
||||
instructions::account_close(ctx)
|
||||
pub fn account_close(ctx: Context<AccountClose>, force_close: bool) -> Result<()> {
|
||||
instructions::account_close(ctx, force_close)
|
||||
}
|
||||
|
||||
// todo:
|
||||
|
|
|
@ -5,6 +5,7 @@ use crate::state::{oracle, StablePriceModel};
|
|||
use crate::util;
|
||||
use crate::util::checked_math as cm;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::TokenAccount;
|
||||
use derivative::Derivative;
|
||||
use fixed::types::I80F48;
|
||||
use fixed_macro::types::I80F48;
|
||||
|
@ -234,6 +235,29 @@ impl Bank {
|
|||
cm!(self.deposit_index * self.indexed_deposits)
|
||||
}
|
||||
|
||||
/// Prevent borrowing away the full bank vault.
|
||||
/// Keep some in reserve to satisfy non-borrow withdraws.
|
||||
pub fn enforce_min_vault_to_deposits_ratio(&self, vault_ai: &AccountInfo) -> Result<()> {
|
||||
require_keys_eq!(self.vault, vault_ai.key());
|
||||
|
||||
let vault = Account::<TokenAccount>::try_from(vault_ai)?;
|
||||
let vault_amount = vault.amount as f64;
|
||||
|
||||
let bank_native_deposits = self.native_deposits();
|
||||
if bank_native_deposits != I80F48::ZERO {
|
||||
let bank_native_deposits: f64 = bank_native_deposits.checked_to_num().unwrap();
|
||||
if vault_amount < self.min_vault_to_deposits_ratio * bank_native_deposits {
|
||||
return err!(MangoError::BankBorrowLimitReached).with_context(|| {
|
||||
format!(
|
||||
"vault_amount ({:?}) below min_vault_to_deposits_ratio * bank_native_deposits ({:?})",
|
||||
vault_amount, self.min_vault_to_deposits_ratio * bank_native_deposits,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deposits `native_amount`.
|
||||
///
|
||||
/// If the token position ends up positive but below one native token and this token
|
||||
|
|
|
@ -1590,7 +1590,7 @@ impl ClientInstruction for AccountCloseInstruction {
|
|||
_account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
let instruction = Self::Instruction {};
|
||||
let instruction = Self::Instruction { force_close: false };
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
group: self.group,
|
||||
|
|
|
@ -2,7 +2,7 @@ import { BN } from '@project-serum/anchor';
|
|||
import { OpenOrders } from '@project-serum/serum';
|
||||
import { expect } from 'chai';
|
||||
import _ from 'lodash';
|
||||
import { I80F48, ZERO_I80F48 } from '../numbers/I80F48';
|
||||
import { I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
|
||||
import { BankForHealth, StablePriceModel, TokenIndex } from './bank';
|
||||
import { HealthCache, PerpInfo, Serum3Info, TokenInfo } from './healthCache';
|
||||
import { HealthType, PerpPosition } from './mangoAccount';
|
||||
|
@ -412,7 +412,7 @@ describe('Health Cache', () => {
|
|||
|
||||
expect(
|
||||
hc
|
||||
.getMaxSourceForTokenSwap(
|
||||
.getMaxSwapSourceForHealthRatio(
|
||||
b0,
|
||||
b1,
|
||||
I80F48.fromNumber(2 / 3),
|
||||
|
@ -425,9 +425,10 @@ describe('Health Cache', () => {
|
|||
hc: HealthCache,
|
||||
source: TokenIndex,
|
||||
target: TokenIndex,
|
||||
ratio: number,
|
||||
minValue: number,
|
||||
priceFactor: number,
|
||||
): I80F48[] {
|
||||
maxSwapFn: (HealthCache) => I80F48,
|
||||
): number[] {
|
||||
const clonedHc: HealthCache = _.cloneDeep(hc);
|
||||
|
||||
const sourcePrice = clonedHc.tokenInfos[source].prices;
|
||||
|
@ -435,278 +436,428 @@ describe('Health Cache', () => {
|
|||
const swapPrice = I80F48.fromNumber(priceFactor)
|
||||
.mul(sourcePrice.oracle)
|
||||
.div(targetPrice.oracle);
|
||||
const sourceAmount = clonedHc.getMaxSourceForTokenSwap(
|
||||
const sourceAmount = clonedHc.getMaxSwapSourceForHealthFn(
|
||||
banks[source],
|
||||
banks[target],
|
||||
swapPrice,
|
||||
I80F48.fromNumber(ratio),
|
||||
I80F48.fromNumber(minValue),
|
||||
maxSwapFn,
|
||||
);
|
||||
|
||||
// adjust token balance
|
||||
clonedHc.tokenInfos[source].balanceNative.isub(sourceAmount);
|
||||
clonedHc.tokenInfos[target].balanceNative.iadd(
|
||||
sourceAmount.mul(swapPrice),
|
||||
);
|
||||
function valueForAmount(amount: I80F48) {
|
||||
// adjust token balance
|
||||
const clonedHcClone: HealthCache = _.cloneDeep(clonedHc);
|
||||
clonedHc.tokenInfos[source].balanceNative.isub(amount);
|
||||
clonedHc.tokenInfos[target].balanceNative.iadd(amount.mul(swapPrice));
|
||||
return maxSwapFn(clonedHcClone);
|
||||
}
|
||||
|
||||
return [sourceAmount, clonedHc.healthRatio(HealthType.init)];
|
||||
return [
|
||||
sourceAmount.toNumber(),
|
||||
valueForAmount(sourceAmount).toNumber(),
|
||||
valueForAmount(sourceAmount.sub(ONE_I80F48())).toNumber(),
|
||||
valueForAmount(sourceAmount.add(ONE_I80F48())).toNumber(),
|
||||
];
|
||||
}
|
||||
|
||||
function checkMaxSwapResult(
|
||||
hc: HealthCache,
|
||||
source: TokenIndex,
|
||||
target: TokenIndex,
|
||||
ratio: number,
|
||||
minValue: number,
|
||||
priceFactor: number,
|
||||
maxSwapFn: (HealthCache) => I80F48,
|
||||
): void {
|
||||
const [sourceAmount, actualRatio] = findMaxSwapActual(
|
||||
hc,
|
||||
source,
|
||||
target,
|
||||
ratio,
|
||||
priceFactor,
|
||||
);
|
||||
const [sourceAmount, actualValue, minusValue, plusValue] =
|
||||
findMaxSwapActual(hc, source, target, minValue, priceFactor, maxSwapFn);
|
||||
console.log(
|
||||
` -- checking ${source} to ${target} for priceFactor: ${priceFactor}, target ratio ${ratio}: actual ratio: ${actualRatio}, amount: ${sourceAmount}`,
|
||||
` -- checking ${source} to ${target} for priceFactor: ${priceFactor}, target: ${minValue} actual: ${minusValue}/${actualValue}/${plusValue}, amount: ${sourceAmount}`,
|
||||
);
|
||||
expect(Math.abs(actualRatio.toNumber() - ratio)).lessThan(1);
|
||||
if (actualValue < minValue) {
|
||||
// check that swapping more would decrease the ratio!
|
||||
expect(plusValue < actualValue);
|
||||
} else {
|
||||
expect(actualValue >= minValue);
|
||||
// either we're within tolerance of the target, or swapping 1 more would
|
||||
// bring us below the target
|
||||
expect(actualValue < minValue + 1 || plusValue < minValue);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
console.log(' - test 0');
|
||||
// adjust by usdc
|
||||
const clonedHc = _.cloneDeep(hc);
|
||||
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||
);
|
||||
function maxSwapFnRatio(hc: HealthCache): I80F48 {
|
||||
return hc.healthRatio(HealthType.init);
|
||||
}
|
||||
|
||||
for (const priceFactor of [0.1, 0.9, 1.1]) {
|
||||
for (const target of _.range(1, 100, 1)) {
|
||||
checkMaxSwapResult(
|
||||
function maxSwapFn(hc: HealthCache): I80F48 {
|
||||
return hc.health(HealthType.init);
|
||||
}
|
||||
|
||||
for (const fn of [maxSwapFn, maxSwapFnRatio]) {
|
||||
{
|
||||
console.log(' - test 0');
|
||||
// adjust by usdc
|
||||
const clonedHc = _.cloneDeep(hc);
|
||||
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||
);
|
||||
|
||||
for (const priceFactor of [0.1, 0.9, 1.1]) {
|
||||
for (const target of _.range(1, 100, 1)) {
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
0 as TokenIndex,
|
||||
1 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
fn,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
1 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
fn,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
0 as TokenIndex,
|
||||
2 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
fn,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// At this unlikely price it's healthy to swap infinitely
|
||||
expect(function () {
|
||||
findMaxSwapActual(
|
||||
clonedHc,
|
||||
0 as TokenIndex,
|
||||
1 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
1 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
0 as TokenIndex,
|
||||
2 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
50.0,
|
||||
1.5,
|
||||
fn,
|
||||
);
|
||||
}).to.throw('Number out of range');
|
||||
}
|
||||
|
||||
{
|
||||
console.log(' - test 1');
|
||||
const clonedHc = _.cloneDeep(hc);
|
||||
// adjust by usdc
|
||||
clonedHc.tokenInfos[0].balanceNative.iadd(
|
||||
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||
);
|
||||
|
||||
for (const priceFactor of [0.1, 0.9, 1.1]) {
|
||||
for (const target of _.range(1, 100, 1)) {
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
0 as TokenIndex,
|
||||
1 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
fn,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
1 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
fn,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
0 as TokenIndex,
|
||||
2 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
fn,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
2 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
fn,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// At this unlikely price it's healthy to swap infinitely
|
||||
expect(function () {
|
||||
findMaxSwapActual(
|
||||
{
|
||||
console.log(' - test 2');
|
||||
const clonedHc = _.cloneDeep(hc);
|
||||
// adjust by usdc
|
||||
clonedHc.tokenInfos[0].balanceNative.iadd(
|
||||
I80F48.fromNumber(-50).div(clonedHc.tokenInfos[0].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||
);
|
||||
// possible even though the init ratio is <100
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
1 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
100,
|
||||
1,
|
||||
|
||||
maxSwapFn,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
console.log(' - test 3');
|
||||
const clonedHc = _.cloneDeep(hc);
|
||||
// adjust by usdc
|
||||
clonedHc.tokenInfos[0].balanceNative.iadd(
|
||||
I80F48.fromNumber(-30).div(clonedHc.tokenInfos[0].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[2].balanceNative.iadd(
|
||||
I80F48.fromNumber(-30).div(clonedHc.tokenInfos[2].prices.oracle),
|
||||
);
|
||||
|
||||
// swapping with a high ratio advises paying back all liabs
|
||||
// and then swapping even more because increasing assets in 0 has better asset weight
|
||||
const initRatio = clonedHc.healthRatio(HealthType.init);
|
||||
const [amount, actualRatio] = findMaxSwapActual(
|
||||
clonedHc,
|
||||
1 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
100,
|
||||
1,
|
||||
maxSwapFn,
|
||||
);
|
||||
expect(actualRatio / 2.0 > initRatio);
|
||||
expect(amount - 100 / 3).lessThan(1);
|
||||
}
|
||||
|
||||
{
|
||||
console.log(' - test 4');
|
||||
const clonedHc = _.cloneDeep(hc);
|
||||
// adjust by usdc
|
||||
clonedHc.tokenInfos[0].balanceNative.iadd(
|
||||
I80F48.fromNumber(100).div(clonedHc.tokenInfos[0].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||
I80F48.fromNumber(-2).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[2].balanceNative.iadd(
|
||||
I80F48.fromNumber(-65).div(clonedHc.tokenInfos[2].prices.oracle),
|
||||
);
|
||||
|
||||
const initRatio = clonedHc.healthRatio(HealthType.init);
|
||||
expect(initRatio.toNumber()).greaterThan(3);
|
||||
expect(initRatio.toNumber()).lessThan(4);
|
||||
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
0 as TokenIndex,
|
||||
1 as TokenIndex,
|
||||
50.0,
|
||||
1.5,
|
||||
);
|
||||
}).to.throw('Number out of range');
|
||||
}
|
||||
|
||||
{
|
||||
console.log(' - test 1');
|
||||
const clonedHc = _.cloneDeep(hc);
|
||||
// adjust by usdc
|
||||
clonedHc.tokenInfos[0].balanceNative.iadd(
|
||||
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||
);
|
||||
|
||||
for (const priceFactor of [0.1, 0.9, 1.1]) {
|
||||
for (const target of _.range(1, 100, 1)) {
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
0 as TokenIndex,
|
||||
1 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
1 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
0 as TokenIndex,
|
||||
2 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
2 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
console.log(' - test 2');
|
||||
const clonedHc = _.cloneDeep(hc);
|
||||
// adjust by usdc
|
||||
clonedHc.tokenInfos[0].balanceNative.iadd(
|
||||
I80F48.fromNumber(-50).div(clonedHc.tokenInfos[0].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||
);
|
||||
// possible even though the init ratio is <100
|
||||
checkMaxSwapResult(clonedHc, 1 as TokenIndex, 0 as TokenIndex, 100, 1);
|
||||
}
|
||||
|
||||
{
|
||||
console.log(' - test 3');
|
||||
const clonedHc = _.cloneDeep(hc);
|
||||
// adjust by usdc
|
||||
clonedHc.tokenInfos[0].balanceNative.iadd(
|
||||
I80F48.fromNumber(-30).div(clonedHc.tokenInfos[0].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[2].balanceNative.iadd(
|
||||
I80F48.fromNumber(-30).div(clonedHc.tokenInfos[2].prices.oracle),
|
||||
);
|
||||
|
||||
// swapping with a high ratio advises paying back all liabs
|
||||
// and then swapping even more because increasing assets in 0 has better asset weight
|
||||
const initRatio = clonedHc.healthRatio(HealthType.init);
|
||||
const [amount, actualRatio] = findMaxSwapActual(
|
||||
clonedHc,
|
||||
1 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
100,
|
||||
1,
|
||||
);
|
||||
expect(actualRatio.div(I80F48.fromNumber(2)).toNumber()).greaterThan(
|
||||
initRatio.toNumber(),
|
||||
);
|
||||
expect(amount.toNumber() - 100 / 3).lessThan(1);
|
||||
}
|
||||
|
||||
{
|
||||
console.log(' - test 4');
|
||||
const clonedHc = _.cloneDeep(hc);
|
||||
// adjust by usdc
|
||||
clonedHc.tokenInfos[0].balanceNative.iadd(
|
||||
I80F48.fromNumber(100).div(clonedHc.tokenInfos[0].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||
I80F48.fromNumber(-2).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[2].balanceNative.iadd(
|
||||
I80F48.fromNumber(-65).div(clonedHc.tokenInfos[2].prices.oracle),
|
||||
);
|
||||
|
||||
const initRatio = clonedHc.healthRatio(HealthType.init);
|
||||
expect(initRatio.toNumber()).greaterThan(3);
|
||||
expect(initRatio.toNumber()).lessThan(4);
|
||||
|
||||
checkMaxSwapResult(clonedHc, 0 as TokenIndex, 1 as TokenIndex, 1, 1);
|
||||
checkMaxSwapResult(clonedHc, 0 as TokenIndex, 1 as TokenIndex, 3, 1);
|
||||
checkMaxSwapResult(clonedHc, 0 as TokenIndex, 1 as TokenIndex, 4, 1);
|
||||
}
|
||||
|
||||
// TODO test 5
|
||||
|
||||
{
|
||||
console.log(' - test 6');
|
||||
const clonedHc = _.cloneDeep(hc);
|
||||
clonedHc.serum3Infos = [
|
||||
new Serum3Info(
|
||||
I80F48.fromNumber(30 / 3),
|
||||
I80F48.fromNumber(30 / 2),
|
||||
1,
|
||||
0,
|
||||
0 as MarketIndex,
|
||||
),
|
||||
];
|
||||
1,
|
||||
maxSwapFn,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
0 as TokenIndex,
|
||||
1 as TokenIndex,
|
||||
3,
|
||||
1,
|
||||
maxSwapFn,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
0 as TokenIndex,
|
||||
1 as TokenIndex,
|
||||
4,
|
||||
1,
|
||||
maxSwapFn,
|
||||
);
|
||||
}
|
||||
|
||||
// adjust by usdc
|
||||
clonedHc.tokenInfos[0].balanceNative.iadd(
|
||||
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||
I80F48.fromNumber(-40).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[2].balanceNative.iadd(
|
||||
I80F48.fromNumber(120).div(clonedHc.tokenInfos[2].prices.oracle),
|
||||
);
|
||||
// TODO test 5
|
||||
|
||||
for (const priceFactor of [
|
||||
// 0.9,
|
||||
{
|
||||
console.log(' - test 6');
|
||||
const clonedHc = _.cloneDeep(hc);
|
||||
clonedHc.serum3Infos = [
|
||||
new Serum3Info(
|
||||
I80F48.fromNumber(30 / 3),
|
||||
I80F48.fromNumber(30 / 2),
|
||||
1,
|
||||
0,
|
||||
0 as MarketIndex,
|
||||
),
|
||||
];
|
||||
|
||||
1.1,
|
||||
]) {
|
||||
for (const target of _.range(1, 100, 1)) {
|
||||
checkMaxSwapResult(
|
||||
// adjust by usdc
|
||||
clonedHc.tokenInfos[0].balanceNative.iadd(
|
||||
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||
I80F48.fromNumber(-40).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[2].balanceNative.iadd(
|
||||
I80F48.fromNumber(120).div(clonedHc.tokenInfos[2].prices.oracle),
|
||||
);
|
||||
|
||||
for (const priceFactor of [0.9, 1.1]) {
|
||||
for (const target of _.range(1, 100, 1)) {
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
0 as TokenIndex,
|
||||
1 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
fn,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
1 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
fn,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
0 as TokenIndex,
|
||||
2 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
fn,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
1 as TokenIndex,
|
||||
2 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
fn,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
2 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
fn,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
2 as TokenIndex,
|
||||
1 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
fn,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// check starting with negative health but swapping can make it positive
|
||||
console.log(' - test 7');
|
||||
const clonedHc = _.cloneDeep(hc);
|
||||
|
||||
// adjust by usdc
|
||||
clonedHc.tokenInfos[0].balanceNative.iadd(
|
||||
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||
I80F48.fromNumber(20).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||
);
|
||||
expect(clonedHc.health(HealthType.init) < 0);
|
||||
|
||||
for (const priceFactor of [0.9, 1.1]) {
|
||||
for (const target of _.range(1, 100, 1)) {
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
1 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
fn,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// check starting with negative health but swapping can't make it positive
|
||||
console.log(' - test 8');
|
||||
const clonedHc = _.cloneDeep(hc);
|
||||
|
||||
// adjust by usdc
|
||||
clonedHc.tokenInfos[0].balanceNative.iadd(
|
||||
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||
I80F48.fromNumber(10).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||
);
|
||||
expect(clonedHc.health(HealthType.init) < 0);
|
||||
|
||||
for (const priceFactor of [0.9, 1.1]) {
|
||||
for (const target of _.range(1, 100, 1)) {
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
1 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
fn,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// swap some assets into a zero-asset-weight token
|
||||
console.log(' - test 9');
|
||||
const clonedHc = _.cloneDeep(hc);
|
||||
|
||||
// adjust by usdc
|
||||
clonedHc.tokenInfos[0].balanceNative.iadd(
|
||||
I80F48.fromNumber(10).div(clonedHc.tokenInfos[0].prices.oracle),
|
||||
);
|
||||
clonedHc.tokenInfos[1].initAssetWeight = ZERO_I80F48();
|
||||
expect(
|
||||
findMaxSwapActual(
|
||||
clonedHc,
|
||||
0 as TokenIndex,
|
||||
1 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
1 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
0 as TokenIndex,
|
||||
2 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
1 as TokenIndex,
|
||||
2 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
2 as TokenIndex,
|
||||
0 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
);
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
2 as TokenIndex,
|
||||
1 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
);
|
||||
1,
|
||||
1,
|
||||
maxSwapFn,
|
||||
)[0] > 0,
|
||||
);
|
||||
|
||||
for (const priceFactor of [0.9, 1.1]) {
|
||||
for (const target of _.range(1, 100, 1)) {
|
||||
checkMaxSwapResult(
|
||||
clonedHc,
|
||||
0 as TokenIndex,
|
||||
1 as TokenIndex,
|
||||
target,
|
||||
priceFactor,
|
||||
fn,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
ONE_I80F48,
|
||||
ZERO_I80F48,
|
||||
} from '../numbers/I80F48';
|
||||
import { toNativeI80F48ForQuote } from '../utils';
|
||||
import { Bank, BankForHealth, TokenIndex } from './bank';
|
||||
import { Group } from './group';
|
||||
|
||||
|
@ -569,6 +570,72 @@ export class HealthCache {
|
|||
throw new Error('Could not find amount that led to health ratio <=0');
|
||||
}
|
||||
|
||||
/// This is not a generic function. It assumes there is a unique maximum between left and right.
|
||||
private static findMaximum(
|
||||
left: I80F48,
|
||||
right: I80F48,
|
||||
minStep: I80F48,
|
||||
fun: (I80F48) => I80F48,
|
||||
): I80F48[] {
|
||||
const half = I80F48.fromNumber(0.5);
|
||||
let mid = half.mul(left.add(right));
|
||||
let leftValue = fun(left);
|
||||
let rightValue = fun(right);
|
||||
let midValue = fun(mid);
|
||||
while (right.sub(left).gt(minStep)) {
|
||||
if (leftValue.gte(midValue)) {
|
||||
// max must be between left and mid
|
||||
right = mid;
|
||||
rightValue = midValue;
|
||||
mid = half.mul(left.add(mid));
|
||||
midValue = fun(mid);
|
||||
} else if (midValue.lte(rightValue)) {
|
||||
// max must be between mid and right
|
||||
left = mid;
|
||||
leftValue = midValue;
|
||||
mid = half.mul(mid.add(right));
|
||||
midValue = fun(mid);
|
||||
} else {
|
||||
// mid is larger than both left and right, max could be on either side
|
||||
const leftmid = half.mul(left.add(mid));
|
||||
const leftMidValue = fun(leftmid);
|
||||
if (leftMidValue.gte(midValue)) {
|
||||
// max between left and mid
|
||||
right = mid;
|
||||
rightValue = midValue;
|
||||
mid = leftmid;
|
||||
midValue = leftMidValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
const rightmid = half.mul(mid.add(right));
|
||||
const rightMidValue = fun(rightmid);
|
||||
if (rightMidValue.gte(midValue)) {
|
||||
// max between mid and right
|
||||
left = mid;
|
||||
leftValue = midValue;
|
||||
mid = rightmid;
|
||||
midValue = rightMidValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
// max between leftmid and rightmid
|
||||
left = leftmid;
|
||||
leftValue = leftMidValue;
|
||||
right = rightmid;
|
||||
rightValue = rightMidValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (leftValue.gte(midValue)) {
|
||||
return [left, leftValue];
|
||||
} else if (midValue.gte(rightValue)) {
|
||||
return [mid, midValue];
|
||||
} else {
|
||||
return [right, rightValue];
|
||||
}
|
||||
}
|
||||
|
||||
private static binaryApproximationSearch(
|
||||
left: I80F48,
|
||||
leftValue: I80F48,
|
||||
|
@ -577,10 +644,14 @@ export class HealthCache {
|
|||
minStep: I80F48,
|
||||
fun: (I80F48) => I80F48,
|
||||
): I80F48 {
|
||||
const maxIterations = 20;
|
||||
const maxIterations = 40;
|
||||
const targetError = I80F48.fromNumber(0.1);
|
||||
const rightValue = fun(right);
|
||||
|
||||
// console.log(
|
||||
// `binaryApproximationSearch left ${left}, leftValue ${leftValue}, right ${right}, rightValue ${rightValue}, targetValue ${targetValue}`,
|
||||
// );
|
||||
|
||||
if (
|
||||
(leftValue.sub(targetValue).isPos() &&
|
||||
rightValue.sub(targetValue).isPos()) ||
|
||||
|
@ -592,19 +663,22 @@ export class HealthCache {
|
|||
);
|
||||
}
|
||||
|
||||
let newAmount;
|
||||
let newAmount, newAmountValue;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const key of Array(maxIterations).fill(0).keys()) {
|
||||
if (right.sub(left).abs().lt(minStep)) {
|
||||
return left;
|
||||
}
|
||||
newAmount = left.add(right).mul(I80F48.fromNumber(0.5));
|
||||
const newAmountRatio = fun(newAmount);
|
||||
const error = newAmountRatio.sub(targetValue);
|
||||
newAmountValue = fun(newAmount);
|
||||
// console.log(
|
||||
// ` - left ${left}, right ${right}, newAmount ${newAmount}, newAmountValue ${newAmountValue}, targetValue ${targetValue}`,
|
||||
// );
|
||||
const error = newAmountValue.sub(targetValue);
|
||||
if (error.isPos() && error.lt(targetError)) {
|
||||
return newAmount;
|
||||
}
|
||||
if (newAmountRatio.gt(targetValue) != rightValue.gt(targetValue)) {
|
||||
if (newAmountValue.gt(targetValue) != rightValue.gt(targetValue)) {
|
||||
left = newAmount;
|
||||
} else {
|
||||
right = newAmount;
|
||||
|
@ -612,16 +686,74 @@ export class HealthCache {
|
|||
}
|
||||
|
||||
console.error(
|
||||
`Unable to get targetRatio within ${maxIterations} iterations`,
|
||||
`Unable to get targetValue within ${maxIterations} iterations, newAmount ${newAmount}, newAmountValue ${newAmountValue}, target ${targetValue}`,
|
||||
);
|
||||
|
||||
return newAmount;
|
||||
}
|
||||
|
||||
getMaxSourceForTokenSwap(
|
||||
getMaxSwapSource(
|
||||
sourceBank: BankForHealth,
|
||||
targetBank: BankForHealth,
|
||||
price: I80F48,
|
||||
): I80F48 {
|
||||
const health = this.health(HealthType.init);
|
||||
if (health.isNeg()) {
|
||||
return this.getMaxSwapSourceForHealth(
|
||||
sourceBank,
|
||||
targetBank,
|
||||
price,
|
||||
toNativeI80F48ForQuote(1), // target 1 ui usd worth health
|
||||
);
|
||||
}
|
||||
return this.getMaxSwapSourceForHealthRatio(
|
||||
sourceBank,
|
||||
targetBank,
|
||||
price,
|
||||
I80F48.fromNumber(2), // target 2% health
|
||||
);
|
||||
}
|
||||
|
||||
getMaxSwapSourceForHealthRatio(
|
||||
sourceBank: BankForHealth,
|
||||
targetBank: BankForHealth,
|
||||
price: I80F48,
|
||||
minRatio: I80F48,
|
||||
): I80F48 {
|
||||
return this.getMaxSwapSourceForHealthFn(
|
||||
sourceBank,
|
||||
targetBank,
|
||||
price,
|
||||
minRatio,
|
||||
function (hc: HealthCache): I80F48 {
|
||||
return hc.healthRatio(HealthType.init);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
getMaxSwapSourceForHealth(
|
||||
sourceBank: BankForHealth,
|
||||
targetBank: BankForHealth,
|
||||
price: I80F48,
|
||||
minHealth: I80F48,
|
||||
): I80F48 {
|
||||
return this.getMaxSwapSourceForHealthFn(
|
||||
sourceBank,
|
||||
targetBank,
|
||||
price,
|
||||
minHealth,
|
||||
function (hc: HealthCache): I80F48 {
|
||||
return hc.health(HealthType.init);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
getMaxSwapSourceForHealthFn(
|
||||
sourceBank: BankForHealth,
|
||||
targetBank: BankForHealth,
|
||||
price: I80F48,
|
||||
minFnValue: I80F48,
|
||||
targetFn: (cache) => I80F48,
|
||||
): I80F48 {
|
||||
if (
|
||||
sourceBank.initLiabWeight
|
||||
|
@ -632,27 +764,29 @@ export class HealthCache {
|
|||
return ZERO_I80F48();
|
||||
}
|
||||
|
||||
// The health_ratio is a nonlinear based on swap amount.
|
||||
// The health and health_ratio are nonlinear based on swap amount.
|
||||
// For large swap amounts the slope is guaranteed to be negative, but small amounts
|
||||
// can have positive slope (e.g. using source deposits to pay back target borrows).
|
||||
//
|
||||
// That means:
|
||||
// - even if the initial ratio is < minRatio it can be useful to swap to *increase* health
|
||||
// - be careful about finding the minRatio point: the function isn't convex
|
||||
// - even if the initial value is < minRatio it can be useful to swap to *increase* health
|
||||
// - even if initial value is < 0, swapping can increase health (maybe above 0)
|
||||
// - be careful about finding the minFnValue: the function isn't convex
|
||||
|
||||
const initialRatio = this.healthRatio(HealthType.init);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const initialHealth = this.health(HealthType.init);
|
||||
if (initialRatio.lte(ZERO_I80F48())) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
|
||||
const healthCacheClone: HealthCache = _.cloneDeep(this);
|
||||
const sourceIndex = healthCacheClone.getOrCreateTokenInfoIndex(sourceBank);
|
||||
const targetIndex = healthCacheClone.getOrCreateTokenInfoIndex(targetBank);
|
||||
|
||||
const source = healthCacheClone.tokenInfos[sourceIndex];
|
||||
const target = healthCacheClone.tokenInfos[targetIndex];
|
||||
|
||||
const res = healthCacheClone.computeSerum3Reservations(HealthType.init);
|
||||
const sourceReserved = res.tokenMaxReserved[sourceIndex];
|
||||
const targetReserved = res.tokenMaxReserved[targetIndex];
|
||||
|
||||
// If the price is sufficiently good, then health will just increase from swapping:
|
||||
// once we've swapped enough, swapping x reduces health by x * source_liab_weight and
|
||||
// increases it by x * target_asset_weight * price_factor.
|
||||
|
@ -688,126 +822,78 @@ export class HealthCache {
|
|||
return adjustedCache;
|
||||
}
|
||||
|
||||
function healthRatioAfterSwap(amount: I80F48): I80F48 {
|
||||
return cacheAfterSwap(amount).healthRatio(HealthType.init);
|
||||
function fnValueAfterSwap(amount: I80F48): I80F48 {
|
||||
return targetFn(cacheAfterSwap(amount));
|
||||
}
|
||||
|
||||
function healthAfterSwap(amount: I80F48): I80F48 {
|
||||
return cacheAfterSwap(amount).health(HealthType.init);
|
||||
}
|
||||
|
||||
// There are two key slope changes: Assume source.balance > 0 and target.balance < 0.
|
||||
// The function we're looking at has a unique maximum.
|
||||
//
|
||||
// If we discount serum3 reservations, there are two key slope changes:
|
||||
// Assume source.balance > 0 and target.balance < 0.
|
||||
// When these values flip sign, the health slope decreases, but could still be positive.
|
||||
// After point1 it's definitely negative (due to finalHealthSlope check above).
|
||||
// The maximum health ratio will be at 0 or at one of these points (ignoring serum3 effects).
|
||||
const sourceForZeroTargetBalance = target.balanceNative.neg().div(price);
|
||||
const point0Amount = source.balanceNative
|
||||
.min(sourceForZeroTargetBalance)
|
||||
.max(ZERO_I80F48());
|
||||
const point1Amount = source.balanceNative
|
||||
.max(sourceForZeroTargetBalance)
|
||||
.max(ZERO_I80F48());
|
||||
const cache0 = cacheAfterSwap(point0Amount);
|
||||
const point0Ratio = cache0.healthRatio(HealthType.init);
|
||||
const point0Health = cache0.health(HealthType.init);
|
||||
const cache1 = cacheAfterSwap(point1Amount);
|
||||
const point1Ratio = cache1.healthRatio(HealthType.init);
|
||||
const point1Health = cache1.health(HealthType.init);
|
||||
//
|
||||
// The first thing we do is to find this maximum.
|
||||
|
||||
// The largest amount that the maximum could be at
|
||||
const rightmost = source.balanceNative
|
||||
.abs()
|
||||
.add(sourceReserved)
|
||||
.max(target.balanceNative.abs().add(targetReserved).div(price));
|
||||
const [amountForMaxValue, maxValue] = HealthCache.findMaximum(
|
||||
ZERO_I80F48(),
|
||||
rightmost,
|
||||
I80F48.fromNumber(0.1),
|
||||
fnValueAfterSwap,
|
||||
);
|
||||
|
||||
if (maxValue.lte(minFnValue)) {
|
||||
// We cannot reach min_ratio, just return the max
|
||||
return amountForMaxValue;
|
||||
}
|
||||
|
||||
let amount: I80F48;
|
||||
|
||||
if (
|
||||
initialRatio.lte(minRatio) &&
|
||||
point0Ratio.lt(minRatio) &&
|
||||
point1Ratio.lt(minRatio)
|
||||
) {
|
||||
// If we have to stay below the target ratio, pick the highest one
|
||||
if (point0Ratio.gt(initialRatio)) {
|
||||
if (point1Ratio.gt(point0Ratio)) {
|
||||
amount = point1Amount;
|
||||
} else {
|
||||
amount = point0Amount;
|
||||
}
|
||||
} else if (point1Ratio.gt(initialRatio)) {
|
||||
amount = point1Amount;
|
||||
} else {
|
||||
amount = ZERO_I80F48();
|
||||
}
|
||||
} else if (point1Ratio.gte(minRatio)) {
|
||||
// If point1Ratio is still bigger than minRatio, the target amount must be >point1Amount
|
||||
// search to the right of point1Amount: but how far?
|
||||
// At point1, source.balance < 0 and target.balance > 0, so use a simple estimation for
|
||||
// zero health: health - source_liab_weight * a + target_asset_weight * a * priceFactor = 0.
|
||||
// where a is the source token native amount.
|
||||
// Note that this is just an estimate. Swapping can increase the amount that serum3
|
||||
// reserved contributions offset, moving the actual zero point further to the right.
|
||||
if (point1Health.lte(ZERO_I80F48())) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
const zeroHealthEstimate = point1Amount.sub(
|
||||
point1Health.div(finalHealthSlope),
|
||||
);
|
||||
const zeroHealthEstimateRatio = healthRatioAfterSwap(zeroHealthEstimate);
|
||||
|
||||
// console.log(`getMaxSourceForTokenSwap`);
|
||||
// console.log(` - finalHealthSlope ${finalHealthSlope.toLocaleString()}`);
|
||||
// console.log(` - minRatio ${minRatio.toLocaleString()}`);
|
||||
// console.log(` - point0Amount ${point0Amount.toLocaleString()}`);
|
||||
// console.log(` - point0Health ${point0Health.toLocaleString()}`);
|
||||
// console.log(` - point0Ratio ${point0Ratio.toLocaleString()}`);
|
||||
// console.log(` - point1Amount ${point1Amount.toLocaleString()}`);
|
||||
// console.log(` - point1Health ${point1Health.toLocaleString()}`);
|
||||
// console.log(` - point1Ratio ${point1Ratio.toLocaleString()}`);
|
||||
// console.log(
|
||||
// ` - zeroHealthEstimate ${zeroHealthEstimate.toLocaleString()}`,
|
||||
// );
|
||||
// console.log(
|
||||
// ` - zeroHealthEstimateRatio ${zeroHealthEstimateRatio.toLocaleString()}`,
|
||||
// );
|
||||
|
||||
const rightBound = HealthCache.scanRightUntilLessThan(
|
||||
zeroHealthEstimate,
|
||||
minRatio,
|
||||
healthRatioAfterSwap,
|
||||
);
|
||||
if (rightBound.eq(zeroHealthEstimate)) {
|
||||
amount = HealthCache.binaryApproximationSearch(
|
||||
point1Amount,
|
||||
point1Ratio,
|
||||
rightBound,
|
||||
minRatio,
|
||||
ZERO_I80F48(),
|
||||
healthRatioAfterSwap,
|
||||
);
|
||||
} else {
|
||||
amount = HealthCache.binaryApproximationSearch(
|
||||
zeroHealthEstimate,
|
||||
healthRatioAfterSwap(zeroHealthEstimate),
|
||||
rightBound,
|
||||
minRatio,
|
||||
ZERO_I80F48(),
|
||||
healthRatioAfterSwap,
|
||||
);
|
||||
}
|
||||
} else if (point0Ratio.gte(minRatio)) {
|
||||
// Must be between point0Amount and point1Amount.
|
||||
// Now max_value is bigger than minFnValue, the target amount must be >amountForMaxValue.
|
||||
// Search to the right of amountForMaxValue: but how far?
|
||||
// Use a simple estimation for the amount that would lead to zero health:
|
||||
// health
|
||||
// - source_liab_weight * source_liab_price * a
|
||||
// + target_asset_weight * target_asset_price * price * a = 0.
|
||||
// where a is the source token native amount.
|
||||
// Note that this is just an estimate. Swapping can increase the amount that serum3
|
||||
// reserved contributions offset, moving the actual zero point further to the right.
|
||||
const healthAtMaxValue = cacheAfterSwap(amountForMaxValue).health(
|
||||
HealthType.init,
|
||||
);
|
||||
if (healthAtMaxValue.lte(ZERO_I80F48())) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
const zeroHealthEstimate = amountForMaxValue.sub(
|
||||
healthAtMaxValue.div(finalHealthSlope),
|
||||
);
|
||||
const rightBound = HealthCache.scanRightUntilLessThan(
|
||||
zeroHealthEstimate,
|
||||
minFnValue,
|
||||
fnValueAfterSwap,
|
||||
);
|
||||
if (rightBound.eq(zeroHealthEstimate)) {
|
||||
amount = HealthCache.binaryApproximationSearch(
|
||||
point0Amount,
|
||||
point0Ratio,
|
||||
point1Amount,
|
||||
minRatio,
|
||||
ZERO_I80F48(),
|
||||
healthRatioAfterSwap,
|
||||
amountForMaxValue,
|
||||
maxValue,
|
||||
rightBound,
|
||||
minFnValue,
|
||||
I80F48.fromNumber(0.1),
|
||||
fnValueAfterSwap,
|
||||
);
|
||||
} else {
|
||||
// Must be between 0 and point0_amount
|
||||
amount = HealthCache.binaryApproximationSearch(
|
||||
ZERO_I80F48(),
|
||||
initialRatio,
|
||||
point0Amount,
|
||||
minRatio,
|
||||
ZERO_I80F48(),
|
||||
healthRatioAfterSwap,
|
||||
zeroHealthEstimate,
|
||||
fnValueAfterSwap(zeroHealthEstimate),
|
||||
rightBound,
|
||||
minFnValue,
|
||||
I80F48.fromNumber(0.1),
|
||||
fnValueAfterSwap,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1013,7 +1099,7 @@ export class HealthCache {
|
|||
|
||||
// Need to figure out how many lots to trade to reach zero health (zero_health_amount).
|
||||
// We do this by looking at the starting health and the health slope per
|
||||
// traded base lot (final_health_slope).
|
||||
// traded base lot (finalHealthSlope).
|
||||
const startCache = cacheAfterTrade(new BN(case1Start.toNumber()));
|
||||
const startHealth = startCache.health(HealthType.init);
|
||||
if (startHealth.lte(ZERO_I80F48())) {
|
||||
|
|
|
@ -505,16 +505,13 @@ export class MangoAccount {
|
|||
|
||||
/**
|
||||
* The max amount of given source ui token you can swap to a target token.
|
||||
* Price is simply the source tokens price divided by target tokens price,
|
||||
* it is supposed to give an indication of how many source tokens can be traded for target tokens,
|
||||
* it can optionally contain information on slippage and fees.
|
||||
* @returns max amount of given source ui token you can swap to a target token, in ui token
|
||||
*/
|
||||
getMaxSourceUiForTokenSwap(
|
||||
group: Group,
|
||||
sourceMintPk: PublicKey,
|
||||
targetMintPk: PublicKey,
|
||||
price: number,
|
||||
slippageAndFeesFactor = 1,
|
||||
): number {
|
||||
if (sourceMintPk.equals(targetMintPk)) {
|
||||
return 0;
|
||||
|
@ -522,11 +519,14 @@ export class MangoAccount {
|
|||
const s = group.getFirstBankByMint(sourceMintPk);
|
||||
const t = group.getFirstBankByMint(targetMintPk);
|
||||
const hc = HealthCache.fromMangoAccount(group, this);
|
||||
const maxSource = hc.getMaxSourceForTokenSwap(
|
||||
const maxSource = hc.getMaxSwapSource(
|
||||
s,
|
||||
t,
|
||||
I80F48.fromNumber(price * Math.pow(10, t.mintDecimals - s.mintDecimals)),
|
||||
I80F48.fromNumber(2), // target 2% health
|
||||
I80F48.fromNumber(
|
||||
slippageAndFeesFactor *
|
||||
((s.uiPrice / t.uiPrice) *
|
||||
Math.pow(10, t.mintDecimals - s.mintDecimals)),
|
||||
),
|
||||
);
|
||||
maxSource.idiv(
|
||||
ONE_I80F48().add(
|
||||
|
@ -831,13 +831,12 @@ export class MangoAccount {
|
|||
public getMaxQuoteForPerpBidUi(
|
||||
group: Group,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
price: number,
|
||||
): number {
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
const hc = HealthCache.fromMangoAccount(group, this);
|
||||
const baseLots = hc.getMaxPerpForHealthRatio(
|
||||
perpMarket,
|
||||
I80F48.fromNumber(price),
|
||||
I80F48.fromNumber(perpMarket.uiPrice),
|
||||
PerpOrderSide.bid,
|
||||
I80F48.fromNumber(2),
|
||||
);
|
||||
|
@ -859,13 +858,12 @@ export class MangoAccount {
|
|||
public getMaxBaseForPerpAskUi(
|
||||
group: Group,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
price: number,
|
||||
): number {
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
const hc = HealthCache.fromMangoAccount(group, this);
|
||||
const baseLots = hc.getMaxPerpForHealthRatio(
|
||||
perpMarket,
|
||||
I80F48.fromNumber(price),
|
||||
I80F48.fromNumber(perpMarket.uiPrice),
|
||||
PerpOrderSide.ask,
|
||||
I80F48.fromNumber(2),
|
||||
);
|
||||
|
@ -876,7 +874,6 @@ export class MangoAccount {
|
|||
group: Group,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
size: number,
|
||||
price: number,
|
||||
): number {
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
|
||||
|
@ -889,7 +886,7 @@ export class MangoAccount {
|
|||
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
|
||||
PerpOrderSide.bid,
|
||||
perpMarket.uiBaseToLots(size),
|
||||
I80F48.fromNumber(price),
|
||||
I80F48.fromNumber(perpMarket.uiPrice),
|
||||
HealthType.init,
|
||||
)
|
||||
.toNumber();
|
||||
|
@ -899,7 +896,6 @@ export class MangoAccount {
|
|||
group: Group,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
size: number,
|
||||
price: number,
|
||||
): number {
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
|
||||
|
@ -912,7 +908,7 @@ export class MangoAccount {
|
|||
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
|
||||
PerpOrderSide.ask,
|
||||
perpMarket.uiBaseToLots(size),
|
||||
I80F48.fromNumber(price),
|
||||
I80F48.fromNumber(perpMarket.uiPrice),
|
||||
HealthType.init,
|
||||
)
|
||||
.toNumber();
|
||||
|
|
|
@ -764,12 +764,22 @@ export class MangoClient {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this ix doesn't settle liabs, reduce open positions, or withdraw tokens to wallet,
|
||||
* it simply closes the account. To close successfully ensure all positions are closed, or
|
||||
* use forceClose flag
|
||||
* @param group
|
||||
* @param mangoAccount
|
||||
* @param forceClose
|
||||
* @returns
|
||||
*/
|
||||
public async closeMangoAccount(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
forceClose = false,
|
||||
): Promise<TransactionSignature> {
|
||||
const ix = await this.program.methods
|
||||
.accountClose()
|
||||
.accountClose(forceClose)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
account: mangoAccount.publicKey,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Cluster, Connection, Keypair } from '@solana/web3.js';
|
||||
import { expect } from 'chai';
|
||||
import fs from 'fs';
|
||||
import { Group } from '../accounts/group';
|
||||
import { HealthCache } from '../accounts/healthCache';
|
||||
|
@ -26,7 +25,11 @@ async function debugUser(
|
|||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
): Promise<void> {
|
||||
console.log(mangoAccount.toString(group));
|
||||
// Log only tokens
|
||||
console.log(mangoAccount.toString(group, true));
|
||||
|
||||
// Turn on, to see serum and perp stuff
|
||||
// console.log(mangoAccount.toString(group));
|
||||
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
|
@ -98,15 +101,18 @@ async function debugUser(
|
|||
|
||||
function getMaxSourceForTokenSwapWrapper(src, tgt): void {
|
||||
// Turn on for debugging specific pairs
|
||||
// if (src != 'DAI' || tgt != 'ETH') return;
|
||||
// if (src != 'USDC' || tgt != 'MNGO') return;
|
||||
|
||||
const maxSourceUi = mangoAccount.getMaxSourceUiForTokenSwap(
|
||||
group,
|
||||
group.banksMapByName.get(src)![0].mint,
|
||||
group.banksMapByName.get(tgt)![0].mint,
|
||||
group.banksMapByName.get(src)![0].uiPrice /
|
||||
group.banksMapByName.get(tgt)![0].uiPrice,
|
||||
);
|
||||
let maxSourceUi;
|
||||
try {
|
||||
maxSourceUi = mangoAccount.getMaxSourceUiForTokenSwap(
|
||||
group,
|
||||
group.banksMapByName.get(src)![0].mint,
|
||||
group.banksMapByName.get(tgt)![0].mint,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(`Error for ${src}->${tgt}, ` + error.toString());
|
||||
}
|
||||
|
||||
const maxSourceWoFees =
|
||||
-maxSourceUi *
|
||||
|
@ -131,10 +137,6 @@ async function debugUser(
|
|||
maxSourceUi.toFixed(3).padStart(10) +
|
||||
`, health ratio after (${sim.toFixed(3).padStart(10)})`,
|
||||
);
|
||||
if (maxSourceUi > 0 && src !== tgt) {
|
||||
expect(sim).gt(2);
|
||||
expect(sim).lt(3);
|
||||
}
|
||||
}
|
||||
for (const srcToken of Array.from(group.banksMapByName.keys()).sort()) {
|
||||
for (const tgtToken of Array.from(group.banksMapByName.keys()).sort()) {
|
||||
|
@ -146,24 +148,20 @@ async function debugUser(
|
|||
const maxQuoteUi = mangoAccount.getMaxQuoteForPerpBidUi(
|
||||
group,
|
||||
perpMarket.perpMarketIndex,
|
||||
perpMarket.uiPrice,
|
||||
);
|
||||
const simMaxQuote = mangoAccount.simHealthRatioWithPerpBidUiChanges(
|
||||
group,
|
||||
perpMarket.perpMarketIndex,
|
||||
maxQuoteUi / perpMarket.uiPrice,
|
||||
perpMarket.uiPrice,
|
||||
);
|
||||
const maxBaseUi = mangoAccount.getMaxBaseForPerpAskUi(
|
||||
group,
|
||||
perpMarket.perpMarketIndex,
|
||||
perpMarket.uiPrice,
|
||||
);
|
||||
const simMaxBase = mangoAccount.simHealthRatioWithPerpAskUiChanges(
|
||||
group,
|
||||
perpMarket.perpMarketIndex,
|
||||
maxBaseUi,
|
||||
perpMarket.uiPrice,
|
||||
);
|
||||
console.log(
|
||||
`getMaxPerp ${perpMarket.name.padStart(
|
||||
|
@ -178,10 +176,6 @@ async function debugUser(
|
|||
.toFixed(3)
|
||||
.padStart(10)})`,
|
||||
);
|
||||
if (maxQuoteUi > 0) {
|
||||
expect(simMaxQuote).gt(2);
|
||||
expect(simMaxQuote).lt(3);
|
||||
}
|
||||
}
|
||||
for (const perpMarket of Array.from(
|
||||
group.perpMarketsMapByMarketIndex.values(),
|
||||
|
@ -266,15 +260,11 @@ async function main(): Promise<void> {
|
|||
true
|
||||
// Enable below to debug specific mango accounts
|
||||
// mangoAccount.publicKey.equals(
|
||||
// new PublicKey('BXUPaeAWRCPvPdpndXJeykD8VYZJwrCBjZdWNZAu8Ca'),
|
||||
// new PublicKey('GGfkfpT4dY8hmNK6SKKSBFdn7ucQXSc4WtDCQpnQt4p2'),
|
||||
// )
|
||||
) {
|
||||
console.log();
|
||||
console.log(`MangoAccount ${mangoAccount.publicKey}`);
|
||||
|
||||
// Log only tokens
|
||||
// console.log(mangoAccount.toString(group, true));
|
||||
|
||||
// Long all debug info
|
||||
await debugUser(client, group, mangoAccount);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1018,7 +1018,12 @@ export type MangoV4 = {
|
|||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
"args": [
|
||||
{
|
||||
"name": "forceClose",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stubOracleCreate",
|
||||
|
@ -4403,7 +4408,8 @@ export type MangoV4 = {
|
|||
{
|
||||
"name": "settlePnlLimitFactor",
|
||||
"docs": [
|
||||
"Fraction of perp base value that can be settled each window.",
|
||||
"Fraction of perp base value (i.e. base_lots * entry_price_in_lots) of unrealized",
|
||||
"positive pnl that can be settled each window.",
|
||||
"Set to a negative value to disable the limit."
|
||||
],
|
||||
"type": "f32"
|
||||
|
@ -8421,7 +8427,12 @@ export const IDL: MangoV4 = {
|
|||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
"args": [
|
||||
{
|
||||
"name": "forceClose",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stubOracleCreate",
|
||||
|
@ -11806,7 +11817,8 @@ export const IDL: MangoV4 = {
|
|||
{
|
||||
"name": "settlePnlLimitFactor",
|
||||
"docs": [
|
||||
"Fraction of perp base value that can be settled each window.",
|
||||
"Fraction of perp base value (i.e. base_lots * entry_price_in_lots) of unrealized",
|
||||
"positive pnl that can be settled each window.",
|
||||
"Set to a negative value to disable the limit."
|
||||
],
|
||||
"type": "f32"
|
||||
|
|
|
@ -501,7 +501,6 @@ async function main() {
|
|||
const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi(
|
||||
group,
|
||||
perpMarket.perpMarketIndex,
|
||||
perpMarket.uiPrice,
|
||||
);
|
||||
const baseQty = quoteQty / price;
|
||||
console.log(
|
||||
|
@ -509,7 +508,6 @@ async function main() {
|
|||
group,
|
||||
perpMarket.perpMarketIndex,
|
||||
baseQty,
|
||||
perpMarket.uiPrice,
|
||||
)}`,
|
||||
);
|
||||
console.log(
|
||||
|
@ -552,7 +550,6 @@ async function main() {
|
|||
mangoAccount.getMaxQuoteForPerpBidUi(
|
||||
group,
|
||||
perpMarket.perpMarketIndex,
|
||||
perpMarket.uiPrice,
|
||||
) * 1.02;
|
||||
|
||||
const baseQty = quoteQty / price;
|
||||
|
@ -588,14 +585,12 @@ async function main() {
|
|||
const baseQty = mangoAccount.getMaxBaseForPerpAskUi(
|
||||
group,
|
||||
perpMarket.perpMarketIndex,
|
||||
perpMarket.uiPrice,
|
||||
);
|
||||
console.log(
|
||||
` simHealthRatioWithPerpAskUiChanges - ${mangoAccount.simHealthRatioWithPerpAskUiChanges(
|
||||
group,
|
||||
perpMarket.perpMarketIndex,
|
||||
baseQty,
|
||||
perpMarket.uiPrice,
|
||||
)}`,
|
||||
);
|
||||
const quoteQty = baseQty * price;
|
||||
|
@ -628,11 +623,8 @@ async function main() {
|
|||
group.banksMapByName.get('BTC')![0].uiPrice! +
|
||||
Math.floor(Math.random() * 100);
|
||||
const baseQty =
|
||||
mangoAccount.getMaxBaseForPerpAskUi(
|
||||
group,
|
||||
perpMarket.perpMarketIndex,
|
||||
perpMarket.uiPrice,
|
||||
) * 1.02;
|
||||
mangoAccount.getMaxBaseForPerpAskUi(group, perpMarket.perpMarketIndex) *
|
||||
1.02;
|
||||
const quoteQty = baseQty * price;
|
||||
console.log(
|
||||
`...placing max qty perp ask * 1.02 clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
|
||||
|
|
|
@ -67,7 +67,6 @@ async function main() {
|
|||
|
||||
// close all banks
|
||||
for (const banks of group.banksMapByMint.values()) {
|
||||
console.log(banks[0].toString());
|
||||
sig = await client.tokenDeregister(group, banks[0].mint);
|
||||
console.log(
|
||||
`Removed token ${banks[0].name}, sig https://explorer.solana.com/tx/${sig}`,
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import {
|
||||
AddressLookupTableProgram,
|
||||
Connection,
|
||||
Keypair,
|
||||
PublicKey,
|
||||
} from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
import { buildVersionedTx } from '../utils';
|
||||
|
||||
//
|
||||
// Script which depoys a new mango group, and registers 3 tokens
|
||||
|
@ -14,23 +20,23 @@ const GROUP_NUM = Number(process.env.GROUP_NUM || 200);
|
|||
|
||||
const MAINNET_MINTS = new Map([
|
||||
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
|
||||
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
|
||||
['ETH', '7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs'],
|
||||
['SOL', 'So11111111111111111111111111111111111111112'],
|
||||
['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'],
|
||||
]);
|
||||
|
||||
const STUB_PRICES = new Map([
|
||||
['USDC', 1.0],
|
||||
['BTC', 20000.0], // btc and usdc both have 6 decimals
|
||||
['SOL', 0.04], // sol has 9 decimals, equivalent to $40 per SOL
|
||||
['MNGO', 0.04], // same price/decimals as SOL for convenience
|
||||
['ETH', 1200.0], // eth and usdc both have 6 decimals
|
||||
['SOL', 0.015], // sol has 9 decimals, equivalent to $15 per SOL
|
||||
['MNGO', 0.02], // same price/decimals as SOL for convenience
|
||||
]);
|
||||
|
||||
// External markets are matched with those in https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json
|
||||
// and verified to have best liquidity for pair on https://openserum.io/
|
||||
const MAINNET_SERUM3_MARKETS = new Map([
|
||||
['BTC/USDC', 'A8YFbxQYFVqKZaoYJLLUVcQiWP7G2MeEgW5wsAQgMvFw'],
|
||||
['SOL/USDC', '9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT'],
|
||||
['ETH/USDC', 'FZxi3yWkE5mMjyaZj6utmYL54QQYfMCKMcLaQZq4UwnA'],
|
||||
['SOL/USDC', '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6'],
|
||||
]);
|
||||
|
||||
const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2;
|
||||
|
@ -130,17 +136,17 @@ async function main() {
|
|||
}
|
||||
|
||||
// register token 1
|
||||
console.log(`Registering BTC...`);
|
||||
const btcMainnetMint = new PublicKey(MAINNET_MINTS.get('BTC')!);
|
||||
const btcMainnetOracle = oracles.get('BTC');
|
||||
console.log(`Registering ETH...`);
|
||||
const ethMainnetMint = new PublicKey(MAINNET_MINTS.get('ETH')!);
|
||||
const ethMainnetOracle = oracles.get('ETH');
|
||||
try {
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
btcMainnetMint,
|
||||
btcMainnetOracle,
|
||||
ethMainnetMint,
|
||||
ethMainnetOracle,
|
||||
defaultOracleConfig,
|
||||
1,
|
||||
'BTC',
|
||||
'ETH',
|
||||
defaultInterestRate,
|
||||
0.0,
|
||||
0.0001,
|
||||
|
@ -215,7 +221,7 @@ async function main() {
|
|||
0,
|
||||
'MNGO-PERP',
|
||||
defaultOracleConfig,
|
||||
9,
|
||||
6,
|
||||
10,
|
||||
100000, // base lots
|
||||
0.9,
|
||||
|
@ -235,14 +241,134 @@ async function main() {
|
|||
0,
|
||||
0,
|
||||
0,
|
||||
1.0,
|
||||
-1.0,
|
||||
2 * 60 * 60,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
await createAndPopulateAlt(client, admin);
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
async function createAndPopulateAlt(client: MangoClient, admin: Keypair) {
|
||||
let group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
|
||||
const connection = client.program.provider.connection;
|
||||
|
||||
// Create ALT, and set to group at index 0
|
||||
if (group.addressLookupTables[0].equals(PublicKey.default)) {
|
||||
try {
|
||||
console.log(`ALT: Creating`);
|
||||
const createIx = AddressLookupTableProgram.createLookupTable({
|
||||
authority: admin.publicKey,
|
||||
payer: admin.publicKey,
|
||||
recentSlot: await connection.getSlot('finalized'),
|
||||
});
|
||||
const createTx = await buildVersionedTx(
|
||||
client.program.provider as AnchorProvider,
|
||||
[createIx[0]],
|
||||
);
|
||||
let sig = await connection.sendTransaction(createTx);
|
||||
console.log(
|
||||
`...created ALT ${createIx[1]} https://explorer.solana.com/tx/${sig}`,
|
||||
);
|
||||
|
||||
console.log(`ALT: set at index 0 for group...`);
|
||||
sig = await client.altSet(group, createIx[1], 0);
|
||||
console.log(`...https://explorer.solana.com/tx/${sig}`);
|
||||
|
||||
group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Extend using mango v4 relevant pub keys
|
||||
try {
|
||||
let bankAddresses = Array.from(group.banksMapByMint.values())
|
||||
.flat()
|
||||
.map((bank) => [bank.publicKey, bank.oracle, bank.vault])
|
||||
.flat()
|
||||
.concat(
|
||||
Array.from(group.banksMapByMint.values())
|
||||
.flat()
|
||||
.map((mintInfo) => mintInfo.publicKey),
|
||||
);
|
||||
|
||||
let serum3MarketAddresses = Array.from(
|
||||
group.serum3MarketsMapByExternal.values(),
|
||||
)
|
||||
.flat()
|
||||
.map((serum3Market) => serum3Market.publicKey);
|
||||
|
||||
let serum3ExternalMarketAddresses = Array.from(
|
||||
group.serum3ExternalMarketsMap.values(),
|
||||
)
|
||||
.flat()
|
||||
.map((serum3ExternalMarket) => [
|
||||
serum3ExternalMarket.publicKey,
|
||||
serum3ExternalMarket.bidsAddress,
|
||||
serum3ExternalMarket.asksAddress,
|
||||
])
|
||||
.flat();
|
||||
|
||||
let perpMarketAddresses = Array.from(
|
||||
group.perpMarketsMapByMarketIndex.values(),
|
||||
)
|
||||
.flat()
|
||||
.map((perpMarket) => [
|
||||
perpMarket.publicKey,
|
||||
perpMarket.oracle,
|
||||
perpMarket.bids,
|
||||
perpMarket.asks,
|
||||
perpMarket.eventQueue,
|
||||
])
|
||||
.flat();
|
||||
|
||||
async function extendTable(addresses: PublicKey[]) {
|
||||
await group.reloadAll(client);
|
||||
const alt =
|
||||
await client.program.provider.connection.getAddressLookupTable(
|
||||
group.addressLookupTables[0],
|
||||
);
|
||||
|
||||
addresses = addresses.filter(
|
||||
(newAddress) =>
|
||||
alt.value?.state.addresses &&
|
||||
alt.value?.state.addresses.findIndex((addressInALt) =>
|
||||
addressInALt.equals(newAddress),
|
||||
) === -1,
|
||||
);
|
||||
if (addresses.length === 0) {
|
||||
return;
|
||||
}
|
||||
const extendIx = AddressLookupTableProgram.extendLookupTable({
|
||||
lookupTable: group.addressLookupTables[0],
|
||||
payer: admin.publicKey,
|
||||
authority: admin.publicKey,
|
||||
addresses,
|
||||
});
|
||||
const extendTx = await buildVersionedTx(
|
||||
client.program.provider as AnchorProvider,
|
||||
[extendIx],
|
||||
);
|
||||
let sig = await client.program.provider.connection.sendTransaction(
|
||||
extendTx,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}`);
|
||||
}
|
||||
|
||||
console.log(`ALT: extending using mango v4 relevant public keys`);
|
||||
await extendTable(bankAddresses);
|
||||
await extendTable(serum3MarketAddresses);
|
||||
await extendTable(serum3ExternalMarketAddresses);
|
||||
await extendTable(perpMarketAddresses);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
// This script deposits some tokens, so other liquidation scripts can borrow.
|
||||
//
|
||||
|
||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 200);
|
||||
const ACCOUNT_NUM = Number(process.env.ACCOUNT_NUM || 0);
|
||||
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(process.env.CLUSTER_URL!, options);
|
||||
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(
|
||||
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
|
||||
),
|
||||
),
|
||||
);
|
||||
const userWallet = new Wallet(admin);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
const client = await MangoClient.connect(
|
||||
userProvider,
|
||||
'mainnet-beta',
|
||||
MANGO_V4_ID['mainnet-beta'],
|
||||
{
|
||||
idsSource: 'get-program-accounts',
|
||||
},
|
||||
);
|
||||
console.log(`User ${userWallet.publicKey.toBase58()}`);
|
||||
|
||||
// fetch group
|
||||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
console.log(group.toString());
|
||||
|
||||
// create + fetch account
|
||||
console.log(`Creating mangoaccount...`);
|
||||
const mangoAccount = (await client.createAndFetchMangoAccount(
|
||||
group,
|
||||
ACCOUNT_NUM,
|
||||
'LIQTEST, FUNDING',
|
||||
8,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
))!;
|
||||
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString());
|
||||
|
||||
const usdcMint = group.banksMapByName.get('USDC')![0].mint;
|
||||
const btcMint = group.banksMapByName.get('BTC')![0].mint;
|
||||
const solMint = group.banksMapByName.get('SOL')![0].mint;
|
||||
|
||||
// deposit
|
||||
try {
|
||||
console.log(`...depositing 5 USDC`);
|
||||
await client.tokenDeposit(group, mangoAccount, usdcMint, 5);
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
console.log(`...depositing 0.0002 BTC`);
|
||||
await client.tokenDeposit(group, mangoAccount, btcMint, 0.0002);
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
console.log(`...depositing 0.15 SOL`);
|
||||
await client.tokenDeposit(group, mangoAccount, solMint, 0.15);
|
||||
await mangoAccount.reload(client);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
main();
|
|
@ -1,8 +1,9 @@
|
|||
import { AnchorProvider, BN, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { Bank } from '../accounts/bank';
|
||||
import { MangoAccount } from '../accounts/mangoAccount';
|
||||
import { PerpOrderSide, PerpOrderType } from '../accounts/perp';
|
||||
import { PerpMarket, PerpOrderSide, PerpOrderType } from '../accounts/perp';
|
||||
import {
|
||||
Serum3OrderType,
|
||||
Serum3SelfTradeBehavior,
|
||||
|
@ -19,24 +20,30 @@ const GROUP_NUM = Number(process.env.GROUP_NUM || 200);
|
|||
|
||||
// native prices
|
||||
const PRICES = {
|
||||
BTC: 20000.0,
|
||||
SOL: 0.04,
|
||||
ETH: 1200.0,
|
||||
SOL: 0.015,
|
||||
USDC: 1,
|
||||
MNGO: 0.04,
|
||||
MNGO: 0.02,
|
||||
};
|
||||
|
||||
const MAINNET_MINTS = new Map([
|
||||
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
|
||||
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
|
||||
['SOL', 'So11111111111111111111111111111111111111112'],
|
||||
['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'],
|
||||
]);
|
||||
|
||||
const TOKEN_SCENARIOS: [string, string, number, string, number][] = [
|
||||
['LIQTEST, LIQOR', 'USDC', 1000000, 'USDC', 0],
|
||||
['LIQTEST, A: USDC, L: SOL', 'USDC', 1000 * PRICES.SOL, 'SOL', 920],
|
||||
['LIQTEST, A: SOL, L: USDC', 'SOL', 1000, 'USDC', 920 * PRICES.SOL],
|
||||
['LIQTEST, A: BTC, L: SOL', 'BTC', 20, 'SOL', (18 * PRICES.BTC) / PRICES.SOL],
|
||||
const TOKEN_SCENARIOS: [string, [string, number][], [string, number][]][] = [
|
||||
[
|
||||
'LIQTEST, FUNDING',
|
||||
[
|
||||
['USDC', 5000000],
|
||||
['ETH', 100000],
|
||||
['SOL', 150000000],
|
||||
],
|
||||
[],
|
||||
],
|
||||
['LIQTEST, LIQOR', [['USDC', 1000000]], []],
|
||||
['LIQTEST, A: USDC, L: SOL', [['USDC', 1000 * PRICES.SOL]], [['SOL', 920]]],
|
||||
['LIQTEST, A: SOL, L: USDC', [['SOL', 1000]], [['USDC', 990 * PRICES.SOL]]],
|
||||
[
|
||||
'LIQTEST, A: ETH, L: SOL',
|
||||
[['ETH', 20]],
|
||||
[['SOL', (18 * PRICES.ETH) / PRICES.SOL]],
|
||||
],
|
||||
];
|
||||
|
||||
async function main() {
|
||||
|
@ -66,17 +73,17 @@ async function main() {
|
|||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
console.log(group.toString());
|
||||
|
||||
const MAINNET_MINTS = new Map([
|
||||
['USDC', group.banksMapByName.get('USDC')![0].mint],
|
||||
['ETH', group.banksMapByName.get('ETH')![0].mint],
|
||||
['SOL', group.banksMapByName.get('SOL')![0].mint],
|
||||
]);
|
||||
|
||||
const accounts = await client.getMangoAccountsForOwner(
|
||||
group,
|
||||
admin.publicKey,
|
||||
);
|
||||
let maxAccountNum = Math.max(0, ...accounts.map((a) => a.accountNum));
|
||||
const fundingAccount = accounts.find(
|
||||
(account) => account.name == 'LIQTEST, FUNDING',
|
||||
);
|
||||
if (!fundingAccount) {
|
||||
throw new Error('could not find funding account');
|
||||
}
|
||||
|
||||
async function createMangoAccount(name: string): Promise<MangoAccount> {
|
||||
const accountNum = maxAccountNum + 1;
|
||||
|
@ -89,8 +96,73 @@ async function main() {
|
|||
))!;
|
||||
}
|
||||
|
||||
async function setBankPrice(bank: Bank, price: number): Promise<void> {
|
||||
await client.stubOracleSet(group, bank.oracle, price);
|
||||
// reset stable price
|
||||
await client.tokenEdit(
|
||||
group,
|
||||
bank.mint,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
null,
|
||||
);
|
||||
}
|
||||
async function setPerpPrice(
|
||||
perpMarket: PerpMarket,
|
||||
price: number,
|
||||
): Promise<void> {
|
||||
await client.stubOracleSet(group, perpMarket.oracle, price);
|
||||
// reset stable price
|
||||
await client.perpEditMarket(
|
||||
group,
|
||||
perpMarket.perpMarketIndex,
|
||||
perpMarket.oracle,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
for (const scenario of TOKEN_SCENARIOS) {
|
||||
const [name, assetName, assetAmount, liabName, liabAmount] = scenario;
|
||||
const [name, assets, liabs] = scenario;
|
||||
|
||||
// create account
|
||||
console.log(`Creating mangoaccount...`);
|
||||
|
@ -99,22 +171,24 @@ async function main() {
|
|||
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
|
||||
);
|
||||
|
||||
const assetMint = new PublicKey(MAINNET_MINTS.get(assetName)!);
|
||||
const liabMint = new PublicKey(MAINNET_MINTS.get(liabName)!);
|
||||
for (let [assetName, assetAmount] of assets) {
|
||||
const assetMint = new PublicKey(MAINNET_MINTS.get(assetName)!);
|
||||
await client.tokenDepositNative(
|
||||
group,
|
||||
mangoAccount,
|
||||
assetMint,
|
||||
new BN(assetAmount),
|
||||
);
|
||||
await mangoAccount.reload(client);
|
||||
}
|
||||
|
||||
await client.tokenDepositNative(
|
||||
group,
|
||||
mangoAccount,
|
||||
assetMint,
|
||||
new BN(assetAmount),
|
||||
);
|
||||
await mangoAccount.reload(client);
|
||||
for (let [liabName, liabAmount] of liabs) {
|
||||
const liabMint = new PublicKey(MAINNET_MINTS.get(liabName)!);
|
||||
|
||||
if (liabAmount > 0) {
|
||||
// temporarily drop the borrowed token value, so the borrow goes through
|
||||
const oracle = group.banksMapByName.get(liabName)![0].oracle;
|
||||
const bank = group.banksMapByName.get(liabName)![0];
|
||||
try {
|
||||
await client.stubOracleSet(group, oracle, PRICES[liabName] / 2);
|
||||
await setBankPrice(bank, PRICES[liabName] / 2);
|
||||
|
||||
await client.tokenWithdrawNative(
|
||||
group,
|
||||
|
@ -125,11 +199,22 @@ async function main() {
|
|||
);
|
||||
} finally {
|
||||
// restore the oracle
|
||||
await client.stubOracleSet(group, oracle, PRICES[liabName]);
|
||||
await setBankPrice(bank, PRICES[liabName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const accounts2 = await client.getMangoAccountsForOwner(
|
||||
group,
|
||||
admin.publicKey,
|
||||
);
|
||||
const fundingAccount = accounts2.find(
|
||||
(account) => account.name == 'LIQTEST, FUNDING',
|
||||
);
|
||||
if (!fundingAccount) {
|
||||
throw new Error('could not find funding account');
|
||||
}
|
||||
|
||||
// Serum order scenario
|
||||
{
|
||||
const name = 'LIQTEST, serum orders';
|
||||
|
@ -233,19 +318,18 @@ async function main() {
|
|||
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
|
||||
);
|
||||
|
||||
const baseMint = new PublicKey(MAINNET_MINTS.get('MNGO')!);
|
||||
const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
|
||||
const collateralOracle = group.banksMapByName.get('SOL')![0].oracle;
|
||||
const collateralBank = group.banksMapByName.get('SOL')![0];
|
||||
|
||||
await client.tokenDepositNative(
|
||||
group,
|
||||
mangoAccount,
|
||||
collateralMint,
|
||||
new BN(100000),
|
||||
); // valued as $0.004 maint collateral
|
||||
new BN(300000),
|
||||
); // valued as 0.0003 SOL, $0.0045 maint collateral
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 4);
|
||||
await setBankPrice(collateralBank, PRICES['SOL'] * 4);
|
||||
|
||||
try {
|
||||
await client.perpPlaceOrder(
|
||||
|
@ -253,9 +337,9 @@ async function main() {
|
|||
mangoAccount,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
PerpOrderSide.bid,
|
||||
1, // ui price that won't get hit
|
||||
0.0011, // ui base quantity, 11 base lots, $0.044
|
||||
0.044, // ui quote quantity
|
||||
0.001, // ui price that won't get hit
|
||||
1.1, // ui base quantity, 11 base lots, 1.1 MNGO, $0.022
|
||||
0.022, // ui quote quantity
|
||||
4200,
|
||||
PerpOrderType.limit,
|
||||
false,
|
||||
|
@ -263,7 +347,7 @@ async function main() {
|
|||
5,
|
||||
);
|
||||
} finally {
|
||||
await client.stubOracleSet(group, collateralOracle, PRICES['SOL']);
|
||||
await setBankPrice(collateralBank, PRICES['SOL']);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,19 +361,18 @@ async function main() {
|
|||
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
|
||||
);
|
||||
|
||||
const baseMint = new PublicKey(MAINNET_MINTS.get('MNGO')!);
|
||||
const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
|
||||
const collateralOracle = group.banksMapByName.get('SOL')![0].oracle;
|
||||
const collateralBank = group.banksMapByName.get('SOL')![0];
|
||||
|
||||
await client.tokenDepositNative(
|
||||
group,
|
||||
mangoAccount,
|
||||
collateralMint,
|
||||
new BN(100000),
|
||||
); // valued as $0.004 maint collateral
|
||||
new BN(300000),
|
||||
); // valued as 0.0003 SOL, $0.0045 maint collateral
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 5);
|
||||
await setBankPrice(collateralBank, PRICES['SOL'] * 10);
|
||||
|
||||
try {
|
||||
await client.perpPlaceOrder(
|
||||
|
@ -297,9 +380,9 @@ async function main() {
|
|||
fundingAccount,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
PerpOrderSide.ask,
|
||||
40,
|
||||
0.0011, // ui base quantity, 11 base lots, $0.044
|
||||
0.044, // ui quote quantity
|
||||
0.03,
|
||||
1.1, // ui base quantity, 11 base lots, $0.022 value, gain $0.033
|
||||
0.033, // ui quote quantity
|
||||
4200,
|
||||
PerpOrderType.limit,
|
||||
false,
|
||||
|
@ -312,9 +395,9 @@ async function main() {
|
|||
mangoAccount,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
PerpOrderSide.bid,
|
||||
40,
|
||||
0.0011, // ui base quantity, 11 base lots, $0.044
|
||||
0.044, // ui quote quantity
|
||||
0.03,
|
||||
1.1, // ui base quantity, 11 base lots, $0.022 value, cost $0.033
|
||||
0.033, // ui quote quantity
|
||||
4200,
|
||||
PerpOrderType.market,
|
||||
false,
|
||||
|
@ -327,7 +410,7 @@ async function main() {
|
|||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
);
|
||||
} finally {
|
||||
await client.stubOracleSet(group, collateralOracle, PRICES['SOL']);
|
||||
await setBankPrice(collateralBank, PRICES['SOL']);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -341,23 +424,22 @@ async function main() {
|
|||
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
|
||||
);
|
||||
|
||||
const baseMint = new PublicKey(MAINNET_MINTS.get('MNGO')!);
|
||||
const baseOracle = (await client.getStubOracle(group, baseMint))[0]
|
||||
.publicKey;
|
||||
const perpMarket = group.perpMarketsMapByName.get('MNGO-PERP')!;
|
||||
const perpIndex = perpMarket.perpMarketIndex;
|
||||
const liabMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
|
||||
const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
|
||||
const collateralOracle = group.banksMapByName.get('SOL')![0].oracle;
|
||||
const collateralBank = group.banksMapByName.get('SOL')![0];
|
||||
|
||||
await client.tokenDepositNative(
|
||||
group,
|
||||
mangoAccount,
|
||||
collateralMint,
|
||||
new BN(100000),
|
||||
); // valued as $0.004 maint collateral
|
||||
new BN(300000),
|
||||
); // valued as $0.0045 maint collateral
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
try {
|
||||
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 10);
|
||||
await setBankPrice(collateralBank, PRICES['SOL'] * 10);
|
||||
|
||||
// Spot-borrow more than the collateral is worth
|
||||
await client.tokenWithdrawNative(
|
||||
|
@ -369,16 +451,16 @@ async function main() {
|
|||
);
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
// Execute two trades that leave the account with +$0.022 positive pnl
|
||||
await client.stubOracleSet(group, baseOracle, PRICES['MNGO'] / 2);
|
||||
// Execute two trades that leave the account with +$0.011 positive pnl
|
||||
await setPerpPrice(perpMarket, PRICES['MNGO'] / 2);
|
||||
await client.perpPlaceOrder(
|
||||
group,
|
||||
fundingAccount,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
perpIndex,
|
||||
PerpOrderSide.ask,
|
||||
20,
|
||||
0.0011, // ui base quantity, 11 base lots, $0.022
|
||||
0.022, // ui quote quantity
|
||||
0.01,
|
||||
1.1, // ui base quantity, 11 base lots, $0.011
|
||||
0.011, // ui quote quantity
|
||||
4200,
|
||||
PerpOrderType.limit,
|
||||
false,
|
||||
|
@ -388,32 +470,29 @@ async function main() {
|
|||
await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
perpIndex,
|
||||
PerpOrderSide.bid,
|
||||
20,
|
||||
0.0011, // ui base quantity, 11 base lots, $0.022
|
||||
0.022, // ui quote quantity
|
||||
0.01,
|
||||
1.1, // ui base quantity, 11 base lots, $0.011
|
||||
0.011, // ui quote quantity
|
||||
4200,
|
||||
PerpOrderType.market,
|
||||
false,
|
||||
0,
|
||||
5,
|
||||
);
|
||||
await client.perpConsumeAllEvents(
|
||||
group,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
);
|
||||
await client.perpConsumeAllEvents(group, perpIndex);
|
||||
|
||||
await client.stubOracleSet(group, baseOracle, PRICES['MNGO']);
|
||||
await setPerpPrice(perpMarket, PRICES['MNGO']);
|
||||
|
||||
await client.perpPlaceOrder(
|
||||
group,
|
||||
fundingAccount,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
perpIndex,
|
||||
PerpOrderSide.bid,
|
||||
40,
|
||||
0.0011, // ui base quantity, 11 base lots, $0.044
|
||||
0.044, // ui quote quantity
|
||||
0.02,
|
||||
1.1, // ui base quantity, 11 base lots, $0.022
|
||||
0.022, // ui quote quantity
|
||||
4201,
|
||||
PerpOrderType.limit,
|
||||
false,
|
||||
|
@ -423,24 +502,21 @@ async function main() {
|
|||
await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
perpIndex,
|
||||
PerpOrderSide.ask,
|
||||
40,
|
||||
0.0011, // ui base quantity, 11 base lots, $0.044
|
||||
0.044, // ui quote quantity
|
||||
0.02,
|
||||
1.1, // ui base quantity, 11 base lots, $0.022
|
||||
0.022, // ui quote quantity
|
||||
4201,
|
||||
PerpOrderType.market,
|
||||
false,
|
||||
0,
|
||||
5,
|
||||
);
|
||||
await client.perpConsumeAllEvents(
|
||||
group,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
);
|
||||
await client.perpConsumeAllEvents(group, perpIndex);
|
||||
} finally {
|
||||
await client.stubOracleSet(group, collateralOracle, PRICES['SOL']);
|
||||
await client.stubOracleSet(group, baseOracle, PRICES['MNGO']);
|
||||
await setPerpPrice(perpMarket, PRICES['MNGO']);
|
||||
await setBankPrice(collateralBank, PRICES['SOL']);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ async function main() {
|
|||
// close account
|
||||
try {
|
||||
console.log(`closing account: ${account}`);
|
||||
await client.closeMangoAccount(group, account);
|
||||
await client.closeMangoAccount(group, account, true);
|
||||
} catch (error) {
|
||||
console.log(`failed to close ${account.publicKey}: ${error}`);
|
||||
}
|
||||
|
|
|
@ -22,6 +22,10 @@ import { I80F48 } from './numbers/I80F48';
|
|||
export const U64_MAX_BN = new BN('18446744073709551615');
|
||||
export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64);
|
||||
|
||||
export function toNativeI80F48ForQuote(uiAmount: number): I80F48 {
|
||||
return I80F48.fromNumber(uiAmount * Math.pow(10, 6));
|
||||
}
|
||||
|
||||
export function toNativeI80F48(uiAmount: number, decimals: number): I80F48 {
|
||||
return I80F48.fromNumber(uiAmount * Math.pow(10, decimals));
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue