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,
|
findLargestTokenAccountForOwner,
|
||||||
getMultipleAccounts,
|
getMultipleAccounts,
|
||||||
IDS,
|
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';
|
} 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 { homedir } from 'os';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { notify, sleep } from './utils';
|
import { notify, sleep } from './utils';
|
||||||
import { Market } from '@project-serum/serum';
|
import { Market, OpenOrders } from '@project-serum/serum';
|
||||||
import {
|
import {
|
||||||
makeForceCancelOrdersInstruction,
|
makeForceCancelOrdersInstruction,
|
||||||
makePartialLiquidateInstruction,
|
makePartialLiquidateInstruction,
|
||||||
} from '@blockworks-foundation/mango-client/lib/instruction';
|
} 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() {
|
async function runPartialLiquidator() {
|
||||||
const client = new MangoClient()
|
const client = new MangoClient()
|
||||||
|
@ -54,20 +247,40 @@ async function runPartialLiquidator() {
|
||||||
// TODO handle failures in any of the steps
|
// 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
|
// 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) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
mangoGroup = await client.getMangoGroup(connection, mangoGroupPk)
|
mangoGroup = await client.getMangoGroup(connection, mangoGroupPk)
|
||||||
// const marginAccounts = await client.getAllMarginAccounts(connection, programId, mangoGroup)
|
// const marginAccounts = await client.getAllMarginAccounts(connection, programId, mangoGroup)
|
||||||
const marginAccounts = [await client.getMarginAccount(connection, new PublicKey("85zCT5JsSmE5tgF42gPH6xxeVic5tXutAQDkSwfm9FN9"), mangoGroup.dexProgramId)]
|
// const marginAccounts = [await client.getMarginAccount(connection, new PublicKey("85zCT5JsSmE5tgF42gPH6xxeVic5tXutAQDkSwfm9FN9"), mangoGroup.dexProgramId)]
|
||||||
let prices = await mangoGroup.getPrices(connection) // TODO put this on websocket as well
|
// 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 = vaultAccs.map(
|
||||||
const vaultValues = tokenAccs.map(
|
|
||||||
(a, i) => nativeToUi(parseTokenAccountData(a.accountInfo.data).amount, mangoGroup.mintDecimals[i])
|
(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(vaultValues)
|
||||||
|
console.log(liqorTokenUi)
|
||||||
|
|
||||||
// FIXME: added bias to collRatio to allow other liquidators to step in for testing
|
// FIXME: added bias to collRatio to allow other liquidators to step in for testing
|
||||||
let coll_bias = 0
|
let coll_bias = 0
|
||||||
|
@ -142,8 +355,8 @@ async function runPartialLiquidator() {
|
||||||
],
|
],
|
||||||
spotMarket.programId
|
spotMarket.programId
|
||||||
)
|
)
|
||||||
let numInstrs = 0
|
|
||||||
while (numInstrs < 10) {
|
for (let i = 0; i < 10; i++) {
|
||||||
const instruction = makeForceCancelOrdersInstruction(
|
const instruction = makeForceCancelOrdersInstruction(
|
||||||
programId,
|
programId,
|
||||||
mangoGroup.publicKey,
|
mangoGroup.publicKey,
|
||||||
|
@ -161,11 +374,11 @@ async function runPartialLiquidator() {
|
||||||
dexSigner,
|
dexSigner,
|
||||||
spotMarket.programId,
|
spotMarket.programId,
|
||||||
ma.openOrders,
|
ma.openOrders,
|
||||||
mangoGroup.oracles
|
mangoGroup.oracles,
|
||||||
|
new BN(cancelLimit)
|
||||||
)
|
)
|
||||||
transaction.add(instruction)
|
transaction.add(instruction)
|
||||||
numOrders -= 6
|
numOrders -= cancelLimit
|
||||||
numInstrs += 1
|
|
||||||
if (numOrders <= 0) {
|
if (numOrders <= 0) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -190,11 +403,6 @@ async function runPartialLiquidator() {
|
||||||
maxNetIndex = i
|
maxNetIndex = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// choose the max
|
|
||||||
const liqorAccs = await getMultipleAccounts(connection, tokenWallets)
|
|
||||||
const liqorTokenValues = liqorAccs.map(
|
|
||||||
(a) => parseTokenAccount(a.accountInfo.data).amount
|
|
||||||
)
|
|
||||||
|
|
||||||
transaction.add(makePartialLiquidateInstruction(
|
transaction.add(makePartialLiquidateInstruction(
|
||||||
programId,
|
programId,
|
||||||
|
@ -211,11 +419,6 @@ async function runPartialLiquidator() {
|
||||||
liqorTokenValues[minNetIndex]
|
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, [])
|
await client.sendTransaction(connection, transaction, payer, [])
|
||||||
console.log('success liquidation')
|
console.log('success liquidation')
|
||||||
liquidated = true
|
liquidated = true
|
||||||
|
@ -231,8 +434,10 @@ async function runPartialLiquidator() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Max Borrow Account: ${maxBorrAcc} | Max Borrow Val: ${maxBorrVal}`)
|
console.log(`Max Borrow Account: ${maxBorrAcc} | Max Borrow Val: ${maxBorrVal}`)
|
||||||
|
|
||||||
|
await balanceWallets(connection, mangoGroup, prices, markets, payer, tokenWallets, liqorTokenUi, liqorOpenOrdersKeys)
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
notify(`unknown error: ${e}`);
|
notify(`unknown error: ${e}`);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
|
@ -345,7 +345,7 @@ async function testAll() {
|
||||||
const marginAccount = await client.getMarginAccount(connection, marginAccountPk, dexProgramId)
|
const marginAccount = await client.getMarginAccount(connection, marginAccountPk, dexProgramId)
|
||||||
const market = await Market.load(connection, mangoGroup.spotMarkets[1], { skipPreflight: true, commitment: 'singleGossip'}, mangoGroup.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
|
const price = 1010 + 10 * i
|
||||||
await client.placeAndSettle(connection, programId, mangoGroup, marginAccount, market, payer, "sell", price, 0.001)
|
await client.placeAndSettle(connection, programId, mangoGroup, marginAccount, market, payer, "sell", price, 0.001)
|
||||||
await sleep(500)
|
await sleep(500)
|
||||||
|
|
|
@ -316,7 +316,7 @@
|
||||||
|
|
||||||
"@blockworks-foundation/mango-client@https://github.com/blockworks-foundation/mango-client-ts#partial_liq":
|
"@blockworks-foundation/mango-client@https://github.com/blockworks-foundation/mango-client-ts#partial_liq":
|
||||||
version "0.1.10"
|
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:
|
dependencies:
|
||||||
"@project-serum/serum" "^0.13.20"
|
"@project-serum/serum" "^0.13.20"
|
||||||
"@project-serum/sol-wallet-adapter" "^0.1.4"
|
"@project-serum/sol-wallet-adapter" "^0.1.4"
|
||||||
|
|
Loading…
Reference in New Issue