liquidator: force-cancel perp orders, liq perp base positions

This commit is contained in:
Christian Kamm 2022-09-15 09:57:48 +02:00
parent a97b40a521
commit 9cbc352197
9 changed files with 512 additions and 102 deletions

View File

@ -15,14 +15,16 @@ use bincode::Options;
use fixed::types::I80F48; use fixed::types::I80F48;
use itertools::Itertools; use itertools::Itertools;
use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side}; 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::nonblocking::rpc_client::RpcClient as RpcClientAsync;
use solana_client::rpc_client::RpcClient; use solana_client::rpc_client::RpcClient;
use solana_sdk::signer::keypair; use solana_sdk::signer::keypair;
use crate::account_fetcher::*; 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::gpa::fetch_mango_accounts;
use crate::jupiter; use crate::jupiter;
use crate::util::MyClone; use crate::util::MyClone;
@ -836,12 +838,98 @@ impl MangoClient {
// //
// Perps // 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 // Liquidation
// //
pub fn liq_token_with_token( pub fn token_liq_with_token(
&self, &self,
liqee: (&Pubkey, &MangoAccountValue), liqee: (&Pubkey, &MangoAccountValue),
asset_token_index: TokenIndex, asset_token_index: TokenIndex,
@ -886,7 +974,7 @@ impl MangoClient {
.map_err(prettify_client_error) .map_err(prettify_client_error)
} }
pub fn liq_token_bankruptcy( pub fn token_liq_bankruptcy(
&self, &self,
liqee: (&Pubkey, &MangoAccountValue), liqee: (&Pubkey, &MangoAccountValue),
liab_token_index: TokenIndex, liab_token_index: TokenIndex,

View File

@ -5,7 +5,7 @@ use crate::account_shared_data::KeyedAccountSharedData;
use client::{chain_data, AccountFetcher, MangoClient, MangoClientError, MangoGroupContext}; use client::{chain_data, AccountFetcher, MangoClient, MangoClientError, MangoGroupContext};
use mango_v4::state::{ use mango_v4::state::{
new_health_cache, Bank, FixedOrderAccountRetriever, HealthCache, HealthType, MangoAccountValue, new_health_cache, Bank, FixedOrderAccountRetriever, HealthCache, HealthType, MangoAccountValue,
Serum3Orders, TokenIndex, QUOTE_TOKEN_INDEX, PerpMarketIndex, Serum3Orders, Side, TokenIndex, QUOTE_TOKEN_INDEX,
}; };
use itertools::Itertools; use itertools::Itertools;
@ -177,6 +177,36 @@ pub fn maybe_liquidate_account(
.filter_map_ok(|v| v) .filter_map_ok(|v| v)
.collect::<anyhow::Result<Vec<Serum3Orders>>>()?; .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 get_max_liab_transfer = |source, target| -> anyhow::Result<I80F48> {
let mut liqor = account_fetcher let mut liqor = account_fetcher
.fetch_fresh_mango_account(&mango_client.mango_account_address) .fetch_fresh_mango_account(&mango_client.mango_account_address)
@ -203,7 +233,7 @@ pub fn maybe_liquidate_account(
// try liquidating // try liquidating
let txsig = if !serum_force_cancels.is_empty() { 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 serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap();
let sig = mango_client.serum3_liq_force_cancel_orders( let sig = mango_client.serum3_liq_force_cancel_orders(
(pubkey, &account), (pubkey, &account),
@ -211,13 +241,68 @@ pub fn maybe_liquidate_account(
&serum_orders.open_orders, &serum_orders.open_orders,
)?; )?;
log::info!( 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, pubkey,
serum_orders.market_index, serum_orders.market_index,
maint_health, maint_health,
sig sig
); );
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 { } else if is_spot_bankrupt {
if tokens.is_empty() { if tokens.is_empty() {
anyhow::bail!("mango account {}, is bankrupt has no active tokens", pubkey); 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 max_liab_transfer = get_max_liab_transfer(liab_token_index, quote_token_index)?;
let sig = mango_client 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")?; .context("sending liq_token_bankruptcy")?;
log::info!( log::info!(
"Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}", "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 // TODO: log liquee's liab_needed, need to refactor program code to be able to be accessed from client side
// //
let sig = mango_client let sig = mango_client
.liq_token_with_token( .token_liq_with_token(
(pubkey, &account), (pubkey, &account),
asset_token_index, asset_token_index,
liab_token_index, liab_token_index,

View File

@ -8,6 +8,7 @@ use client::{chain_data, keypair_from_cli, Client, MangoClient, MangoGroupContex
use log::*; use log::*;
use mango_v4::state::{PerpMarketIndex, TokenIndex}; use mango_v4::state::{PerpMarketIndex, TokenIndex};
use itertools::Itertools;
use solana_sdk::commitment_config::CommitmentConfig; use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::pubkey::Pubkey; use solana_sdk::pubkey::Pubkey;
use std::collections::HashSet; use std::collections::HashSet;
@ -126,6 +127,8 @@ async fn main() -> anyhow::Result<()> {
.tokens .tokens
.values() .values()
.map(|value| value.mint_info.oracle) .map(|value| value.mint_info.oracle)
.chain(group_context.perp_markets.values().map(|p| p.market.oracle))
.unique()
.collect::<Vec<Pubkey>>(); .collect::<Vec<Pubkey>>();
// //

View File

@ -34,7 +34,14 @@ import {
PerpPosition, PerpPosition,
} from './accounts/mangoAccount'; } from './accounts/mangoAccount';
import { StubOracle } from './accounts/oracle'; import { StubOracle } from './accounts/oracle';
import { PerpMarket, PerpOrderSide, PerpOrderType } from './accounts/perp'; import {
PerpEventQueue,
PerpMarket,
PerpOrderType,
PerpOrderSide,
FillEvent,
OutEvent,
} from './accounts/perp';
import { import {
generateSerum3MarketExternalVaultSignerAddress, generateSerum3MarketExternalVaultSignerAddress,
Serum3Market, Serum3Market,
@ -541,9 +548,20 @@ export class MangoClient {
group: Group, group: Group,
accountNumber?: number, accountNumber?: number,
name?: string, name?: string,
tokenCount?: number,
serum3Count?: number,
perpCount?: number,
perpOoCount?: number,
): Promise<TransactionSignature> { ): Promise<TransactionSignature> {
const transaction = await this.program.methods 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({ .accounts({
group: group.publicKey, group: group.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.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( public async expandMangoAccount(
group: Group, group: Group,
account: MangoAccount, 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( async perpPlaceOrder(
group: Group, group: Group,
mangoAccount: MangoAccount, mangoAccount: MangoAccount,
@ -1586,6 +1661,60 @@ export class MangoClient {
.rpc(); .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({ public async marginTrade({
group, group,
mangoAccount, mangoAccount,

View File

@ -45,15 +45,6 @@ async function main() {
let sig; 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 // close all banks
for (const banks of group.banksMapByMint.values()) { for (const banks of group.banksMapByMint.values()) {
sig = await client.tokenDeregister(group, banks[0].mint); 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 // finally, close the group
sig = await client.groupClose(group); sig = await client.groupClose(group);
console.log(`Closed group, sig https://explorer.solana.com/tx/${sig}`); console.log(`Closed group, sig https://explorer.solana.com/tx/${sig}`);

View File

@ -16,12 +16,14 @@ const MAINNET_MINTS = new Map([
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'], ['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'], ['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
['SOL', 'So11111111111111111111111111111111111111112'], ['SOL', 'So11111111111111111111111111111111111111112'],
['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'],
]); ]);
const STUB_PRICES = new Map([ const STUB_PRICES = new Map([
['USDC', 1.0], ['USDC', 1.0],
['BTC', 20000.0], // btc and usdc both have 6 decimals ['BTC', 20000.0], // btc and usdc both have 6 decimals
['SOL', 0.04], // sol has 9 decimals, equivalent to $40 per SOL ['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 // 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...'); console.log('Registering SOL/USDC serum market...');
await client.serum3RegisterMarket( try {
group, await client.serum3RegisterMarket(
new PublicKey(MAINNET_SERUM3_MARKETS.get('SOL/USDC')!), group,
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('SOL')!)), new PublicKey(MAINNET_SERUM3_MARKETS.get('SOL/USDC')!),
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('USDC')!)), group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('SOL')!)),
1, group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('USDC')!)),
'SOL/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(); process.exit();
} }

View File

@ -39,11 +39,14 @@ async function main() {
// create + fetch account // create + fetch account
console.log(`Creating mangoaccount...`); console.log(`Creating mangoaccount...`);
const mangoAccount = (await client.getOrCreateMangoAccount( const mangoAccount = (await client.createAndFetchMangoAccount(
group, group,
admin.publicKey,
ACCOUNT_NUM, ACCOUNT_NUM,
'LIQTEST, FUNDING', 'LIQTEST, FUNDING',
8,
4,
4,
4,
))!; ))!;
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString()); console.log(mangoAccount.toString());
@ -54,16 +57,16 @@ async function main() {
// deposit // deposit
try { try {
console.log(`...depositing 10 USDC`); console.log(`...depositing 5 USDC`);
await client.tokenDeposit(group, mangoAccount, usdcMint, 10); await client.tokenDeposit(group, mangoAccount, usdcMint, 5);
await mangoAccount.reload(client, group); await mangoAccount.reload(client, group);
console.log(`...depositing 0.0004 BTC`); console.log(`...depositing 0.0002 BTC`);
await client.tokenDeposit(group, mangoAccount, btcMint, 0.0004); await client.tokenDeposit(group, mangoAccount, btcMint, 0.0002);
await mangoAccount.reload(client, group); await mangoAccount.reload(client, group);
console.log(`...depositing 0.25 SOL`); console.log(`...depositing 0.15 SOL`);
await client.tokenDeposit(group, mangoAccount, solMint, 0.25); await client.tokenDeposit(group, mangoAccount, solMint, 0.15);
await mangoAccount.reload(client, group); await mangoAccount.reload(client, group);
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@ -6,6 +6,8 @@ import {
Serum3SelfTradeBehavior, Serum3SelfTradeBehavior,
Serum3Side, Serum3Side,
} from '../accounts/serum3'; } from '../accounts/serum3';
import { Side, PerpOrderType } from '../accounts/perp';
import { MangoAccount } from '../accounts/mangoAccount';
import { MangoClient } from '../client'; import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants'; import { MANGO_V4_ID } from '../constants';
@ -26,6 +28,7 @@ const MAINNET_MINTS = new Map([
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'], ['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'], ['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
['SOL', 'So11111111111111111111111111111111111111112'], ['SOL', 'So11111111111111111111111111111111111111112'],
['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'],
]); ]);
const TOKEN_SCENARIOS: [string, string, number, string, number][] = [ const TOKEN_SCENARIOS: [string, string, number, string, number][] = [
@ -66,19 +69,30 @@ async function main() {
admin.publicKey, admin.publicKey,
); );
let maxAccountNum = Math.max(0, ...accounts.map((a) => a.accountNum)); 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) { for (const scenario of TOKEN_SCENARIOS) {
const [name, assetName, assetAmount, liabName, liabAmount] = scenario; const [name, assetName, assetAmount, liabName, liabAmount] = scenario;
// create account // create account
console.log(`Creating mangoaccount...`); console.log(`Creating mangoaccount...`);
let mangoAccount = (await client.getOrCreateMangoAccount( let mangoAccount = await createMangoAccount(name);
group,
admin.publicKey,
maxAccountNum + 1,
name,
))!;
maxAccountNum = maxAccountNum + 1;
console.log( console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`, `...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
); );
@ -119,13 +133,7 @@ async function main() {
const name = 'LIQTEST, serum orders'; const name = 'LIQTEST, serum orders';
console.log(`Creating mangoaccount...`); console.log(`Creating mangoaccount...`);
let mangoAccount = (await client.getOrCreateMangoAccount( let mangoAccount = await createMangoAccount(name);
group,
admin.publicKey,
maxAccountNum + 1,
name,
))!;
maxAccountNum = maxAccountNum + 1;
console.log( console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`, `...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(); process.exit();
} }

View File

@ -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 { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs'; import fs from 'fs';
import { MangoClient } from '../client'; import { MangoClient } from '../client';
import { Side, PerpOrderType } from '../accounts/perp';
import { MANGO_V4_ID } from '../constants'; import { MANGO_V4_ID } from '../constants';
// //
@ -57,63 +58,18 @@ async function main() {
await client.serum3SettleFunds(group, account, serumExternal); await client.serum3SettleFunds(group, account, serumExternal);
await client.serum3CloseOpenOrders(group, account, serumExternal); await client.serum3CloseOpenOrders(group, account, serumExternal);
} }
}
accounts = await client.getMangoAccountsForOwner(group, admin.publicKey); for (let perpPosition of account.perpActive()) {
for (let account of accounts) { const perpMarket = group.findPerpMarket(perpPosition.marketIndex)!;
console.log(`settling borrows on account: ${account}`); console.log(
`closing perp orders on: ${account} for market ${perpMarket.name}`,
// first, settle all borrows );
for (let token of account.tokensActive()) { await client.perpCancelAllOrders(group, account, perpMarket.name, 10);
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();
}
}
} }
} }
accounts = await client.getMangoAccountsForOwner(group, admin.publicKey); accounts = await client.getMangoAccountsForOwner(group, admin.publicKey);
for (let account of accounts) { 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 // close account
try { try {
console.log(`closing account: ${account}`); console.log(`closing account: ${account}`);