diff --git a/Cargo.lock b/Cargo.lock index 4751beb08..ef4230d14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3005,7 +3005,7 @@ dependencies = [ [[package]] name = "mango-v4" -version = "0.13.0" +version = "0.14.0" dependencies = [ "anchor-lang", "anchor-spl", diff --git a/Procfile b/Procfile index 192e6aae3..192e8799c 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,3 @@ mm: node dist/cjs/scripts/mm/market-maker.js +rebalancer: node dist/cjs/scripts/rebalancer.js keeper: node dist/cjs/scripts/keeper/keeper.js diff --git a/ts/client/scripts/archive/mb-admin.ts b/ts/client/scripts/archive/mb-admin.ts index 98775174e..f3087df46 100644 --- a/ts/client/scripts/archive/mb-admin.ts +++ b/ts/client/scripts/archive/mb-admin.ts @@ -529,7 +529,7 @@ async function makeTokenReduceonly() { await client.tokenEdit( group, bank.mint, - Builder(NullTokenEditParams).reduceOnly(0).build(), + Builder(NullTokenEditParams).reduceOnly(1).build(), ); } diff --git a/ts/client/scripts/create-gov-ix.ts b/ts/client/scripts/create-gov-ix.ts index 2d1f3cdb4..39b2cd2d5 100644 --- a/ts/client/scripts/create-gov-ix.ts +++ b/ts/client/scripts/create-gov-ix.ts @@ -322,6 +322,7 @@ async function perpEdit(): Promise { params.resetStablePrice ?? false, params.positivePnlLiquidationFee, params.name, + params.forceClose, ) .accounts({ group: group.publicKey, diff --git a/ts/client/scripts/rebalancer.ts b/ts/client/scripts/rebalancer.ts new file mode 100644 index 000000000..0406ab3b6 --- /dev/null +++ b/ts/client/scripts/rebalancer.ts @@ -0,0 +1,197 @@ +import { AnchorProvider, Wallet } from '@coral-xyz/anchor'; +import { + Cluster, + Connection, + Keypair, + PublicKey, + TransactionInstruction, +} from '@solana/web3.js'; +import { BN } from 'bn.js'; +import fs from 'fs'; +import { + MarketIndex, + Serum3OrderType, + Serum3SelfTradeBehavior, + Serum3Side, +} from '../src/accounts/serum3'; +import { MangoClient } from '../src/client'; +import { MANGO_V4_ID } from '../src/constants'; +import { sendTransaction } from '../src/utils/rpc'; + +// Env vars +const CLUSTER: Cluster = + (process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta'; +const CLUSTER_URL = + process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL; +const USER_KEYPAIR = + process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR; +const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK || ''; + +export interface OrderbookL2 { + bids: number[][]; + asks: number[][]; +} + +async function rebalancer(): Promise { + // Load client + const options = AnchorProvider.defaultOptions(); + const connection = new Connection(CLUSTER_URL!, options); + const user = Keypair.fromSecretKey( + Buffer.from( + JSON.parse( + process.env.KEYPAIR || fs.readFileSync(USER_KEYPAIR!, 'utf-8'), + ), + ), + ); + const userWallet = new Wallet(user); + const userProvider = new AnchorProvider(connection, userWallet, options); + const client = await MangoClient.connect( + userProvider, + CLUSTER, + MANGO_V4_ID[CLUSTER], + { + idsSource: 'get-program-accounts', + }, + ); + + // Load mango account + let mangoAccount = await client.getMangoAccount( + new PublicKey(MANGO_ACCOUNT_PK), + true, + ); + console.log( + `MangoAccount ${mangoAccount.publicKey} for user ${user.publicKey} ${ + mangoAccount.isDelegate(client) ? 'via delegate ' + user.publicKey : '' + }`, + ); + await mangoAccount.reload(client); + + // Load group + const group = await client.getGroup(mangoAccount.group); + await group.reloadAll(client); + const usdcBank = group.getFirstBankByMint( + new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'), + ); + + // Loop indefinitely + // eslint-disable-next-line no-constant-condition + while (true) { + await group.reloadAll(client); + mangoAccount = await mangoAccount.reload(client); + // console.log(mangoAccount.toString(group, true)); + + for (const tp of mangoAccount + .tokensActive() + .filter((tp) => tp.tokenIndex !== usdcBank.tokenIndex)) { + const baseBank = group.getFirstBankByTokenIndex(tp.tokenIndex); + const tokenBalance = tp.balanceUi(baseBank); + + const serum3Markets = Array.from( + group.serum3MarketsMapByMarketIndex.values(), + ) + // Find correct $TOKEN/$USDC market + .filter( + (serum3Market) => + serum3Market.baseTokenIndex === tp.tokenIndex && + serum3Market.quoteTokenIndex === usdcBank.tokenIndex, + ); + if (!serum3Markets) { + continue; + } + const serum3Market = serum3Markets[0]; + const serum3MarketExternal = group.serum3ExternalMarketsMap.get( + serum3Market.serumMarketExternal.toBase58(), + )!; + const maxBaseQuantity = serum3MarketExternal.baseSizeNumberToLots( + Math.abs(tokenBalance), + ); + // Skip if quantity is too small + if (maxBaseQuantity.eq(new BN(0))) { + // console.log( + // ` - Not rebalancing ${tokenBalance} $${baseBank.name}, quantity too small`, + // ); + continue; + } + console.log(`- Rebalancing ${tokenBalance} $${baseBank.name}`); + + // if balance is negative we want to bid at a higher price + // if balance is positive we want to ask at a lower price + const price = + baseBank.uiPrice * + (1 + (tokenBalance > 0 ? -1 : 1) * baseBank.liquidationFee.toNumber()); + try { + const sig = await sendTransaction( + client.program.provider as AnchorProvider, + [ + ...(await client.serum3PlaceOrderIx( + group, + mangoAccount, + serum3Market.serumMarketExternal, + tokenBalance > 0 ? Serum3Side.ask : Serum3Side.bid, + price, + Math.abs(tokenBalance), + Serum3SelfTradeBehavior.decrementTake, + Serum3OrderType.immediateOrCancel, + new Date().valueOf(), + 10, + )), + await client.serum3CancelAllOrdersIx( + group, + mangoAccount, + serum3Market.serumMarketExternal, + ), + await client.serum3SettleFundsV2Ix( + group, + mangoAccount, + serum3Market.serumMarketExternal, + ), + ], + group.addressLookupTablesList, + { prioritizationFee: true }, + ); + + console.log(` -- sig https://explorer.solana.com/tx/${sig}`); + } catch (e) { + console.log(e); + } + } + + mangoAccount = await mangoAccount.reload(client); + const ixs: TransactionInstruction[] = []; + for (const serum3OoMarketIndex of Array.from( + mangoAccount.serum3OosMapByMarketIndex.keys(), + )) { + const serum3ExternalPk = group.serum3MarketsMapByMarketIndex.get( + serum3OoMarketIndex as MarketIndex, + )!.serumMarketExternal; + // 12502 cu per market + ixs.push( + await client.serum3CloseOpenOrdersIx( + group, + mangoAccount, + serum3ExternalPk, + ), + ); + } + if (ixs.length) { + try { + const sig = await sendTransaction( + client.program.provider as AnchorProvider, + ixs, + group.addressLookupTablesList, + { prioritizationFee: true }, + ); + console.log( + ` - closed all serum3 oo accounts, sig https://explorer.solana.com/tx/${sig}`, + ); + } catch (e) { + console.log(e); + } + } + + // console.log(`${new Date().toUTCString()} sleeping for 1s`); + await new Promise((r) => setTimeout(r, 1000)); + } +} + +rebalancer(); diff --git a/ts/client/src/accounts/serum3.ts b/ts/client/src/accounts/serum3.ts index e77522033..cc9a4357d 100644 --- a/ts/client/src/accounts/serum3.ts +++ b/ts/client/src/accounts/serum3.ts @@ -140,6 +140,40 @@ export class Serum3Market { ); } + public async computePriceForMarketOrderOfSize( + client: MangoClient, + group: Group, + size: number, + side: 'buy' | 'sell', + ): Promise { + const ob = + side == 'buy' + ? await this.loadBids(client, group) + : await this.loadAsks(client, group); + let acc = 0; + let selectedOrder; + const orderSize = size; + for (const order of ob.getL2(size * 2 /* TODO Fix random constant */)) { + acc += order[1]; + if (acc >= orderSize) { + selectedOrder = order; + break; + } + } + + if (!selectedOrder) { + throw new Error( + 'Unable to place market order for this order size. Please retry.', + ); + } + + if (side === 'buy') { + return selectedOrder[0] * 1.05 /* TODO Fix random constant */; + } else { + return selectedOrder[0] * 0.95 /* TODO Fix random constant */; + } + } + public async logOb(client: MangoClient, group: Group): Promise { let res = ``; res += ` ${this.name} OrderBook`; diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 09a07ac0a..a1ea5747d 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -1410,10 +1410,6 @@ export class MangoClient { externalMarketPk.toBase58(), )!; - const openOrders = mangoAccount.serum3.find( - (account) => account.marketIndex === serum3Market.marketIndex, - )?.openOrders; - return await this.program.methods .serum3CloseOpenOrders() .accounts({ @@ -1422,7 +1418,10 @@ export class MangoClient { serumMarket: serum3Market.publicKey, serumProgram: serum3Market.serumProgram, serumMarketExternal: serum3Market.serumMarketExternal, - openOrders, + openOrders: await serum3Market.findOoPda( + this.programId, + mangoAccount.publicKey, + ), solDestination: (this.program.provider as AnchorProvider).wallet .publicKey, }) @@ -1601,24 +1600,24 @@ export class MangoClient { clientOrderId, limit, ); + const settleIx = await this.serum3SettleFundsIx( group, mangoAccount, externalMarketPk, ); - return await this.sendAndConfirmTransactionForGroup(group, [ - ...placeOrderIxes, - settleIx, - ]); + const ixs = [...placeOrderIxes, settleIx]; + + return await this.sendAndConfirmTransactionForGroup(group, ixs); } - public async serum3CancelAllOrders( + public async serum3CancelAllOrdersIx( group: Group, mangoAccount: MangoAccount, externalMarketPk: PublicKey, limit?: number, - ): Promise { + ): Promise { const serum3Market = group.serum3MarketsMapByExternal.get( externalMarketPk.toBase58(), )!; @@ -1627,14 +1626,16 @@ export class MangoClient { externalMarketPk.toBase58(), )!; - const ix = await this.program.methods + return await this.program.methods .serum3CancelAllOrders(limit ? limit : 10) .accounts({ group: group.publicKey, account: mangoAccount.publicKey, owner: (this.program.provider as AnchorProvider).wallet.publicKey, - openOrders: mangoAccount.getSerum3Account(serum3Market.marketIndex) - ?.openOrders, + openOrders: await serum3Market.findOoPda( + this.programId, + mangoAccount.publicKey, + ), serumMarket: serum3Market.publicKey, serumProgram: OPENBOOK_PROGRAM_ID[this.cluster], serumMarketExternal: serum3Market.serumMarketExternal, @@ -1643,8 +1644,22 @@ export class MangoClient { marketEventQueue: serum3MarketExternal.decoded.eventQueue, }) .instruction(); + } - return await this.sendAndConfirmTransactionForGroup(group, [ix]); + public async serum3CancelAllOrders( + group: Group, + mangoAccount: MangoAccount, + externalMarketPk: PublicKey, + limit?: number, + ): Promise { + return await this.sendAndConfirmTransactionForGroup(group, [ + await this.serum3CancelAllOrdersIx( + group, + mangoAccount, + externalMarketPk, + limit, + ), + ]); } public async serum3SettleFundsIx(