From 84e0f9f7b752381cd7d5d2b7568beffa92fe8942 Mon Sep 17 00:00:00 2001 From: dd Date: Fri, 9 Apr 2021 16:18:06 -0400 Subject: [PATCH] Finished partial liquidator --- src/partial.ts | 253 ++++++++++++++++++++++++++++++++++++++++++++----- src/test.ts | 2 +- yarn.lock | 2 +- 3 files changed, 231 insertions(+), 26 deletions(-) diff --git a/src/partial.ts b/src/partial.ts index 7b83c63..02ef1f0 100644 --- a/src/partial.ts +++ b/src/partial.ts @@ -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[] = [] + 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); diff --git a/src/test.ts b/src/test.ts index 4ab2353..6fba0e4 100644 --- a/src/test.ts +++ b/src/test.ts @@ -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) diff --git a/yarn.lock b/yarn.lock index 670092c..7c511d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"