Finished partial liquidator

This commit is contained in:
dd 2021-04-09 16:18:06 -04:00
parent d92212723a
commit 84e0f9f7b7
3 changed files with 231 additions and 26 deletions

View File

@ -2,17 +2,210 @@ import {
findLargestTokenAccountForOwner,
getMultipleAccounts,
IDS,
MangoClient, nativeToUi, NUM_MARKETS, NUM_TOKENS, parseTokenAccount, parseTokenAccountData, uiToNative,
MangoClient,
MangoGroup,
MarginAccount,
nativeToUi,
NUM_MARKETS,
NUM_TOKENS,
parseTokenAccount,
parseTokenAccountData, tokenToDecimals,
uiToNative,
} from '@blockworks-foundation/mango-client';
import { Account, Connection, PublicKey, Transaction } from '@solana/web3.js';
import { Account, Connection, PublicKey, Transaction, TransactionSignature } from '@solana/web3.js';
import { homedir } from 'os';
import fs from 'fs';
import { notify, sleep } from './utils';
import { Market } from '@project-serum/serum';
import { Market, OpenOrders } from '@project-serum/serum';
import {
makeForceCancelOrdersInstruction,
makePartialLiquidateInstruction,
} from '@blockworks-foundation/mango-client/lib/instruction';
import BN = require('bn.js');
async function drainAccount(
client: MangoClient,
connection: Connection,
programId: PublicKey,
mangoGroup: MangoGroup,
ma: MarginAccount,
markets: Market[],
payer: Account,
prices: number[],
usdWallet: PublicKey
) {
// Cancel all open orders
const bidsPromises = markets.map((market) => market.loadBids(connection))
const asksPromises = markets.map((market) => market.loadAsks(connection))
const books = await Promise.all(bidsPromises.concat(asksPromises))
const bids = books.slice(0, books.length / 2)
const asks = books.slice(books.length / 2, books.length)
const cancelProms: Promise<TransactionSignature[]>[] = []
for (let i = 0; i < NUM_MARKETS; i++) {
cancelProms.push(ma.cancelAllOrdersByMarket(connection, client, programId, mangoGroup, markets[i], bids[i], asks[i], payer))
}
await Promise.all(cancelProms)
console.log('all orders cancelled')
ma = await client.getMarginAccount(connection, ma.publicKey, mangoGroup.dexProgramId)
await client.settleAll(connection, programId, mangoGroup, ma, markets, payer)
console.log('settleAll complete')
await sleep(2000)
ma = await client.getMarginAccount(connection, ma.publicKey, mangoGroup.dexProgramId)
// sort non-quote currency assets by value
const assets = ma.getAssets(mangoGroup)
const liabs = ma.getLiabs(mangoGroup)
const netValues: [number, number][] = []
for (let i = 0; i < NUM_TOKENS - 1; i++) {
netValues.push([i, (assets[i] - liabs[i]) * prices[i]])
}
// Sort by those with largest net deposits and sell those first before trying to buy back the borrowed
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']
const tokenDecimalAdj = Math.pow(10, tokenDecimals)
if (netValues[i][1] > 0) { // sell to close
const price = prices[marketIndex] * 0.95
const size = Math.floor(assets[marketIndex] * tokenDecimalAdj) / tokenDecimalAdj // round down the size
if (size === 0) {
continue
}
console.log(`Sell to close ${marketIndex} ${size}`)
await client.placeOrder(connection, programId, mangoGroup, ma, market, payer, 'sell', price, size, 'limit')
} 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(liabs[marketIndex] * tokenDecimalAdj) / tokenDecimalAdj
console.log(`Buy to close ${marketIndex} ${size}`)
await client.placeOrder(connection, programId, mangoGroup, ma, market, payer, 'buy', price, size, 'limit')
}
}
await sleep(3000)
ma = await client.getMarginAccount(connection, ma.publicKey, mangoGroup.dexProgramId)
await client.settleAll(connection, programId, mangoGroup, ma, markets, payer)
console.log('settleAll complete')
ma = await client.getMarginAccount(connection, ma.publicKey, mangoGroup.dexProgramId)
console.log('Liquidation process complete\n', ma.toPrettyString(mangoGroup, prices))
console.log('Withdrawing USD')
await client.withdraw(connection, programId, mangoGroup, ma, payer, mangoGroup.tokens[NUM_TOKENS-1], usdWallet, ma.getUiDeposit(mangoGroup, NUM_TOKENS-1) * 0.999)
console.log('Successfully drained account', ma.publicKey.toString())
}
/*
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[],
liqorOpenOrdersKeys: PublicKey[]
) {
const liqorOpenOrders = await Promise.all(liqorOpenOrdersKeys.map((pk) => OpenOrders.load(connection, pk, mangoGroup.dexProgramId)))
let updateWallets = false
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 on liqor wallet ${i}`)
await markets[i].settleFunds(connection, liqor, oo, liqorWallets[i], liqorWallets[NUM_TOKENS-1])
updateWallets = true
}
}
if (updateWallets) {
await sleep(1000)
const liqorWalletAccounts = await getMultipleAccounts(connection, liqorWallets)
liqorValuesUi = liqorWalletAccounts.map(
(a, i) => nativeToUi(parseTokenAccountData(a.accountInfo.data).amount, mangoGroup.mintDecimals[i])
)
}
// TODO cancel outstanding orders as well
const targets = [0.1, 2]
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
// const tokenDecimalAdj = Math.pow(10, tokenDecimals)
const tokenDecimalAdj = Math.pow(10, 3)
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
}
)
console.log("settling funds")
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
}
)
console.log("settling funds")
await market.settleFunds(connection, liqor, liqorOpenOrders[marketIndex], liqorWallets[marketIndex], liqorWallets[NUM_TOKENS-1])
}
}
}
async function runPartialLiquidator() {
const client = new MangoClient()
@ -54,20 +247,40 @@ async function runPartialLiquidator() {
// 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
const liqorOpenOrdersKeys: PublicKey[] = []
for (let i = 0; i < NUM_MARKETS; i++) {
let openOrdersAccounts: OpenOrders[] = await markets[i].findOpenOrdersAccountsForOwner(connection, payer.publicKey)
liqorOpenOrdersKeys.push(openOrdersAccounts[0].publicKey)
}
const cancelLimit = 5
while (true) {
try {
mangoGroup = await client.getMangoGroup(connection, mangoGroupPk)
// const marginAccounts = await client.getAllMarginAccounts(connection, programId, mangoGroup)
const marginAccounts = [await client.getMarginAccount(connection, new PublicKey("85zCT5JsSmE5tgF42gPH6xxeVic5tXutAQDkSwfm9FN9"), mangoGroup.dexProgramId)]
let prices = await mangoGroup.getPrices(connection) // TODO put this on websocket as well
// const marginAccounts = [await client.getMarginAccount(connection, new PublicKey("85zCT5JsSmE5tgF42gPH6xxeVic5tXutAQDkSwfm9FN9"), mangoGroup.dexProgramId)]
// let prices = await mangoGroup.getPrices(connection) // TODO put this on websocket as well
console.log(prices)
let [marginAccounts, prices, vaultAccs, liqorAccs] = await Promise.all([
client.getAllMarginAccounts(connection, programId, mangoGroup),
mangoGroup.getPrices(connection),
getMultipleAccounts(connection, mangoGroup.vaults),
getMultipleAccounts(connection, tokenWallets),
])
const tokenAccs = await getMultipleAccounts(connection, mangoGroup.vaults)
const vaultValues = tokenAccs.map(
const vaultValues = vaultAccs.map(
(a, i) => nativeToUi(parseTokenAccountData(a.accountInfo.data).amount, mangoGroup.mintDecimals[i])
)
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(vaultValues)
console.log(liqorTokenUi)
// FIXME: added bias to collRatio to allow other liquidators to step in for testing
let coll_bias = 0
@ -142,8 +355,8 @@ async function runPartialLiquidator() {
],
spotMarket.programId
)
let numInstrs = 0
while (numInstrs < 10) {
for (let i = 0; i < 10; i++) {
const instruction = makeForceCancelOrdersInstruction(
programId,
mangoGroup.publicKey,
@ -161,11 +374,11 @@ async function runPartialLiquidator() {
dexSigner,
spotMarket.programId,
ma.openOrders,
mangoGroup.oracles
mangoGroup.oracles,
new BN(cancelLimit)
)
transaction.add(instruction)
numOrders -= 6
numInstrs += 1
numOrders -= cancelLimit
if (numOrders <= 0) {
break
}
@ -190,11 +403,6 @@ async function runPartialLiquidator() {
maxNetIndex = i
}
}
// choose the max
const liqorAccs = await getMultipleAccounts(connection, tokenWallets)
const liqorTokenValues = liqorAccs.map(
(a) => parseTokenAccount(a.accountInfo.data).amount
)
transaction.add(makePartialLiquidateInstruction(
programId,
@ -211,11 +419,6 @@ async function runPartialLiquidator() {
liqorTokenValues[minNetIndex]
))
// transaction.recentBlockhash = (await connection.getRecentBlockhash('singleGossip')).blockhash
// transaction.setSigners(payer.publicKey)
// transaction.sign(payer)
// const raw_tx = transaction.serialize()
// console.log('tx size', raw_tx.length)
await client.sendTransaction(connection, transaction, payer, [])
console.log('success liquidation')
liquidated = true
@ -231,8 +434,10 @@ async function runPartialLiquidator() {
}
}
}
console.log(`Max Borrow Account: ${maxBorrAcc} | Max Borrow Val: ${maxBorrVal}`)
await balanceWallets(connection, mangoGroup, prices, markets, payer, tokenWallets, liqorTokenUi, liqorOpenOrdersKeys)
} catch (e) {
notify(`unknown error: ${e}`);
console.error(e);

View File

@ -345,7 +345,7 @@ async function testAll() {
const marginAccount = await client.getMarginAccount(connection, marginAccountPk, dexProgramId)
const market = await Market.load(connection, mangoGroup.spotMarkets[1], { skipPreflight: true, commitment: 'singleGossip'}, mangoGroup.dexProgramId)
for (let i = 0; i < 12; i++) {
for (let i = 0; i < 45; i++) {
const price = 1010 + 10 * i
await client.placeAndSettle(connection, programId, mangoGroup, marginAccount, market, payer, "sell", price, 0.001)
await sleep(500)

View File

@ -316,7 +316,7 @@
"@blockworks-foundation/mango-client@https://github.com/blockworks-foundation/mango-client-ts#partial_liq":
version "0.1.10"
resolved "https://github.com/blockworks-foundation/mango-client-ts#e10fadd7bb9dee4f04994ad6cf293b4654c7edf1"
resolved "https://github.com/blockworks-foundation/mango-client-ts#7e5016ccff5be1949b0948a9cfb07dfcc27c3576"
dependencies:
"@project-serum/serum" "^0.13.20"
"@project-serum/sol-wallet-adapter" "^0.1.4"