Spot based token rebalancer (#541)

* script to relabance account to usdc

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* update procfile

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fix script

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* add prio fees

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* reset

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

---------

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2023-04-19 18:15:39 +02:00 committed by GitHub
parent 2f1839cb98
commit 29002e7197
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 265 additions and 17 deletions

2
Cargo.lock generated
View File

@ -3005,7 +3005,7 @@ dependencies = [
[[package]] [[package]]
name = "mango-v4" name = "mango-v4"
version = "0.13.0" version = "0.14.0"
dependencies = [ dependencies = [
"anchor-lang", "anchor-lang",
"anchor-spl", "anchor-spl",

View File

@ -1,2 +1,3 @@
mm: node dist/cjs/scripts/mm/market-maker.js mm: node dist/cjs/scripts/mm/market-maker.js
rebalancer: node dist/cjs/scripts/rebalancer.js
keeper: node dist/cjs/scripts/keeper/keeper.js keeper: node dist/cjs/scripts/keeper/keeper.js

View File

@ -529,7 +529,7 @@ async function makeTokenReduceonly() {
await client.tokenEdit( await client.tokenEdit(
group, group,
bank.mint, bank.mint,
Builder(NullTokenEditParams).reduceOnly(0).build(), Builder(NullTokenEditParams).reduceOnly(1).build(),
); );
} }

View File

@ -322,6 +322,7 @@ async function perpEdit(): Promise<void> {
params.resetStablePrice ?? false, params.resetStablePrice ?? false,
params.positivePnlLiquidationFee, params.positivePnlLiquidationFee,
params.name, params.name,
params.forceClose,
) )
.accounts({ .accounts({
group: group.publicKey, group: group.publicKey,

View File

@ -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<void> {
// 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();

View File

@ -140,6 +140,40 @@ export class Serum3Market {
); );
} }
public async computePriceForMarketOrderOfSize(
client: MangoClient,
group: Group,
size: number,
side: 'buy' | 'sell',
): Promise<number> {
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<string> { public async logOb(client: MangoClient, group: Group): Promise<string> {
let res = ``; let res = ``;
res += ` ${this.name} OrderBook`; res += ` ${this.name} OrderBook`;

View File

@ -1410,10 +1410,6 @@ export class MangoClient {
externalMarketPk.toBase58(), externalMarketPk.toBase58(),
)!; )!;
const openOrders = mangoAccount.serum3.find(
(account) => account.marketIndex === serum3Market.marketIndex,
)?.openOrders;
return await this.program.methods return await this.program.methods
.serum3CloseOpenOrders() .serum3CloseOpenOrders()
.accounts({ .accounts({
@ -1422,7 +1418,10 @@ export class MangoClient {
serumMarket: serum3Market.publicKey, serumMarket: serum3Market.publicKey,
serumProgram: serum3Market.serumProgram, serumProgram: serum3Market.serumProgram,
serumMarketExternal: serum3Market.serumMarketExternal, serumMarketExternal: serum3Market.serumMarketExternal,
openOrders, openOrders: await serum3Market.findOoPda(
this.programId,
mangoAccount.publicKey,
),
solDestination: (this.program.provider as AnchorProvider).wallet solDestination: (this.program.provider as AnchorProvider).wallet
.publicKey, .publicKey,
}) })
@ -1601,24 +1600,24 @@ export class MangoClient {
clientOrderId, clientOrderId,
limit, limit,
); );
const settleIx = await this.serum3SettleFundsIx( const settleIx = await this.serum3SettleFundsIx(
group, group,
mangoAccount, mangoAccount,
externalMarketPk, externalMarketPk,
); );
return await this.sendAndConfirmTransactionForGroup(group, [ const ixs = [...placeOrderIxes, settleIx];
...placeOrderIxes,
settleIx, return await this.sendAndConfirmTransactionForGroup(group, ixs);
]);
} }
public async serum3CancelAllOrders( public async serum3CancelAllOrdersIx(
group: Group, group: Group,
mangoAccount: MangoAccount, mangoAccount: MangoAccount,
externalMarketPk: PublicKey, externalMarketPk: PublicKey,
limit?: number, limit?: number,
): Promise<TransactionSignature> { ): Promise<TransactionInstruction> {
const serum3Market = group.serum3MarketsMapByExternal.get( const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(), externalMarketPk.toBase58(),
)!; )!;
@ -1627,14 +1626,16 @@ export class MangoClient {
externalMarketPk.toBase58(), externalMarketPk.toBase58(),
)!; )!;
const ix = await this.program.methods return await this.program.methods
.serum3CancelAllOrders(limit ? limit : 10) .serum3CancelAllOrders(limit ? limit : 10)
.accounts({ .accounts({
group: group.publicKey, group: group.publicKey,
account: mangoAccount.publicKey, account: mangoAccount.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey, owner: (this.program.provider as AnchorProvider).wallet.publicKey,
openOrders: mangoAccount.getSerum3Account(serum3Market.marketIndex) openOrders: await serum3Market.findOoPda(
?.openOrders, this.programId,
mangoAccount.publicKey,
),
serumMarket: serum3Market.publicKey, serumMarket: serum3Market.publicKey,
serumProgram: OPENBOOK_PROGRAM_ID[this.cluster], serumProgram: OPENBOOK_PROGRAM_ID[this.cluster],
serumMarketExternal: serum3Market.serumMarketExternal, serumMarketExternal: serum3Market.serumMarketExternal,
@ -1643,8 +1644,22 @@ export class MangoClient {
marketEventQueue: serum3MarketExternal.decoded.eventQueue, marketEventQueue: serum3MarketExternal.decoded.eventQueue,
}) })
.instruction(); .instruction();
}
return await this.sendAndConfirmTransactionForGroup(group, [ix]); public async serum3CancelAllOrders(
group: Group,
mangoAccount: MangoAccount,
externalMarketPk: PublicKey,
limit?: number,
): Promise<TransactionSignature> {
return await this.sendAndConfirmTransactionForGroup(group, [
await this.serum3CancelAllOrdersIx(
group,
mangoAccount,
externalMarketPk,
limit,
),
]);
} }
public async serum3SettleFundsIx( public async serum3SettleFundsIx(