liquidator/src/partial.ts

407 lines
15 KiB
TypeScript
Raw Normal View History

2021-04-06 17:43:51 -07:00
import {
findLargestTokenAccountForOwner,
getMultipleAccounts,
IDS,
2021-04-09 13:18:06 -07:00
MangoClient,
MangoGroup, MarginAccount,
2021-04-09 13:18:06 -07:00
nativeToUi,
NUM_MARKETS,
NUM_TOKENS,
parseTokenAccount,
parseTokenAccountData,
tokenToDecimals,
2021-04-06 17:43:51 -07:00
} from '@blockworks-foundation/mango-client';
import { Account, Commitment, Connection, PublicKey, Transaction } from '@solana/web3.js';
2021-04-06 17:43:51 -07:00
import { homedir } from 'os';
import fs from 'fs';
import { notify, sleep } from './utils';
2021-04-09 13:18:06 -07:00
import { Market, OpenOrders } from '@project-serum/serum';
2021-04-06 17:43:51 -07:00
import {
makeForceCancelOrdersInstruction,
makePartialLiquidateInstruction,
} from '@blockworks-foundation/mango-client/lib/instruction';
2021-04-09 13:18:06 -07:00
import BN = require('bn.js');
/*
After a liquidation, the amounts in each wallet become unbalanced
Make sure to sell or buy quantities different from the target on base currencies
Convert excess into quote currency
*/
async function balanceWallets(
connection: Connection,
mangoGroup: MangoGroup,
prices: number[],
markets: Market[],
liqor: Account,
liqorWallets: PublicKey[],
liqorValuesUi: number[],
2021-04-09 14:37:10 -07:00
liqorOpenOrdersKeys: PublicKey[],
targets: number[]
2021-04-09 13:18:06 -07:00
) {
// Retrieve orders from order book by owner
const liqorOrders = await Promise.all(markets.map((m) =>
m.loadOrdersForOwner(connection, liqor.publicKey)));
// Cancel all
const cancelTransactions: Promise<string>[] = []
for (let i = 0; i < NUM_MARKETS; i++) {
for (let order of liqorOrders[i]) {
console.log(`Cancelling liqor order on market ${i} size=${order.size} price=${order.price}`)
cancelTransactions.push(markets[i].cancelOrder(connection, liqor, order));
}
}
await Promise.all(cancelTransactions)
// Load open orders accounts
2021-04-09 13:18:06 -07:00
const liqorOpenOrders = await Promise.all(liqorOpenOrdersKeys.map((pk) => OpenOrders.load(connection, pk, mangoGroup.dexProgramId)))
// Settle all
const settleTransactions: Promise<string>[] = []
2021-04-09 13:18:06 -07:00
for (let i = 0; i < NUM_MARKETS; i++) {
const oo = liqorOpenOrders[i]
if (parseFloat(oo.quoteTokenTotal.toString()) > 0 || parseFloat(oo.baseTokenTotal.toString()) > 0) {
console.log(`Settling funds to liqor wallet ${i} quote:${oo.quoteTokenTotal.toString()} base:${oo.baseTokenTotal.toString()}`)
settleTransactions.push(markets[i].settleFunds(connection, liqor, oo, liqorWallets[i], liqorWallets[NUM_TOKENS-1]))
2021-04-09 13:18:06 -07:00
}
}
await Promise.all(settleTransactions)
2021-04-09 13:18:06 -07:00
// Account wallets should instantly update
const liqorWalletAccounts = await getMultipleAccounts(connection, liqorWallets, 'processed' as Commitment)
liqorValuesUi = liqorWalletAccounts.map(
(a, i) => nativeToUi(parseTokenAccountData(a.accountInfo.data).amount, mangoGroup.mintDecimals[i])
)
2021-04-09 13:18:06 -07:00
const diffs: number[] = []
const netValues: [number, number][] = []
// Go to each base currency and see if it's above or below target
for (let i = 0; i < NUM_TOKENS - 1; i++) {
const diff = liqorValuesUi[i] - targets[i]
diffs.push(diff)
netValues.push([i, diff * prices[i]])
}
// Sort in decreasing order so you sell first then buy
netValues.sort((a, b) => (b[1] - a[1]))
for (let i = 0; i < NUM_TOKENS - 1; i++) {
const marketIndex = netValues[i][0]
const market = markets[marketIndex]
const tokenDecimals = tokenToDecimals[marketIndex === 0 ? 'BTC' : 'ETH'] // TODO make this mapping allow arbitrary mango groups
2021-04-09 17:25:53 -07:00
const tokenDecimalAdj = Math.pow(10, tokenDecimals)
2021-04-09 13:18:06 -07:00
if (netValues[i][1] > 0) { // sell to close
const price = prices[marketIndex] * 0.95
const size = Math.floor(diffs[marketIndex] * tokenDecimalAdj) / tokenDecimalAdj // round down the size
if (size === 0) {
continue
}
console.log(`Sell to close ${marketIndex} ${size} @ ${price}`)
let txid = await market.placeOrder(
connection,
{
owner: liqor,
payer: liqorWallets[marketIndex],
side: 'sell',
price,
size,
orderType: 'ioc',
openOrdersAddressKey: liqorOpenOrdersKeys[marketIndex],
feeDiscountPubkey: null // TODO find liqor's SRM fee account
2021-04-09 13:18:06 -07:00
}
)
// TODO add a SettleFunds instruction to this transaction
2021-04-09 14:37:10 -07:00
console.log(`Place order successful: ${txid}; Settling funds`)
2021-04-09 13:18:06 -07:00
await market.settleFunds(connection, liqor, liqorOpenOrders[marketIndex], liqorWallets[marketIndex], liqorWallets[NUM_TOKENS-1])
} else if (netValues[i][1] < 0) { // buy to close
const price = prices[marketIndex] * 1.05 // buy at up to 5% higher than oracle price
const size = Math.ceil(-diffs[marketIndex] * tokenDecimalAdj) / tokenDecimalAdj
console.log(`Buy to close ${marketIndex} ${size} @ ${price}`)
let txid = await market.placeOrder(
connection,
{
owner: liqor,
payer: liqorWallets[NUM_TOKENS-1],
side: 'buy',
price,
size,
orderType: 'ioc',
openOrdersAddressKey: liqorOpenOrdersKeys[marketIndex],
feeDiscountPubkey: null
}
)
2021-04-09 14:37:10 -07:00
console.log(`Place order successful: ${txid}; Settling funds`)
2021-04-09 13:18:06 -07:00
await market.settleFunds(connection, liqor, liqorOpenOrders[marketIndex], liqorWallets[marketIndex], liqorWallets[NUM_TOKENS-1])
}
}
}
2021-04-06 17:43:51 -07:00
async function runPartialLiquidator() {
const client = new MangoClient()
const cluster = process.env.CLUSTER || 'mainnet-beta'
const group_name = process.env.GROUP_NAME || 'BTC_ETH_USDT'
const clusterUrl = process.env.CLUSTER_URL || IDS.cluster_urls[cluster]
2021-04-09 14:37:10 -07:00
const targetsStr = process.env.TARGETS || "0.1 2"
const checkInterval = parseFloat(process.env.CHECK_INTERVAL || "1000.0")
2021-04-09 14:37:10 -07:00
const targets = targetsStr.split(' ').map((s) => parseFloat(s))
2021-04-06 17:43:51 -07:00
const connection = new Connection(clusterUrl, 'singleGossip')
// The address of the Mango Program on the blockchain
const programId = new PublicKey(IDS[cluster].mango_program_id)
// The address of the serum dex program on the blockchain: https://github.com/project-serum/serum-dex
const dexProgramId = new PublicKey(IDS[cluster].dex_program_id)
// Address of the MangoGroup
const mangoGroupPk = new PublicKey(IDS[cluster].mango_groups[group_name].mango_group_pk)
// liquidator's keypair
const keyPairPath = process.env.KEYPAIR || homedir() + '/.config/solana/id.json'
const payer = new Account(JSON.parse(fs.readFileSync(keyPairPath, 'utf-8')))
2021-04-09 14:37:10 -07:00
notify(`partial liquidator launched cluster=${cluster} group=${group_name}`);
2021-04-06 17:43:51 -07:00
let mangoGroup = await client.getMangoGroup(connection, mangoGroupPk)
const tokenWallets = (await Promise.all(
mangoGroup.tokens.map(
(mint) => findLargestTokenAccountForOwner(connection, payer.publicKey, mint).then(
(response) => response.publicKey
)
)
))
// load all markets
const markets = await Promise.all(mangoGroup.spotMarkets.map(
(pk) => Market.load(connection, pk, {skipPreflight: true, commitment: 'singleGossip'}, dexProgramId)
))
// TODO handle failures in any of the steps
// Find a way to get all margin accounts without querying fresh--get incremental updates to margin accounts
2021-04-09 13:18:06 -07:00
const liqorOpenOrdersKeys: PublicKey[] = []
for (let i = 0; i < NUM_MARKETS; i++) {
let openOrdersAccounts: OpenOrders[] = await markets[i].findOpenOrdersAccountsForOwner(connection, payer.publicKey)
if(openOrdersAccounts.length) {
liqorOpenOrdersKeys.push(openOrdersAccounts[0].publicKey)
} else {
console.log(`No OpenOrders account found for market ${markets[i].publicKey.toBase58()}`)
}
}
if(liqorOpenOrdersKeys.length != NUM_MARKETS) {
console.log('Warning: Missing OpenOrders accounts. Wallet balancing has been disabled.')
2021-04-09 13:18:06 -07:00
}
const cancelLimit = 5
2021-04-06 17:43:51 -07:00
while (true) {
try {
mangoGroup = await client.getMangoGroup(connection, mangoGroupPk)
2021-04-09 13:18:06 -07:00
let [marginAccounts, prices, vaultAccs, liqorAccs] = await Promise.all([
client.getAllMarginAccounts(connection, programId, mangoGroup),
mangoGroup.getPrices(connection),
getMultipleAccounts(connection, mangoGroup.vaults),
getMultipleAccounts(connection, tokenWallets, 'processed' as Commitment),
2021-04-09 13:18:06 -07:00
])
2021-04-06 17:43:51 -07:00
2021-04-09 13:18:06 -07:00
const vaultValues = vaultAccs.map(
2021-04-06 17:43:51 -07:00
(a, i) => nativeToUi(parseTokenAccountData(a.accountInfo.data).amount, mangoGroup.mintDecimals[i])
)
2021-04-09 13:18:06 -07:00
const liqorTokenValues = liqorAccs.map(
(a) => parseTokenAccount(a.accountInfo.data).amount
)
const liqorTokenUi = liqorAccs.map(
(a, i) => nativeToUi(parseTokenAccountData(a.accountInfo.data).amount, mangoGroup.mintDecimals[i])
)
console.log(prices)
2021-04-06 17:43:51 -07:00
console.log(vaultValues)
2021-04-09 13:18:06 -07:00
console.log(liqorTokenUi)
2021-04-06 17:43:51 -07:00
// FIXME: added bias to collRatio to allow other liquidators to step in for testing
let coll_bias = 0
if (process.env.COLL_BIAS) {
coll_bias = parseFloat(process.env.COLL_BIAS)
}
let maxBorrAcc: MarginAccount | undefined = undefined;
2021-04-06 17:43:51 -07:00
let maxBorrVal = 0;
let minCollAcc: MarginAccount | undefined = undefined;
let minCollVal = 99999;
2021-04-06 17:43:51 -07:00
for (let ma of marginAccounts) { // parallelize this if possible
let description = ''
try {
const assetsVal = ma.getAssetsVal(mangoGroup, prices)
const liabsVal = ma.getLiabsVal(mangoGroup, prices)
if (liabsVal > maxBorrVal) {
maxBorrVal = liabsVal
maxBorrAcc = ma
}
2021-04-06 17:43:51 -07:00
2021-04-17 20:54:47 -07:00
if (liabsVal < 0.1) { // too small of an account; number precision may cause errors
2021-04-14 14:48:32 -07:00
continue
}
if (!ma.beingLiquidated) {
let collRatio = (assetsVal / liabsVal)
2021-04-06 17:43:51 -07:00
const deficit = liabsVal * mangoGroup.initCollRatio - assetsVal
if (deficit < 0.1) { // too small of an account; number precision may cause errors
2021-04-14 14:48:32 -07:00
continue
2021-04-06 17:43:51 -07:00
}
if (collRatio < minCollVal)
{
minCollVal = collRatio
minCollAcc = ma
}
if (collRatio + coll_bias >= mangoGroup.maintCollRatio) {
continue
}
}
description = ma.toPrettyString(mangoGroup, prices)
console.log(`Liquidatable\n${description}\nbeingLiquidated: ${ma.beingLiquidated}`)
notify(`Liquidatable\n${description}\nbeingLiquidated: ${ma.beingLiquidated}`)
// find the market with the most value in OpenOrdersAccount
let maxMarketIndex = -1
let maxMarketVal = 0
for (let i = 0; i < NUM_MARKETS; i++) {
const openOrdersAccount = ma.openOrdersAccounts[i]
if (openOrdersAccount === undefined) {
continue
2021-04-06 17:43:51 -07:00
}
const marketVal = openOrdersAccount.quoteTokenTotal.toNumber() + openOrdersAccount.baseTokenTotal.toNumber() * prices[i]
if (marketVal > maxMarketVal) {
maxMarketIndex = i
maxMarketVal = marketVal
}
}
const transaction = new Transaction()
if (maxMarketIndex !== -1) {
// force cancel orders on this particular market
const spotMarket = markets[maxMarketIndex]
const [bids, asks] = await Promise.all([spotMarket.loadBids(connection), spotMarket.loadAsks(connection)])
const openOrdersAccount = ma.openOrdersAccounts[maxMarketIndex]
if (openOrdersAccount === undefined) {
console.log('error state')
continue
}
let numOrders = spotMarket.filterForOpenOrders(bids, asks, [openOrdersAccount]).length
const dexSigner = await PublicKey.createProgramAddress(
[
spotMarket.publicKey.toBuffer(),
spotMarket['_decoded'].vaultSignerNonce.toArrayLike(Buffer, 'le', 8)
],
spotMarket.programId
)
for (let i = 0; i < 10; i++) {
const instruction = makeForceCancelOrdersInstruction(
programId,
mangoGroup.publicKey,
payer.publicKey,
ma.publicKey,
mangoGroup.vaults[maxMarketIndex],
mangoGroup.vaults[NUM_TOKENS-1],
spotMarket.publicKey,
spotMarket.bidsAddress,
spotMarket.asksAddress,
mangoGroup.signerKey,
spotMarket['_decoded'].eventQueue,
spotMarket['_decoded'].baseVault,
spotMarket['_decoded'].quoteVault,
dexSigner,
spotMarket.programId,
ma.openOrders,
mangoGroup.oracles,
new BN(cancelLimit)
2021-04-06 17:43:51 -07:00
)
transaction.add(instruction)
numOrders -= cancelLimit
if (numOrders <= 0) {
break
}
2021-04-06 17:43:51 -07:00
}
}
2021-04-06 17:43:51 -07:00
// Find the market with the highest borrows and lowest deposits
const deposits = ma.getAssets(mangoGroup)
const borrows = ma.getLiabs(mangoGroup)
let minNet = 0
let minNetIndex = -1
let maxNet = 0
let maxNetIndex = NUM_TOKENS-1
for (let i = 0; i < NUM_TOKENS; i++) {
const netDeposit = (deposits[i] - borrows[i]) * prices[i]
if (netDeposit < minNet) {
minNet = netDeposit
minNetIndex = i
} else if (netDeposit > maxNet) {
maxNet = netDeposit
maxNetIndex = i
2021-04-06 17:43:51 -07:00
}
}
2021-04-06 17:43:51 -07:00
transaction.add(makePartialLiquidateInstruction(
programId,
mangoGroup.publicKey,
payer.publicKey,
liqorAccs[minNetIndex].publicKey,
liqorAccs[maxNetIndex].publicKey,
ma.publicKey,
mangoGroup.vaults[minNetIndex],
mangoGroup.vaults[maxNetIndex],
mangoGroup.signerKey,
ma.openOrders,
mangoGroup.oracles,
liqorTokenValues[minNetIndex]
))
await client.sendTransaction(connection, transaction, payer, [])
await sleep(2000)
ma = await client.getMarginAccount(connection, ma.publicKey, dexProgramId)
console.log(`Successful partial liquidation\n${ma.toPrettyString(mangoGroup, prices)}\nbeingLiquidated: ${ma.beingLiquidated}`)
notify(`Successful partial liquidation\n${ma.toPrettyString(mangoGroup, prices)}\nbeingLiquidated: ${ma.beingLiquidated}`)
break // This is so wallets get balanced
} catch (e) {
if (!e.timeout) {
throw e
} else {
notify(`unknown error: ${e}`);
console.error(e);
2021-04-06 17:43:51 -07:00
}
}
}
2021-04-09 13:18:06 -07:00
const maxBorrAccPk = maxBorrAcc ? maxBorrAcc.publicKey.toBase58() : ""
const maxBorrAccCr = maxBorrAcc ? maxBorrAcc.getCollateralRatio(mangoGroup, prices) : 0
const minCollAccPk = minCollAcc ? minCollAcc.publicKey.toBase58() : ""
const minCollBorrVal = minCollAcc ? minCollAcc.getLiabsVal(mangoGroup, prices) : 0
console.log(`Max Borrow Account: ${maxBorrAccPk} | Borrow Val: ${maxBorrVal} | CR: ${maxBorrAccCr}`)
console.log(`Min Coll-Ratio Account: ${minCollAccPk} | Borrow Val: ${minCollBorrVal} | CR: ${minCollVal}`)
if(liqorOpenOrdersKeys.length == NUM_MARKETS) {
await balanceWallets(connection, mangoGroup, prices, markets, payer, tokenWallets, liqorTokenUi, liqorOpenOrdersKeys, targets)
} else {
console.log('Could not balance wallets due to missing OpenOrders account')
}
2021-04-09 13:18:06 -07:00
2021-04-06 17:43:51 -07:00
} catch (e) {
notify(`unknown error: ${e}`);
console.error(e);
} finally {
await sleep(checkInterval)
2021-04-06 17:43:51 -07:00
}
}
}
runPartialLiquidator()