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:
parent
2f1839cb98
commit
29002e7197
|
@ -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",
|
||||||
|
|
1
Procfile
1
Procfile
|
@ -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
|
||||||
|
|
|
@ -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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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();
|
|
@ -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`;
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in New Issue