liquidator: force-cancel perp orders, liq perp base positions
This commit is contained in:
parent
a97b40a521
commit
9cbc352197
|
@ -15,14 +15,16 @@ use bincode::Options;
|
|||
use fixed::types::I80F48;
|
||||
use itertools::Itertools;
|
||||
use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
|
||||
use mango_v4::state::{Bank, Group, MangoAccountValue, Serum3MarketIndex, TokenIndex};
|
||||
use mango_v4::state::{
|
||||
Bank, Group, MangoAccountValue, PerpMarketIndex, Serum3MarketIndex, TokenIndex,
|
||||
};
|
||||
|
||||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
use solana_sdk::signer::keypair;
|
||||
|
||||
use crate::account_fetcher::*;
|
||||
use crate::context::{MangoGroupContext, Serum3MarketContext, TokenContext};
|
||||
use crate::context::{MangoGroupContext, PerpMarketContext, Serum3MarketContext, TokenContext};
|
||||
use crate::gpa::fetch_mango_accounts;
|
||||
use crate::jupiter;
|
||||
use crate::util::MyClone;
|
||||
|
@ -836,12 +838,98 @@ impl MangoClient {
|
|||
//
|
||||
// Perps
|
||||
//
|
||||
fn perp_data_by_market_index(
|
||||
&self,
|
||||
market_index: PerpMarketIndex,
|
||||
) -> Result<&PerpMarketContext, ClientError> {
|
||||
Ok(self.context.perp_markets.get(&market_index).unwrap())
|
||||
}
|
||||
|
||||
pub fn perp_liq_force_cancel_orders(
|
||||
&self,
|
||||
liqee: (&Pubkey, &MangoAccountValue),
|
||||
market_index: PerpMarketIndex,
|
||||
) -> anyhow::Result<Signature> {
|
||||
let perp = self.perp_data_by_market_index(market_index)?;
|
||||
|
||||
let health_remaining_ams = self
|
||||
.context
|
||||
.derive_health_check_remaining_account_metas(liqee.1, vec![], false)
|
||||
.unwrap();
|
||||
|
||||
self.program()
|
||||
.request()
|
||||
.instruction(Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: {
|
||||
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::PerpLiqForceCancelOrders {
|
||||
group: self.group(),
|
||||
account: *liqee.0,
|
||||
perp_market: perp.address,
|
||||
asks: perp.market.asks,
|
||||
bids: perp.market.bids,
|
||||
oracle: perp.market.oracle,
|
||||
},
|
||||
None,
|
||||
);
|
||||
ams.extend(health_remaining_ams.into_iter());
|
||||
ams
|
||||
},
|
||||
data: anchor_lang::InstructionData::data(
|
||||
&mango_v4::instruction::PerpLiqForceCancelOrders { limit: 5 },
|
||||
),
|
||||
})
|
||||
.send()
|
||||
.map_err(prettify_client_error)
|
||||
}
|
||||
|
||||
pub fn perp_liq_base_position(
|
||||
&self,
|
||||
liqee: (&Pubkey, &MangoAccountValue),
|
||||
market_index: PerpMarketIndex,
|
||||
max_base_transfer: i64,
|
||||
) -> anyhow::Result<Signature> {
|
||||
let perp = self.perp_data_by_market_index(market_index)?;
|
||||
|
||||
let health_remaining_ams = self
|
||||
.context
|
||||
.derive_health_check_remaining_account_metas(liqee.1, vec![], false)
|
||||
.unwrap();
|
||||
|
||||
self.program()
|
||||
.request()
|
||||
.instruction(Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: {
|
||||
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::PerpLiqBasePosition {
|
||||
group: self.group(),
|
||||
perp_market: perp.address,
|
||||
oracle: perp.market.oracle,
|
||||
liqor: self.mango_account_address,
|
||||
liqor_owner: self.owner(),
|
||||
liqee: *liqee.0,
|
||||
},
|
||||
None,
|
||||
);
|
||||
ams.extend(health_remaining_ams.into_iter());
|
||||
ams
|
||||
},
|
||||
data: anchor_lang::InstructionData::data(
|
||||
&mango_v4::instruction::PerpLiqBasePosition { max_base_transfer },
|
||||
),
|
||||
})
|
||||
.signer(&self.owner)
|
||||
.send()
|
||||
.map_err(prettify_client_error)
|
||||
}
|
||||
|
||||
//
|
||||
// Liquidation
|
||||
//
|
||||
|
||||
pub fn liq_token_with_token(
|
||||
pub fn token_liq_with_token(
|
||||
&self,
|
||||
liqee: (&Pubkey, &MangoAccountValue),
|
||||
asset_token_index: TokenIndex,
|
||||
|
@ -886,7 +974,7 @@ impl MangoClient {
|
|||
.map_err(prettify_client_error)
|
||||
}
|
||||
|
||||
pub fn liq_token_bankruptcy(
|
||||
pub fn token_liq_bankruptcy(
|
||||
&self,
|
||||
liqee: (&Pubkey, &MangoAccountValue),
|
||||
liab_token_index: TokenIndex,
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::account_shared_data::KeyedAccountSharedData;
|
|||
use client::{chain_data, AccountFetcher, MangoClient, MangoClientError, MangoGroupContext};
|
||||
use mango_v4::state::{
|
||||
new_health_cache, Bank, FixedOrderAccountRetriever, HealthCache, HealthType, MangoAccountValue,
|
||||
Serum3Orders, TokenIndex, QUOTE_TOKEN_INDEX,
|
||||
PerpMarketIndex, Serum3Orders, Side, TokenIndex, QUOTE_TOKEN_INDEX,
|
||||
};
|
||||
|
||||
use itertools::Itertools;
|
||||
|
@ -177,6 +177,36 @@ pub fn maybe_liquidate_account(
|
|||
.filter_map_ok(|v| v)
|
||||
.collect::<anyhow::Result<Vec<Serum3Orders>>>()?;
|
||||
|
||||
// look for any perp open orders and base positions
|
||||
let perp_force_cancels = account
|
||||
.active_perp_positions()
|
||||
.filter_map(|pp| pp.has_open_orders().then(|| pp.market_index))
|
||||
.collect::<Vec<PerpMarketIndex>>();
|
||||
let mut perp_base_positions = account
|
||||
.active_perp_positions()
|
||||
.map(|pp| {
|
||||
let base_lots = pp.base_position_lots();
|
||||
if base_lots == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let perp = mango_client.context.perp(pp.market_index);
|
||||
let oracle = account_fetcher.fetch_raw_account(perp.market.oracle)?;
|
||||
let price = perp.market.oracle_price(&KeyedAccountSharedData::new(
|
||||
perp.market.oracle,
|
||||
oracle.into(),
|
||||
))?;
|
||||
Ok(Some((
|
||||
pp.market_index,
|
||||
base_lots,
|
||||
price,
|
||||
I80F48::from(base_lots.abs()) * price,
|
||||
)))
|
||||
})
|
||||
.filter_map_ok(|v| v)
|
||||
.collect::<anyhow::Result<Vec<(PerpMarketIndex, i64, I80F48, I80F48)>>>()?;
|
||||
// sort by base_position_value, ascending
|
||||
perp_base_positions.sort_by(|a, b| a.3.cmp(&b.3));
|
||||
|
||||
let get_max_liab_transfer = |source, target| -> anyhow::Result<I80F48> {
|
||||
let mut liqor = account_fetcher
|
||||
.fetch_fresh_mango_account(&mango_client.mango_account_address)
|
||||
|
@ -203,7 +233,7 @@ pub fn maybe_liquidate_account(
|
|||
|
||||
// try liquidating
|
||||
let txsig = if !serum_force_cancels.is_empty() {
|
||||
// pick a random market to force-cancel orders on
|
||||
// Cancel all orders on a random serum market
|
||||
let serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap();
|
||||
let sig = mango_client.serum3_liq_force_cancel_orders(
|
||||
(pubkey, &account),
|
||||
|
@ -211,13 +241,68 @@ pub fn maybe_liquidate_account(
|
|||
&serum_orders.open_orders,
|
||||
)?;
|
||||
log::info!(
|
||||
"Force cancelled serum market on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||
"Force cancelled serum orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||
pubkey,
|
||||
serum_orders.market_index,
|
||||
maint_health,
|
||||
sig
|
||||
);
|
||||
sig
|
||||
} else if !perp_force_cancels.is_empty() {
|
||||
// Cancel all orders on a random perp market
|
||||
let perp_market_index = *perp_force_cancels.choose(&mut rand::thread_rng()).unwrap();
|
||||
let sig =
|
||||
mango_client.perp_liq_force_cancel_orders((pubkey, &account), perp_market_index)?;
|
||||
log::info!(
|
||||
"Force cancelled perp orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||
pubkey,
|
||||
perp_market_index,
|
||||
maint_health,
|
||||
sig
|
||||
);
|
||||
sig
|
||||
} else if !perp_base_positions.is_empty() {
|
||||
// Liquidate the highest-value perp base position
|
||||
let (perp_market_index, base_lots, price, _) = perp_base_positions.last().unwrap();
|
||||
let perp = mango_client.context.perp(*perp_market_index);
|
||||
|
||||
let (side, side_signum) = if *base_lots > 0 {
|
||||
(Side::Bid, 1)
|
||||
} else {
|
||||
(Side::Ask, -1)
|
||||
};
|
||||
|
||||
// Compute the max number of base_lots the liqor is willing to take
|
||||
let max_base_transfer_abs = {
|
||||
let mut liqor = account_fetcher
|
||||
.fetch_fresh_mango_account(&mango_client.mango_account_address)
|
||||
.context("getting liquidator account")?;
|
||||
liqor.ensure_perp_position(*perp_market_index, QUOTE_TOKEN_INDEX)?;
|
||||
let health_cache = new_health_cache_(&mango_client.context, account_fetcher, &liqor)
|
||||
.expect("always ok");
|
||||
health_cache.max_perp_for_health_ratio(
|
||||
*perp_market_index,
|
||||
*price,
|
||||
perp.market.base_lot_size,
|
||||
side,
|
||||
min_health_ratio,
|
||||
)?
|
||||
};
|
||||
log::info!("computed max_base_transfer to be {max_base_transfer_abs}");
|
||||
|
||||
let sig = mango_client.perp_liq_base_position(
|
||||
(pubkey, &account),
|
||||
*perp_market_index,
|
||||
side_signum * max_base_transfer_abs,
|
||||
)?;
|
||||
log::info!(
|
||||
"Liquidated base position for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||
pubkey,
|
||||
perp_market_index,
|
||||
maint_health,
|
||||
sig
|
||||
);
|
||||
sig
|
||||
} else if is_spot_bankrupt {
|
||||
if tokens.is_empty() {
|
||||
anyhow::bail!("mango account {}, is bankrupt has no active tokens", pubkey);
|
||||
|
@ -240,7 +325,7 @@ pub fn maybe_liquidate_account(
|
|||
let max_liab_transfer = get_max_liab_transfer(liab_token_index, quote_token_index)?;
|
||||
|
||||
let sig = mango_client
|
||||
.liq_token_bankruptcy((pubkey, &account), liab_token_index, max_liab_transfer)
|
||||
.token_liq_bankruptcy((pubkey, &account), liab_token_index, max_liab_transfer)
|
||||
.context("sending liq_token_bankruptcy")?;
|
||||
log::info!(
|
||||
"Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}",
|
||||
|
@ -288,7 +373,7 @@ pub fn maybe_liquidate_account(
|
|||
// TODO: log liquee's liab_needed, need to refactor program code to be able to be accessed from client side
|
||||
//
|
||||
let sig = mango_client
|
||||
.liq_token_with_token(
|
||||
.token_liq_with_token(
|
||||
(pubkey, &account),
|
||||
asset_token_index,
|
||||
liab_token_index,
|
||||
|
|
|
@ -8,6 +8,7 @@ use client::{chain_data, keypair_from_cli, Client, MangoClient, MangoGroupContex
|
|||
use log::*;
|
||||
use mango_v4::state::{PerpMarketIndex, TokenIndex};
|
||||
|
||||
use itertools::Itertools;
|
||||
use solana_sdk::commitment_config::CommitmentConfig;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::collections::HashSet;
|
||||
|
@ -126,6 +127,8 @@ async fn main() -> anyhow::Result<()> {
|
|||
.tokens
|
||||
.values()
|
||||
.map(|value| value.mint_info.oracle)
|
||||
.chain(group_context.perp_markets.values().map(|p| p.market.oracle))
|
||||
.unique()
|
||||
.collect::<Vec<Pubkey>>();
|
||||
|
||||
//
|
||||
|
|
|
@ -34,7 +34,14 @@ import {
|
|||
PerpPosition,
|
||||
} from './accounts/mangoAccount';
|
||||
import { StubOracle } from './accounts/oracle';
|
||||
import { PerpMarket, PerpOrderSide, PerpOrderType } from './accounts/perp';
|
||||
import {
|
||||
PerpEventQueue,
|
||||
PerpMarket,
|
||||
PerpOrderType,
|
||||
PerpOrderSide,
|
||||
FillEvent,
|
||||
OutEvent,
|
||||
} from './accounts/perp';
|
||||
import {
|
||||
generateSerum3MarketExternalVaultSignerAddress,
|
||||
Serum3Market,
|
||||
|
@ -541,9 +548,20 @@ export class MangoClient {
|
|||
group: Group,
|
||||
accountNumber?: number,
|
||||
name?: string,
|
||||
tokenCount?: number,
|
||||
serum3Count?: number,
|
||||
perpCount?: number,
|
||||
perpOoCount?: number,
|
||||
): Promise<TransactionSignature> {
|
||||
const transaction = await this.program.methods
|
||||
.accountCreate(accountNumber ?? 0, 8, 8, 0, 0, name ?? '')
|
||||
.accountCreate(
|
||||
accountNumber ?? 0,
|
||||
tokenCount ?? 8,
|
||||
serum3Count ?? 8,
|
||||
perpCount ?? 0,
|
||||
perpOoCount ?? 0,
|
||||
name ?? '',
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
|
@ -560,6 +578,32 @@ export class MangoClient {
|
|||
);
|
||||
}
|
||||
|
||||
public async createAndFetchMangoAccount(
|
||||
group: Group,
|
||||
accountNumber?: number,
|
||||
name?: string,
|
||||
tokenCount?: number,
|
||||
serum3Count?: number,
|
||||
perpCount?: number,
|
||||
perpOoCount?: number,
|
||||
): Promise<MangoAccount | undefined> {
|
||||
const accNum = accountNumber ?? 0;
|
||||
await this.createMangoAccount(
|
||||
group,
|
||||
accNum,
|
||||
name,
|
||||
tokenCount,
|
||||
serum3Count,
|
||||
perpCount,
|
||||
perpOoCount,
|
||||
);
|
||||
return await this.getMangoAccountForOwner(
|
||||
group,
|
||||
(this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
accNum,
|
||||
);
|
||||
}
|
||||
|
||||
public async expandMangoAccount(
|
||||
group: Group,
|
||||
account: MangoAccount,
|
||||
|
@ -1511,6 +1555,37 @@ export class MangoClient {
|
|||
);
|
||||
}
|
||||
|
||||
async perpDeactivatePosition(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
perpMarketName: string,
|
||||
): Promise<TransactionSignature> {
|
||||
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
|
||||
const healthRemainingAccounts: PublicKey[] =
|
||||
this.buildHealthRemainingAccounts(
|
||||
AccountRetriever.Fixed,
|
||||
group,
|
||||
[mangoAccount],
|
||||
[],
|
||||
[],
|
||||
);
|
||||
return await this.program.methods
|
||||
.perpDeactivatePosition()
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
account: mangoAccount.publicKey,
|
||||
perpMarket: perpMarket.publicKey,
|
||||
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
})
|
||||
.remainingAccounts(
|
||||
healthRemainingAccounts.map(
|
||||
(pk) =>
|
||||
({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta),
|
||||
),
|
||||
)
|
||||
.rpc();
|
||||
}
|
||||
|
||||
async perpPlaceOrder(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
|
@ -1586,6 +1661,60 @@ export class MangoClient {
|
|||
.rpc();
|
||||
}
|
||||
|
||||
async perpConsumeEvents(
|
||||
group: Group,
|
||||
perpMarketName: string,
|
||||
accounts: PublicKey[],
|
||||
limit: number,
|
||||
): Promise<TransactionSignature> {
|
||||
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
|
||||
return await this.program.methods
|
||||
.perpConsumeEvents(new BN(limit))
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
perpMarket: perpMarket.publicKey,
|
||||
eventQueue: perpMarket.eventQueue,
|
||||
})
|
||||
.remainingAccounts(
|
||||
accounts.map(
|
||||
(pk) =>
|
||||
({ pubkey: pk, isWritable: true, isSigner: false } as AccountMeta),
|
||||
),
|
||||
)
|
||||
.rpc();
|
||||
}
|
||||
|
||||
async perpConsumeAllEvents(
|
||||
group: Group,
|
||||
perpMarketName: string,
|
||||
): Promise<void> {
|
||||
const limit = 8;
|
||||
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
|
||||
const eventQueue = await perpMarket.loadEventQueue(this);
|
||||
let unconsumedEvents = eventQueue.getUnconsumedEvents();
|
||||
while (unconsumedEvents.length > 0) {
|
||||
const events = unconsumedEvents.splice(0, limit);
|
||||
const accounts = events
|
||||
.map((ev) => {
|
||||
switch (ev.eventType) {
|
||||
case PerpEventQueue.FILL_EVENT_TYPE:
|
||||
const fill = <FillEvent>ev;
|
||||
return [fill.maker, fill.taker];
|
||||
case PerpEventQueue.OUT_EVENT_TYPE:
|
||||
const out = <OutEvent>ev;
|
||||
return [out.owner];
|
||||
case PerpEventQueue.LIQUIDATE_EVENT_TYPE:
|
||||
return [];
|
||||
default:
|
||||
throw new Error(`Unknown event with eventType ${ev.eventType}`);
|
||||
}
|
||||
})
|
||||
.flat();
|
||||
|
||||
await this.perpConsumeEvents(group, perpMarketName, accounts, limit);
|
||||
}
|
||||
}
|
||||
|
||||
public async marginTrade({
|
||||
group,
|
||||
mangoAccount,
|
||||
|
|
|
@ -45,15 +45,6 @@ async function main() {
|
|||
|
||||
let sig;
|
||||
|
||||
// close stub oracles
|
||||
const stubOracles = await client.getStubOracle(group);
|
||||
for (const stubOracle of stubOracles) {
|
||||
sig = await client.stubOracleClose(group, stubOracle.publicKey);
|
||||
console.log(
|
||||
`Closed stub oracle ${stubOracle.publicKey}, sig https://explorer.solana.com/tx/${sig}`,
|
||||
);
|
||||
}
|
||||
|
||||
// close all banks
|
||||
for (const banks of group.banksMapByMint.values()) {
|
||||
sig = await client.tokenDeregister(group, banks[0].mint);
|
||||
|
@ -81,6 +72,15 @@ async function main() {
|
|||
);
|
||||
}
|
||||
|
||||
// close stub oracles
|
||||
const stubOracles = await client.getStubOracle(group);
|
||||
for (const stubOracle of stubOracles) {
|
||||
sig = await client.stubOracleClose(group, stubOracle.publicKey);
|
||||
console.log(
|
||||
`Closed stub oracle ${stubOracle.publicKey}, sig https://explorer.solana.com/tx/${sig}`,
|
||||
);
|
||||
}
|
||||
|
||||
// finally, close the group
|
||||
sig = await client.groupClose(group);
|
||||
console.log(`Closed group, sig https://explorer.solana.com/tx/${sig}`);
|
||||
|
|
|
@ -16,12 +16,14 @@ const MAINNET_MINTS = new Map([
|
|||
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
|
||||
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
|
||||
['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
|
||||
]);
|
||||
|
||||
// External markets are matched with those in https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json
|
||||
|
@ -179,14 +181,48 @@ async function main() {
|
|||
}
|
||||
|
||||
console.log('Registering SOL/USDC serum market...');
|
||||
await client.serum3RegisterMarket(
|
||||
group,
|
||||
new PublicKey(MAINNET_SERUM3_MARKETS.get('SOL/USDC')!),
|
||||
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('SOL')!)),
|
||||
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('USDC')!)),
|
||||
1,
|
||||
'SOL/USDC',
|
||||
);
|
||||
try {
|
||||
await client.serum3RegisterMarket(
|
||||
group,
|
||||
new PublicKey(MAINNET_SERUM3_MARKETS.get('SOL/USDC')!),
|
||||
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('SOL')!)),
|
||||
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('USDC')!)),
|
||||
1,
|
||||
'SOL/USDC',
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
console.log('Registering MNGO-PERP market...');
|
||||
const mngoMainnetOracle = oracles.get('MNGO');
|
||||
try {
|
||||
await client.perpCreateMarket(
|
||||
group,
|
||||
mngoMainnetOracle,
|
||||
0,
|
||||
'MNGO-PERP',
|
||||
0.1,
|
||||
9,
|
||||
0,
|
||||
10,
|
||||
100000, // base lots
|
||||
0.9,
|
||||
0.8,
|
||||
1.1,
|
||||
1.2,
|
||||
0.05,
|
||||
-0.001,
|
||||
0.002,
|
||||
-0.1,
|
||||
0.1,
|
||||
10,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
|
|
@ -39,11 +39,14 @@ async function main() {
|
|||
|
||||
// create + fetch account
|
||||
console.log(`Creating mangoaccount...`);
|
||||
const mangoAccount = (await client.getOrCreateMangoAccount(
|
||||
const mangoAccount = (await client.createAndFetchMangoAccount(
|
||||
group,
|
||||
admin.publicKey,
|
||||
ACCOUNT_NUM,
|
||||
'LIQTEST, FUNDING',
|
||||
8,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
))!;
|
||||
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString());
|
||||
|
@ -54,16 +57,16 @@ async function main() {
|
|||
|
||||
// deposit
|
||||
try {
|
||||
console.log(`...depositing 10 USDC`);
|
||||
await client.tokenDeposit(group, mangoAccount, usdcMint, 10);
|
||||
console.log(`...depositing 5 USDC`);
|
||||
await client.tokenDeposit(group, mangoAccount, usdcMint, 5);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(`...depositing 0.0004 BTC`);
|
||||
await client.tokenDeposit(group, mangoAccount, btcMint, 0.0004);
|
||||
console.log(`...depositing 0.0002 BTC`);
|
||||
await client.tokenDeposit(group, mangoAccount, btcMint, 0.0002);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(`...depositing 0.25 SOL`);
|
||||
await client.tokenDeposit(group, mangoAccount, solMint, 0.25);
|
||||
console.log(`...depositing 0.15 SOL`);
|
||||
await client.tokenDeposit(group, mangoAccount, solMint, 0.15);
|
||||
await mangoAccount.reload(client, group);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
|
|
@ -6,6 +6,8 @@ import {
|
|||
Serum3SelfTradeBehavior,
|
||||
Serum3Side,
|
||||
} from '../accounts/serum3';
|
||||
import { Side, PerpOrderType } from '../accounts/perp';
|
||||
import { MangoAccount } from '../accounts/mangoAccount';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
|
@ -26,6 +28,7 @@ const MAINNET_MINTS = new Map([
|
|||
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
|
||||
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
|
||||
['SOL', 'So11111111111111111111111111111111111111112'],
|
||||
['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'],
|
||||
]);
|
||||
|
||||
const TOKEN_SCENARIOS: [string, string, number, string, number][] = [
|
||||
|
@ -66,19 +69,30 @@ async function main() {
|
|||
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;
|
||||
maxAccountNum = maxAccountNum + 1;
|
||||
await client.createMangoAccount(group, accountNum, name, 4, 4, 4, 4);
|
||||
return (await client.getMangoAccountForOwner(
|
||||
group,
|
||||
admin.publicKey,
|
||||
accountNum,
|
||||
))!;
|
||||
}
|
||||
|
||||
for (const scenario of TOKEN_SCENARIOS) {
|
||||
const [name, assetName, assetAmount, liabName, liabAmount] = scenario;
|
||||
|
||||
// create account
|
||||
console.log(`Creating mangoaccount...`);
|
||||
let mangoAccount = (await client.getOrCreateMangoAccount(
|
||||
group,
|
||||
admin.publicKey,
|
||||
maxAccountNum + 1,
|
||||
name,
|
||||
))!;
|
||||
maxAccountNum = maxAccountNum + 1;
|
||||
let mangoAccount = await createMangoAccount(name);
|
||||
console.log(
|
||||
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
|
||||
);
|
||||
|
@ -119,13 +133,7 @@ async function main() {
|
|||
const name = 'LIQTEST, serum orders';
|
||||
|
||||
console.log(`Creating mangoaccount...`);
|
||||
let mangoAccount = (await client.getOrCreateMangoAccount(
|
||||
group,
|
||||
admin.publicKey,
|
||||
maxAccountNum + 1,
|
||||
name,
|
||||
))!;
|
||||
maxAccountNum = maxAccountNum + 1;
|
||||
let mangoAccount = await createMangoAccount(name);
|
||||
console.log(
|
||||
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
|
||||
);
|
||||
|
@ -188,6 +196,108 @@ async function main() {
|
|||
}
|
||||
}
|
||||
|
||||
// Perp orders bring health <0, liquidator force closes
|
||||
{
|
||||
const name = 'LIQTEST, perp orders';
|
||||
|
||||
console.log(`Creating mangoaccount...`);
|
||||
let mangoAccount = await createMangoAccount(name);
|
||||
console.log(
|
||||
`...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;
|
||||
|
||||
await client.tokenDepositNative(
|
||||
group,
|
||||
mangoAccount,
|
||||
collateralMint,
|
||||
100000,
|
||||
); // valued as $0.004 maint collateral
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 4);
|
||||
|
||||
try {
|
||||
await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
'MNGO-PERP',
|
||||
Side.bid,
|
||||
1, // ui price that won't get hit
|
||||
0.0011, // ui base quantity, 11 base lots, $0.044
|
||||
0.044, // ui quote quantity
|
||||
4200,
|
||||
PerpOrderType.limit,
|
||||
0,
|
||||
5,
|
||||
);
|
||||
} finally {
|
||||
await client.stubOracleSet(group, collateralOracle, PRICES['SOL']);
|
||||
}
|
||||
}
|
||||
|
||||
// Perp base pos brings health<0, liquidator takes most of it
|
||||
{
|
||||
const name = 'LIQTEST, perp base pos';
|
||||
|
||||
console.log(`Creating mangoaccount...`);
|
||||
let mangoAccount = await createMangoAccount(name);
|
||||
console.log(
|
||||
`...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;
|
||||
|
||||
await client.tokenDepositNative(
|
||||
group,
|
||||
mangoAccount,
|
||||
collateralMint,
|
||||
100000,
|
||||
); // valued as $0.004 maint collateral
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 5);
|
||||
|
||||
try {
|
||||
await client.perpPlaceOrder(
|
||||
group,
|
||||
fundingAccount,
|
||||
'MNGO-PERP',
|
||||
Side.ask,
|
||||
40,
|
||||
0.0011, // ui base quantity, 11 base lots, $0.044
|
||||
0.044, // ui quote quantity
|
||||
4200,
|
||||
PerpOrderType.limit,
|
||||
0,
|
||||
5,
|
||||
);
|
||||
|
||||
await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
'MNGO-PERP',
|
||||
Side.bid,
|
||||
40,
|
||||
0.0011, // ui base quantity, 11 base lots, $0.044
|
||||
0.044, // ui quote quantity
|
||||
4200,
|
||||
PerpOrderType.market,
|
||||
0,
|
||||
5,
|
||||
);
|
||||
|
||||
await client.perpConsumeAllEvents(group, 'MNGO-PERP');
|
||||
} finally {
|
||||
await client.stubOracleSet(group, collateralOracle, PRICES['SOL']);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { BN, AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { MangoClient } from '../client';
|
||||
import { Side, PerpOrderType } from '../accounts/perp';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
|
@ -57,63 +58,18 @@ async function main() {
|
|||
await client.serum3SettleFunds(group, account, serumExternal);
|
||||
await client.serum3CloseOpenOrders(group, account, serumExternal);
|
||||
}
|
||||
}
|
||||
|
||||
accounts = await client.getMangoAccountsForOwner(group, admin.publicKey);
|
||||
for (let account of accounts) {
|
||||
console.log(`settling borrows on account: ${account}`);
|
||||
|
||||
// first, settle all borrows
|
||||
for (let token of account.tokensActive()) {
|
||||
const bank = group.getFirstBankByTokenIndex(token.tokenIndex);
|
||||
const amount = token.balance(bank).toNumber();
|
||||
if (amount < 0) {
|
||||
try {
|
||||
await client.tokenDepositNative(
|
||||
group,
|
||||
account,
|
||||
bank.mint,
|
||||
Math.ceil(-amount),
|
||||
);
|
||||
await account.reload(client, group);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`failed to deposit ${bank.name} into ${account.publicKey}: ${error}`,
|
||||
);
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
for (let perpPosition of account.perpActive()) {
|
||||
const perpMarket = group.findPerpMarket(perpPosition.marketIndex)!;
|
||||
console.log(
|
||||
`closing perp orders on: ${account} for market ${perpMarket.name}`,
|
||||
);
|
||||
await client.perpCancelAllOrders(group, account, perpMarket.name, 10);
|
||||
}
|
||||
}
|
||||
|
||||
accounts = await client.getMangoAccountsForOwner(group, admin.publicKey);
|
||||
for (let account of accounts) {
|
||||
console.log(`withdrawing deposits of account: ${account}`);
|
||||
|
||||
// withdraw all funds
|
||||
for (let token of account.tokensActive()) {
|
||||
const bank = group.getFirstBankByTokenIndex(token.tokenIndex);
|
||||
const amount = token.balance(bank).toNumber();
|
||||
if (amount > 0) {
|
||||
try {
|
||||
const allowBorrow = false;
|
||||
await client.tokenWithdrawNative(
|
||||
group,
|
||||
account,
|
||||
bank.mint,
|
||||
amount,
|
||||
allowBorrow,
|
||||
);
|
||||
await account.reload(client, group);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`failed to withdraw ${bank.name} from ${account.publicKey}: ${error}`,
|
||||
);
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// close account
|
||||
try {
|
||||
console.log(`closing account: ${account}`);
|
||||
|
|
Loading…
Reference in New Issue