Finished partial liquidator
This commit is contained in:
parent
d92212723a
commit
84e0f9f7b7
253
src/partial.ts
253
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<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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue