ts: perp improvements (#263)

* ts: perp improvements

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

* ts: fixes from review

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

* ts: fixes from review

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

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2022-10-07 13:52:04 +02:00 committed by GitHub
parent 6808171ee3
commit 8e919bb741
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1287 additions and 270 deletions

View File

@ -239,6 +239,27 @@ export class HealthCache {
return this.findTokenInfoIndex(bank.tokenIndex);
}
simHealthRatioWithTokenPositionChanges(
group: Group,
nativeTokenChanges: {
nativeTokenAmount: I80F48;
mintPk: PublicKey;
}[],
healthType: HealthType = HealthType.init,
): I80F48 {
const adjustedCache: HealthCache = _.cloneDeep(this);
// HealthCache.logHealthCache('beforeChange', adjustedCache);
for (const change of nativeTokenChanges) {
const bank: Bank = group.getFirstBankByMint(change.mintPk);
const changeIndex = adjustedCache.getOrCreateTokenInfoIndex(bank);
adjustedCache.tokenInfos[changeIndex].balance.iadd(
change.nativeTokenAmount.mul(bank.price),
);
}
// HealthCache.logHealthCache('afterChange', adjustedCache);
return adjustedCache.healthRatio(healthType);
}
findSerum3InfoIndex(marketIndex: MarketIndex): number {
return this.serum3Infos.findIndex(
(serum3Info) => serum3Info.marketIndex === marketIndex,
@ -266,7 +287,6 @@ export class HealthCache {
}
adjustSerum3Reserved(
// todo change indices to types from numbers
baseBank: BankForHealth,
quoteBank: BankForHealth,
serum3Market: Serum3Market,
@ -300,64 +320,6 @@ export class HealthCache {
serum3Info.reserved = serum3Info.reserved.add(reservedAmount);
}
findPerpInfoIndex(perpMarketIndex: number): number {
return this.perpInfos.findIndex(
(perpInfo) => perpInfo.perpMarketIndex === perpMarketIndex,
);
}
getOrCreatePerpInfoIndex(perpMarket: PerpMarket): number {
const index = this.findPerpInfoIndex(perpMarket.perpMarketIndex);
if (index == -1) {
this.perpInfos.push(PerpInfo.emptyFromPerpMarket(perpMarket));
}
return this.findPerpInfoIndex(perpMarket.perpMarketIndex);
}
public static logHealthCache(debug: string, healthCache: HealthCache): void {
if (debug) console.log(debug);
for (const token of healthCache.tokenInfos) {
console.log(` ${token.toString()}`);
}
for (const serum3Info of healthCache.serum3Infos) {
console.log(` ${serum3Info.toString(healthCache.tokenInfos)}`);
}
console.log(
` assets ${healthCache.assets(
HealthType.init,
)}, liabs ${healthCache.liabs(HealthType.init)}, `,
);
console.log(
` health(HealthType.init) ${healthCache.health(HealthType.init)}`,
);
console.log(
` healthRatio(HealthType.init) ${healthCache.healthRatio(
HealthType.init,
)}`,
);
}
simHealthRatioWithTokenPositionChanges(
group: Group,
nativeTokenChanges: {
nativeTokenAmount: I80F48;
mintPk: PublicKey;
}[],
healthType: HealthType = HealthType.init,
): I80F48 {
const adjustedCache: HealthCache = _.cloneDeep(this);
// HealthCache.logHealthCache('beforeChange', adjustedCache);
for (const change of nativeTokenChanges) {
const bank: Bank = group.getFirstBankByMint(change.mintPk);
const changeIndex = adjustedCache.getOrCreateTokenInfoIndex(bank);
adjustedCache.tokenInfos[changeIndex].balance.iadd(
change.nativeTokenAmount.mul(bank.price),
);
}
// HealthCache.logHealthCache('afterChange', adjustedCache);
return adjustedCache.healthRatio(healthType);
}
simHealthRatioWithSerum3BidChanges(
baseBank: BankForHealth,
quoteBank: BankForHealth,
@ -422,6 +384,83 @@ export class HealthCache {
return adjustedCache.healthRatio(healthType);
}
findPerpInfoIndex(perpMarketIndex: number): number {
return this.perpInfos.findIndex(
(perpInfo) => perpInfo.perpMarketIndex === perpMarketIndex,
);
}
getOrCreatePerpInfoIndex(perpMarket: PerpMarket): number {
const index = this.findPerpInfoIndex(perpMarket.perpMarketIndex);
if (index == -1) {
this.perpInfos.push(PerpInfo.emptyFromPerpMarket(perpMarket));
}
return this.findPerpInfoIndex(perpMarket.perpMarketIndex);
}
recomputePerpInfo(
perpMarket: PerpMarket,
perpInfoIndex: number,
clonedExistingPerpPosition: PerpPosition,
side: PerpOrderSide,
newOrderBaseLots: BN,
): void {
if (side == PerpOrderSide.bid) {
clonedExistingPerpPosition.bidsBaseLots.iadd(newOrderBaseLots);
} else {
clonedExistingPerpPosition.asksBaseLots.iadd(newOrderBaseLots);
}
this.perpInfos[perpInfoIndex] = PerpInfo.fromPerpPosition(
perpMarket,
clonedExistingPerpPosition,
);
}
simHealthRatioWithPerpOrderChanges(
perpMarket: PerpMarket,
existingPerpPosition: PerpPosition,
baseLots: BN,
side: PerpOrderSide,
healthType: HealthType = HealthType.init,
): I80F48 {
const clonedHealthCache: HealthCache = _.cloneDeep(this);
const clonedExistingPosition: PerpPosition =
_.cloneDeep(existingPerpPosition);
const perpInfoIndex =
clonedHealthCache.getOrCreatePerpInfoIndex(perpMarket);
clonedHealthCache.recomputePerpInfo(
perpMarket,
perpInfoIndex,
clonedExistingPosition,
side,
baseLots,
);
return clonedHealthCache.healthRatio(healthType);
}
public static logHealthCache(debug: string, healthCache: HealthCache): void {
if (debug) console.log(debug);
for (const token of healthCache.tokenInfos) {
console.log(` ${token.toString()}`);
}
for (const serum3Info of healthCache.serum3Infos) {
console.log(` ${serum3Info.toString(healthCache.tokenInfos)}`);
}
console.log(
` assets ${healthCache.assets(
HealthType.init,
)}, liabs ${healthCache.liabs(HealthType.init)}, `,
);
console.log(
` health(HealthType.init) ${healthCache.health(HealthType.init)}`,
);
console.log(
` healthRatio(HealthType.init) ${healthCache.healthRatio(
HealthType.init,
)}`,
);
}
private static binaryApproximationSearch(
left: I80F48,
leftRatio: I80F48,
@ -719,9 +758,9 @@ export class HealthCache {
getMaxPerpForHealthRatio(
perpMarket: PerpMarket,
existingPerpPosition: PerpPosition,
side: PerpOrderSide,
minRatio: I80F48,
price: I80F48,
): I80F48 {
const healthCacheClone: HealthCache = _.cloneDeep(this);
@ -732,37 +771,43 @@ export class HealthCache {
const direction = side == PerpOrderSide.bid ? 1 : -1;
const perpInfoIndex = this.getOrCreatePerpInfoIndex(perpMarket);
const perpInfo = this.perpInfos[perpInfoIndex];
const perpInfoIndex = healthCacheClone.getOrCreatePerpInfoIndex(perpMarket);
const perpInfo = healthCacheClone.perpInfos[perpInfoIndex];
const oraclePrice = perpInfo.oraclePrice;
const baseLotSize = I80F48.fromI64(perpMarket.baseLotSize);
// If the price is sufficiently good then health will just increase from trading
const finalHealthSlope =
direction == 1
? perpInfo.initAssetWeight.mul(oraclePrice).sub(price)
: price.sub(perpInfo.initLiabWeight.mul(oraclePrice));
? perpInfo.initAssetWeight.mul(oraclePrice).sub(oraclePrice)
: oraclePrice.sub(perpInfo.initLiabWeight.mul(oraclePrice));
if (finalHealthSlope.gte(ZERO_I80F48())) {
return MAX_I80F48();
}
function cacheAfterTrade(baseLots: I80F48): HealthCache {
function cacheAfterPlaceOrder(baseLots: BN): HealthCache {
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
const d = I80F48.fromNumber(direction);
adjustedCache.perpInfos[perpInfoIndex].base.iadd(
d.mul(baseLots.mul(baseLotSize.mul(oraclePrice))),
);
adjustedCache.perpInfos[perpInfoIndex].quote.isub(
d.mul(baseLots.mul(baseLotSize.mul(price))),
const adjustedExistingPerpPosition: PerpPosition =
_.cloneDeep(existingPerpPosition);
adjustedCache.recomputePerpInfo(
perpMarket,
perpInfoIndex,
adjustedExistingPerpPosition,
side,
baseLots,
);
return adjustedCache;
}
function healthAfterTrade(baseLots: I80F48): I80F48 {
return cacheAfterTrade(baseLots).health(HealthType.init);
return cacheAfterPlaceOrder(new BN(baseLots.toNumber())).health(
HealthType.init,
);
}
function healthRatioAfterTrade(baseLots: I80F48): I80F48 {
return cacheAfterTrade(baseLots).healthRatio(HealthType.init);
return cacheAfterPlaceOrder(new BN(baseLots.toNumber())).healthRatio(
HealthType.init,
);
}
const initialBaseLots = perpInfo.base
@ -1064,7 +1109,7 @@ export class PerpInfo {
perpPosition.basePositionLots.add(perpPosition.takerBaseLots),
);
const unsettledFunding = perpPosition.unsettledFunding(perpMarket);
const unsettledFunding = perpPosition.getUnsettledFunding(perpMarket);
const takerQuote = I80F48.fromI64(
new BN(perpPosition.takerQuoteLots).mul(perpMarket.quoteLotSize),
@ -1164,13 +1209,13 @@ export class PerpInfo {
weight = this.maintAssetWeight;
}
// console.log(`initLiabWeight ${this.initLiabWeight}`);
// console.log(`initAssetWeight ${this.initAssetWeight}`);
// console.log(`weight ${weight}`);
// console.log(`this.quote ${this.quote}`);
// console.log(`this.base ${this.base}`);
// console.log(` - this.quote ${this.quote}`);
// console.log(` - weight ${weight}`);
// console.log(` - this.base ${this.base}`);
// console.log(` - weight.mul(this.base) ${weight.mul(this.base)}`);
const uncappedHealthContribution = this.quote.add(weight.mul(this.base));
// console.log(` - uncappedHealthContribution ${uncappedHealthContribution}`);
if (this.trustedMarket) {
return uncappedHealthContribution;
} else {

View File

@ -153,6 +153,19 @@ export class MangoAccount {
return this.serum3.find((sa) => sa.marketIndex == marketIndex);
}
getPerpPosition(perpMarketIndex: PerpMarketIndex): PerpPosition | undefined {
return this.perps.find((pp) => pp.marketIndex == perpMarketIndex);
}
getPerpPositionUi(group: Group, perpMarketIndex: PerpMarketIndex): number {
const pp = this.perps.find((pp) => pp.marketIndex == perpMarketIndex);
if (!pp) {
throw new Error(`No position found for PerpMarket ${perpMarketIndex}!`);
}
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
return pp.getBasePositionUi(perpMarket);
}
getSerum3OoAccount(marketIndex: MarketIndex): OpenOrders {
const oo: OpenOrders | undefined =
this.serum3OosMapByMarketIndex.get(marketIndex);
@ -543,7 +556,7 @@ export class MangoAccount {
/**
* @param group
* @param externalMarketPk
* @returns maximum ui quote which can be traded for base token given current health
* @returns maximum ui quote which can be traded at oracle price for base token given current health
*/
public getMaxQuoteForSerum3BidUi(
group: Group,
@ -580,7 +593,7 @@ export class MangoAccount {
/**
* @param group
* @param externalMarketPk
* @returns maximum ui base which can be traded for quote token given current health
* @returns maximum ui base which can be traded at oracle price for quote token given current health
*/
public getMaxBaseForSerum3AskUi(
group: Group,
@ -694,21 +707,22 @@ export class MangoAccount {
*
* @param group
* @param perpMarketName
* @param uiPrice ui price at which bid would be placed at
* @returns max ui quote bid
* @returns maximum ui quote which can be traded at oracle price for quote token given current health
*/
public getMaxQuoteForPerpBidUi(
group: Group,
perpMarketIndex: PerpMarketIndex,
uiPrice: number,
): number {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
const hc = HealthCache.fromMangoAccount(group, this);
const baseLots = hc.getMaxPerpForHealthRatio(
perpMarket,
pp
? pp
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
PerpOrderSide.bid,
I80F48.fromNumber(2),
group.toNativePrice(uiPrice, perpMarket.baseDecimals),
);
const nativeBase = baseLots.mul(I80F48.fromI64(perpMarket.baseLotSize));
const nativeQuote = nativeBase.mul(perpMarket.price);
@ -725,19 +739,63 @@ export class MangoAccount {
public getMaxBaseForPerpAskUi(
group: Group,
perpMarketIndex: PerpMarketIndex,
uiPrice: number,
): number {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
const hc = HealthCache.fromMangoAccount(group, this);
const baseLots = hc.getMaxPerpForHealthRatio(
perpMarket,
pp
? pp
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
PerpOrderSide.ask,
I80F48.fromNumber(2),
group.toNativePrice(uiPrice, perpMarket.baseDecimals),
);
return perpMarket.baseLotsToUi(new BN(baseLots.toString()));
}
public simHealthRatioWithPerpBidUiChanges(
group: Group,
perpMarketIndex: PerpMarketIndex,
size: number,
): number {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
const hc = HealthCache.fromMangoAccount(group, this);
return hc
.simHealthRatioWithPerpOrderChanges(
perpMarket,
pp
? pp
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
perpMarket.uiBaseToLots(size),
PerpOrderSide.bid,
HealthType.init,
)
.toNumber();
}
public simHealthRatioWithPerpAskUiChanges(
group: Group,
perpMarketIndex: PerpMarketIndex,
size: number,
): number {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
const hc = HealthCache.fromMangoAccount(group, this);
return hc
.simHealthRatioWithPerpOrderChanges(
perpMarket,
pp
? pp
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
perpMarket.uiBaseToLots(size),
PerpOrderSide.ask,
HealthType.init,
)
.toNumber();
}
public async loadPerpOpenOrdersForMarket(
client: MangoClient,
group: Group,
@ -963,6 +1021,22 @@ export class PerpPosition {
);
}
static emptyFromPerpMarketIndex(
perpMarketIndex: PerpMarketIndex,
): PerpPosition {
return new PerpPosition(
perpMarketIndex,
new BN(0),
ZERO_I80F48(),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
ZERO_I80F48(),
ZERO_I80F48(),
);
}
constructor(
public marketIndex: PerpMarketIndex,
public basePositionLots: BN,
@ -979,7 +1053,11 @@ export class PerpPosition {
return this.marketIndex != PerpPosition.PerpMarketIndexUnset;
}
public unsettledFunding(perpMarket: PerpMarket): I80F48 {
public getBasePositionUi(perpMarket: PerpMarket): number {
return perpMarket.baseLotsToUi(this.basePositionLots);
}
public getUnsettledFunding(perpMarket: PerpMarket): I80F48 {
if (this.basePositionLots.gt(new BN(0))) {
return perpMarket.longFunding
.sub(this.longSettledFunding)
@ -1001,7 +1079,7 @@ export class PerpPosition {
this.basePositionLots.add(this.takerBaseLots),
);
const unsettledFunding = this.unsettledFunding(perpMarket);
const unsettledFunding = this.getUnsettledFunding(perpMarket);
const takerQuote = I80F48.fromI64(
new BN(this.takerQuoteLots).mul(perpMarket.quoteLotSize),
);

View File

@ -209,6 +209,25 @@ export class PerpMarket {
.filter((event) => event.eventType == PerpEventQueue.FILL_EVENT_TYPE);
}
public async logOb(client: MangoClient): Promise<string> {
let res = ``;
res += ` ${this.name} OrderBook`;
let orders = await this?.loadAsks(client);
for (const order of orders!.items()) {
res += `\n ${order.uiPrice.toFixed(5).padStart(10)}, ${order.uiSize
.toString()
.padStart(10)}`;
}
res += `\n asks ↑ --------- ↓ bids`;
orders = await this?.loadBids(client);
for (const order of orders!.items()) {
res += `\n ${order.uiPrice.toFixed(5).padStart(10)}, ${order.uiSize
.toString()
.padStart(10)}`;
}
return res;
}
/**
*
* @param bids
@ -380,6 +399,10 @@ export class BookSide {
}
}
public best(): PerpOrder | undefined {
return this.items().next().value;
}
getImpactPriceUi(baseLots: BN): number | undefined {
const s = new BN(0);
for (const order of this.items()) {
@ -491,10 +514,10 @@ export class PerpOrderSide {
export class PerpOrderType {
static limit = { limit: {} };
static immediateOrCancel = { immediateorcancel: {} };
static postOnly = { postonly: {} };
static immediateOrCancel = { immediateOrCancel: {} };
static postOnly = { postOnly: {} };
static market = { market: {} };
static postOnlySlide = { postonlyslide: {} };
static postOnlySlide = { postOnlySlide: {} };
}
export class PerpOrder {

View File

@ -697,6 +697,30 @@ export class MangoClient {
});
}
public async getMangoAccountsForDelegate(
group: Group,
delegate: PublicKey,
): Promise<MangoAccount[]> {
return (
await this.program.account.mangoAccount.all([
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 8,
},
},
{
memcmp: {
bytes: delegate.toBase58(),
offset: 104,
},
},
])
).map((pa) => {
return MangoAccount.from(pa.publicKey, pa.account);
});
}
public async getAllMangoAccounts(group: Group): Promise<MangoAccount[]> {
return (
await this.program.account.mangoAccount.all([
@ -1183,7 +1207,7 @@ export class MangoClient {
);
}
async serum3CancelAllorders(
public async serum3CancelAllOrders(
group: Group,
mangoAccount: MangoAccount,
externalMarketPk: PublicKey,
@ -1224,7 +1248,7 @@ export class MangoClient {
);
}
async serum3SettleFunds(
public async serum3SettleFunds(
group: Group,
mangoAccount: MangoAccount,
externalMarketPk: PublicKey,
@ -1277,7 +1301,7 @@ export class MangoClient {
);
}
async serum3CancelOrder(
public async serum3CancelOrder(
group: Group,
mangoAccount: MangoAccount,
externalMarketPk: PublicKey,
@ -1320,7 +1344,7 @@ export class MangoClient {
/// perps
async perpCreateMarket(
public async perpCreateMarket(
group: Group,
oraclePk: PublicKey,
perpMarketIndex: number,
@ -1438,7 +1462,7 @@ export class MangoClient {
.rpc();
}
async perpEditMarket(
public async perpEditMarket(
group: Group,
perpMarketIndex: PerpMarketIndex,
oracle: PublicKey,
@ -1497,7 +1521,7 @@ export class MangoClient {
.rpc();
}
async perpCloseMarket(
public async perpCloseMarket(
group: Group,
perpMarketIndex: PerpMarketIndex,
): Promise<TransactionSignature> {
@ -1536,7 +1560,7 @@ export class MangoClient {
);
}
async perpDeactivatePosition(
public async perpDeactivatePosition(
group: Group,
mangoAccount: MangoAccount,
perpMarketIndex: PerpMarketIndex,
@ -1567,19 +1591,56 @@ export class MangoClient {
.rpc();
}
async perpPlaceOrder(
public async perpPlaceOrder(
group: Group,
mangoAccount: MangoAccount,
perpMarketIndex: PerpMarketIndex,
side: PerpOrderSide,
price: number,
quantity: number,
maxQuoteQuantity: number,
clientOrderId: number,
orderType: PerpOrderType,
expiryTimestamp: number,
limit: number,
maxQuoteQuantity: number | undefined,
clientOrderId: number | undefined,
orderType: PerpOrderType | undefined,
expiryTimestamp: number | undefined,
limit: number | undefined,
): Promise<TransactionSignature> {
return await sendTransaction(
this.program.provider as AnchorProvider,
[
await this.perpPlaceOrderIx(
group,
mangoAccount,
perpMarketIndex,
side,
price,
quantity,
maxQuoteQuantity,
clientOrderId,
orderType,
expiryTimestamp,
limit,
),
],
group.addressLookupTablesList,
{
postSendTxCallback: this.postSendTxCallback,
},
);
}
public async perpPlaceOrderIx(
group: Group,
mangoAccount: MangoAccount,
perpMarketIndex: PerpMarketIndex,
side: PerpOrderSide,
price: number,
quantity: number,
maxQuoteQuantity?: number,
clientOrderId?: number,
orderType?: PerpOrderType,
expiryTimestamp?: number,
limit?: number,
): Promise<TransactionInstruction> {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
@ -1590,7 +1651,7 @@ export class MangoClient {
[group.getFirstBankByTokenIndex(0 as TokenIndex)],
[perpMarket],
);
const ix = await this.program.methods
return await this.program.methods
.perpPlaceOrder(
side,
perpMarket.uiPriceToLots(price),
@ -1598,10 +1659,10 @@ export class MangoClient {
maxQuoteQuantity
? perpMarket.uiQuoteToLots(maxQuoteQuantity)
: I64_MAX_BN,
new BN(clientOrderId),
orderType,
new BN(expiryTimestamp),
limit,
new BN(clientOrderId ? clientOrderId : Date.now()),
orderType ? orderType : PerpOrderType.limit,
new BN(expiryTimestamp ? expiryTimestamp : 0),
limit ? limit : 10,
)
.accounts({
group: group.publicKey,
@ -1620,10 +1681,24 @@ export class MangoClient {
),
)
.instruction();
}
public async perpCancelAllOrders(
group: Group,
mangoAccount: MangoAccount,
perpMarketIndex: PerpMarketIndex,
limit: number,
): Promise<TransactionSignature> {
return await sendTransaction(
this.program.provider as AnchorProvider,
[ix],
[
await this.perpCancelAllOrdersIx(
group,
mangoAccount,
perpMarketIndex,
limit,
),
],
group.addressLookupTablesList,
{
postSendTxCallback: this.postSendTxCallback,
@ -1631,14 +1706,14 @@ export class MangoClient {
);
}
async perpCancelAllOrders(
public async perpCancelAllOrdersIx(
group: Group,
mangoAccount: MangoAccount,
perpMarketIndex: PerpMarketIndex,
limit: number,
): Promise<TransactionSignature> {
): Promise<TransactionInstruction> {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const ix = await this.program.methods
return await this.program.methods
.perpCancelAllOrders(limit)
.accounts({
group: group.publicKey,
@ -1649,18 +1724,9 @@ export class MangoClient {
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
})
.instruction();
return await sendTransaction(
this.program.provider as AnchorProvider,
[ix],
group.addressLookupTablesList,
{
postSendTxCallback: this.postSendTxCallback,
},
);
}
async perpConsumeEvents(
public async perpConsumeEvents(
group: Group,
perpMarketIndex: PerpMarketIndex,
accounts: PublicKey[],
@ -1683,7 +1749,7 @@ export class MangoClient {
.rpc();
}
async perpConsumeAllEvents(
public async perpConsumeAllEvents(
group: Group,
perpMarketIndex: PerpMarketIndex,
): Promise<void> {
@ -1887,7 +1953,7 @@ export class MangoClient {
);
}
async updateIndexAndRate(
public async updateIndexAndRate(
group: Group,
mintPk: PublicKey,
): Promise<TransactionSignature> {
@ -1915,7 +1981,7 @@ export class MangoClient {
/// liquidations
async liqTokenWithToken(
public async liqTokenWithToken(
group: Group,
liqor: MangoAccount,
liqee: MangoAccount,
@ -1970,7 +2036,7 @@ export class MangoClient {
);
}
async altSet(
public async altSet(
group: Group,
addressLookupTable: PublicKey,
index: number,
@ -1994,7 +2060,7 @@ export class MangoClient {
);
}
async altExtend(
public async altExtend(
group: Group,
addressLookupTable: PublicKey,
index: number,

View File

@ -493,7 +493,7 @@ async function main() {
try {
let sig = await client.perpEditMarket(
group,
'BTC-PERP',
group.getPerpMarketByName('BTC-PERP').perpMarketIndex,
btcDevnetOracle,
0.1,
6,

View File

@ -1,5 +1,6 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { AnchorProvider, BN, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import { expect } from 'chai';
import fs from 'fs';
import { HealthType } from '../accounts/mangoAccount';
import { BookSide, PerpOrderSide, PerpOrderType } from '../accounts/perp';
@ -69,101 +70,132 @@ async function main() {
// create + fetch account
console.log(`Creating mangoaccount...`);
let mangoAccount = (await client.getOrCreateMangoAccount(group))!;
await mangoAccount.reload(client);
if (!mangoAccount) {
throw new Error(`MangoAccount not found for user ${user.publicKey}`);
}
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString(group));
await mangoAccount.reload(client);
// set delegate, and change name
if (true) {
console.log(`...changing mango account name, and setting a delegate`);
const newName = 'my_changed_name';
const randomKey = new PublicKey(
'4ZkS7ZZkxfsC3GtvvsHP3DFcUeByU9zzZELS4r8HCELo',
);
await client.editMangoAccount(
group,
mangoAccount,
'my_changed_name',
randomKey,
);
await client.editMangoAccount(group, mangoAccount, newName, randomKey);
await mangoAccount.reload(client);
console.log(mangoAccount.toString());
expect(mangoAccount.name).deep.equals(newName);
expect(mangoAccount.delegate).deep.equals(randomKey);
const oldName = 'my_mango_account';
console.log(`...resetting mango account name, and re-setting a delegate`);
await client.editMangoAccount(
group,
mangoAccount,
'my_mango_account',
oldName,
PublicKey.default,
);
await mangoAccount.reload(client);
console.log(mangoAccount.toString());
expect(mangoAccount.name).deep.equals(oldName);
expect(mangoAccount.delegate).deep.equals(PublicKey.default);
}
// expand account
if (false) {
if (
mangoAccount.tokens.length < 16 ||
mangoAccount.serum3.length < 8 ||
mangoAccount.perps.length < 8 ||
mangoAccount.perpOpenOrders.length < 64
) {
console.log(
`...expanding mango account to have serum3 and perp position slots`,
`...expanding mango account to max 16 token positions, 8 serum3, 8 perp position and 64 perp oo slots, previous (tokens ${mangoAccount.tokens.length}, serum3 ${mangoAccount.serum3.length}, perps ${mangoAccount.perps.length}, perps oo ${mangoAccount.perpOpenOrders.length})`,
);
await client.expandMangoAccount(group, mangoAccount, 8, 8, 8, 8);
let sig = await client.expandMangoAccount(
group,
mangoAccount,
16,
8,
8,
64,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await mangoAccount.reload(client);
expect(mangoAccount.tokens.length).equals(16);
expect(mangoAccount.serum3.length).equals(8);
expect(mangoAccount.perps.length).equals(8);
expect(mangoAccount.perpOpenOrders.length).equals(64);
}
// deposit and withdraw
if (true) {
try {
console.log(`...depositing 50 USDC, 1 SOL, 1 MNGO`);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('USDC')!),
50,
);
await mangoAccount.reload(client);
console.log(`...depositing 50 USDC, 1 SOL, 1 MNGO`);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('SOL')!),
1,
);
await mangoAccount.reload(client);
// deposit USDC
let oldBalance = mangoAccount.getTokenBalance(
group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)),
);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('USDC')!),
50,
);
await mangoAccount.reload(client);
let newBalance = mangoAccount.getTokenBalance(
group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)),
);
expect(toUiDecimalsForQuote(newBalance.sub(oldBalance)).toString()).equals(
'50',
);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('MNGO')!),
1,
);
await mangoAccount.reload(client);
// deposit SOL
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('SOL')!),
1,
);
await mangoAccount.reload(client);
console.log(`...withdrawing 1 USDC`);
await client.tokenWithdraw(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('USDC')!),
1,
true,
);
await mangoAccount.reload(client);
// deposit MNGO
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('MNGO')!),
1,
);
await mangoAccount.reload(client);
console.log(`...depositing 0.0005 BTC`);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('BTC')!),
0.0005,
);
await mangoAccount.reload(client);
// withdraw USDC
console.log(`...withdrawing 1 USDC`);
oldBalance = mangoAccount.getTokenBalance(
group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)),
);
await client.tokenWithdraw(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('USDC')!),
1,
true,
);
await mangoAccount.reload(client);
newBalance = mangoAccount.getTokenBalance(
group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)),
);
expect(toUiDecimalsForQuote(oldBalance.sub(newBalance)).toString()).equals(
'1',
);
console.log(mangoAccount.toString(group));
} catch (error) {
console.log(error);
}
console.log(`...depositing 0.0005 BTC`);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('BTC')!),
0.0005,
);
await mangoAccount.reload(client);
}
if (true) {
@ -177,7 +209,19 @@ async function main() {
client,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
const highestBid = Array.from(asks!)![0];
const highestBid = Array.from(bids!)![0];
console.log(`...cancelling all existing serum3 orders`);
if (
Array.from(mangoAccount.serum3OosMapByMarketIndex.values()).length > 0
) {
await client.serum3CancelAllOrders(
group,
mangoAccount,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
10,
);
}
let price = 20;
let qty = 0.0001;
@ -197,6 +241,13 @@ async function main() {
10,
);
await mangoAccount.reload(client);
let orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
client,
group,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
expect(orders[0].price).equals(20);
expect(orders[0].size).equals(qty);
price = lowestAsk.price + lowestAsk.price / 2;
qty = 0.0001;
@ -236,7 +287,7 @@ async function main() {
);
console.log(`...current own orders on OB`);
let orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
client,
group,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
@ -273,14 +324,6 @@ async function main() {
);
}
if (true) {
// serum3 market
const serum3Market = group.serum3MarketsMapByExternal.get(
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!.toBase58(),
);
console.log(await serum3Market?.logOb(client, group));
}
if (true) {
await mangoAccount.reload(client);
console.log(
@ -319,19 +362,7 @@ async function main() {
}
if (true) {
const asks = await group.loadSerum3AsksForMarket(
client,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
const lowestAsk = Array.from(asks!)[0];
const bids = await group.loadSerum3BidsForMarket(
client,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
const highestBid = Array.from(asks!)![0];
function getMaxSourceForTokenSwapWrapper(src, tgt) {
// console.log();
console.log(
`getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` +
mangoAccount.getMaxSourceUiForTokenSwap(
@ -409,15 +440,23 @@ async function main() {
// bid max perp
try {
const clientId = Math.floor(Math.random() * 99999);
await mangoAccount.reload(client);
await group.reloadAll(client);
const price =
group.banksMapByName.get('BTC')![0].uiPrice! -
Math.floor(Math.random() * 100);
const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi(
group,
perpMarket.perpMarketIndex,
1,
);
const baseQty = quoteQty / price;
console.log(
` simHealthRatioWithPerpBidUiChanges - ${mangoAccount.simHealthRatioWithPerpBidUiChanges(
group,
perpMarket.perpMarketIndex,
baseQty,
)}`,
);
console.log(
`...placing max qty perp bid clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
);
@ -457,8 +496,8 @@ async function main() {
mangoAccount.getMaxQuoteForPerpBidUi(
group,
perpMarket.perpMarketIndex,
1,
) * 1.02;
const baseQty = quoteQty / price;
console.log(
`...placing max qty * 1.02 perp bid clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
@ -478,6 +517,7 @@ async function main() {
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
} catch (error) {
console.log(error);
console.log('Errored out as expected');
}
@ -490,7 +530,13 @@ async function main() {
const baseQty = mangoAccount.getMaxBaseForPerpAskUi(
group,
perpMarket.perpMarketIndex,
1,
);
console.log(
` simHealthRatioWithPerpAskUiChanges - ${mangoAccount.simHealthRatioWithPerpAskUiChanges(
group,
perpMarket.perpMarketIndex,
baseQty,
)}`,
);
const quoteQty = baseQty * price;
console.log(
@ -521,11 +567,8 @@ async function main() {
group.banksMapByName.get('BTC')![0].uiPrice! +
Math.floor(Math.random() * 100);
const baseQty =
mangoAccount.getMaxBaseForPerpAskUi(
group,
perpMarket.perpMarketIndex,
1,
) * 1.02;
mangoAccount.getMaxBaseForPerpAskUi(group, perpMarket.perpMarketIndex) *
1.02;
const quoteQty = baseQty * price;
console.log(
`...placing max qty perp ask * 1.02 clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
@ -545,6 +588,7 @@ async function main() {
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
} catch (error) {
console.log(error);
console.log('Errored out as expected');
}
@ -557,54 +601,54 @@ async function main() {
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// // scenario 2
// // make + take orders
// try {
// const clientId = Math.floor(Math.random() * 99999);
// const price = group.banksMapByName.get('BTC')![0].uiPrice!;
// console.log(`...placing perp bid ${clientId} at ${price}`);
// const sig = await client.perpPlaceOrder(
// group,
// mangoAccount,
// perpMarket.perpMarketIndex,
// PerpOrderSide.bid,
// price,
// 0.01,
// price * 0.01,
// clientId,
// PerpOrderType.limit,
// 0, //Date.now() + 200,
// 1,
// );
// console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// } catch (error) {
// console.log(error);
// }
// try {
// const clientId = Math.floor(Math.random() * 99999);
// const price = group.banksMapByName.get('BTC')![0].uiPrice!;
// console.log(`...placing perp ask ${clientId} at ${price}`);
// const sig = await client.perpPlaceOrder(
// group,
// mangoAccount,
// perpMarket.perpMarketIndex,
// PerpOrderSide.ask,
// price,
// 0.01,
// price * 0.011,
// clientId,
// PerpOrderType.limit,
// 0, //Date.now() + 200,
// 1,
// );
// console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// } catch (error) {
// console.log(error);
// }
// // // should be able to cancel them : know bug
// // console.log(`...cancelling all perp orders`);
// // sig = await client.perpCancelAllOrders(group, mangoAccount, perpMarket.perpMarketIndex, 10);
// // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// scenario 2
// make + take orders
try {
const clientId = Math.floor(Math.random() * 99999);
const price = group.banksMapByName.get('BTC')![0].uiPrice!;
console.log(`...placing perp bid ${clientId} at ${price}`);
const sig = await client.perpPlaceOrder(
group,
mangoAccount,
perpMarket.perpMarketIndex,
PerpOrderSide.bid,
price,
0.01,
price * 0.01,
clientId,
PerpOrderType.limit,
0, //Date.now() + 200,
1,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
} catch (error) {
console.log(error);
}
try {
const clientId = Math.floor(Math.random() * 99999);
const price = group.banksMapByName.get('BTC')![0].uiPrice!;
console.log(`...placing perp ask ${clientId} at ${price}`);
const sig = await client.perpPlaceOrder(
group,
mangoAccount,
perpMarket.perpMarketIndex,
PerpOrderSide.ask,
price,
0.01,
price * 0.011,
clientId,
PerpOrderType.limit,
0, //Date.now() + 200,
1,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
} catch (error) {
console.log(error);
}
// // should be able to cancel them : know bug
// console.log(`...cancelling all perp orders`);
// sig = await client.perpCancelAllOrders(group, mangoAccount, perpMarket.perpMarketIndex, 10);
// console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
const bids: BookSide = await perpMarket?.loadBids(client)!;
console.log(`bids - ${Array.from(bids.items())}`);
@ -619,12 +663,14 @@ async function main() {
console.log(`current funding rate per hour is ${fr}`);
const eq = await perpMarket?.loadEventQueue(client)!;
console.log(`raw events - ${eq.rawEvents}`);
console.log(
`raw events - ${JSON.stringify(eq.eventsSince(new BN(0)), null, 2)}`,
);
// sleep so that keeper can catch up
await new Promise((r) => setTimeout(r, 2000));
// make+take orders should have cancelled each other, and if keeper has already cranked, then should not appear in position
// make+take orders should have cancelled each other, and if keeper has already cranked, then should not appear in position or we see a small quotePositionNative
await group.reloadAll(client);
await mangoAccount.reload(client);
console.log(`${mangoAccount.toString(group)}`);

View File

@ -0,0 +1,48 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import { MangoClient } from '../../client';
import { MANGO_V4_ID } from '../../constants';
// For easy switching between mainnet and devnet, default is mainnet
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 MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK || '';
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(CLUSTER_URL!, options);
// Throwaway keypair
const user = new Keypair();
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
CLUSTER,
MANGO_V4_ID[CLUSTER],
{},
'get-program-accounts',
);
// Load mango account
let mangoAccount = await client.getMangoAccountForPublicKey(
new PublicKey(MANGO_ACCOUNT_PK),
);
await mangoAccount.reload(client);
// Load group for mango account
const group = await client.getGroup(mangoAccount.group);
await group.reloadAll(client);
// Log OB
const perpMarket = group.getPerpMarketByName('BTC-PERP');
while (true) {
await new Promise((r) => setTimeout(r, 2000));
console.clear();
console.log(await perpMarket.logOb(client));
}
}
main();

View File

@ -0,0 +1,514 @@
import { AnchorProvider, BN, Wallet } from '@project-serum/anchor';
import {
Cluster,
Connection,
Keypair,
PublicKey,
TransactionInstruction,
} from '@solana/web3.js';
import fs from 'fs';
import path from 'path';
import { Group } from '../../accounts/group';
import { MangoAccount } from '../../accounts/mangoAccount';
import {
BookSide,
PerpMarket,
PerpMarketIndex,
PerpOrderSide,
PerpOrderType,
} from '../../accounts/perp';
import { MangoClient } from '../../client';
import { MANGO_V4_ID } from '../../constants';
import { toUiDecimalsForQuote } from '../../utils';
import { sendTransaction } from '../../utils/rpc';
import {
makeCheckAndSetSequenceNumberIx,
makeInitSequenceEnforcerAccountIx,
seqEnforcerProgramIds,
} from './sequence-enforcer-util';
// TODO switch to more efficient async logging if available in nodejs
// 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 || '';
// Load configuration
const paramsFileName = process.env.PARAMS || 'default.json';
const params = JSON.parse(
fs.readFileSync(
path.resolve(__dirname, `./params/${paramsFileName}`),
'utf-8',
),
);
const control = { isRunning: true, interval: params.interval };
// State which is passed around
type State = {
mangoAccount: MangoAccount;
lastMangoAccountUpdate: number;
marketContexts: Map<PerpMarketIndex, MarketContext>;
};
type MarketContext = {
params: any;
market: PerpMarket;
bids: BookSide;
asks: BookSide;
lastBookUpdate: number;
aggBid: number | undefined;
aggAsk: number | undefined;
ftxMid: number | undefined;
sequenceAccount: PublicKey;
sequenceAccountBump: number;
sentBidPrice: number;
sentAskPrice: number;
lastOrderUpdate: number;
};
// Refresh mango account and perp market order books
async function refreshState(
client: MangoClient,
group: Group,
mangoAccount: MangoAccount,
marketContexts: Map<PerpMarketIndex, MarketContext>,
): Promise<State> {
const ts = Date.now() / 1000;
// TODO do all updates in one RPC call
await Promise.all([group.reloadAll(client), mangoAccount.reload(client)]);
for (const perpMarket of Array.from(
group.perpMarketsMapByMarketIndex.values(),
)) {
const mc = marketContexts.get(perpMarket.perpMarketIndex)!;
mc.market = perpMarket;
mc.bids = await perpMarket.loadBids(client);
mc.asks = await perpMarket.loadAsks(client);
mc.lastBookUpdate = ts;
}
return {
mangoAccount,
lastMangoAccountUpdate: ts,
marketContexts,
};
}
// Initialiaze sequence enforcer accounts
async function initSequenceEnforcerAccounts(
client: MangoClient,
marketContexts: MarketContext[],
) {
const seqAccIxs = marketContexts.map((mc) =>
makeInitSequenceEnforcerAccountIx(
mc.sequenceAccount,
(client.program.provider as AnchorProvider).wallet.publicKey,
mc.sequenceAccountBump,
mc.market.name,
CLUSTER,
),
);
while (true) {
try {
const sig = await sendTransaction(
client.program.provider as AnchorProvider,
seqAccIxs,
[],
);
console.log(
`Sequence enforcer accounts created, sig https://explorer.solana.com/tx/${sig}?cluster=${
CLUSTER == 'devnet' ? 'devnet' : ''
}`,
);
} catch (e) {
console.log('Failed to initialize sequence enforcer accounts!');
console.log(e);
continue;
}
break;
}
}
// Cancel all orders on exit
async function onExit(
client: MangoClient,
group: Group,
mangoAccount: MangoAccount,
marketContexts: MarketContext[],
) {
const ixs: TransactionInstruction[] = [];
for (const mc of marketContexts) {
const cancelAllIx = await client.perpCancelAllOrdersIx(
group,
mangoAccount,
mc.market.perpMarketIndex,
10,
);
ixs.push(cancelAllIx);
}
await sendTransaction(client.program.provider as AnchorProvider, ixs, []);
}
// Main driver for the market maker
async function fullMarketMaker() {
// Load client
const options = AnchorProvider.defaultOptions();
const connection = new Connection(CLUSTER_URL!, options);
const user = Keypair.fromSecretKey(
Buffer.from(JSON.parse(fs.readFileSync(USER_KEYPAIR!, 'utf-8'))),
);
// TODO: make work for delegate
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
CLUSTER,
MANGO_V4_ID[CLUSTER],
{},
'get-program-accounts',
);
// Load mango account
let mangoAccount = await client.getMangoAccountForPublicKey(
new PublicKey(MANGO_ACCOUNT_PK),
);
console.log(
`MangoAccount ${mangoAccount.publicKey} for user ${user.publicKey}`,
);
await mangoAccount.reload(client);
// Load group
const group = await client.getGroup(mangoAccount.group);
await group.reloadAll(client);
// Cancel all existing orders
for (const perpMarket of Array.from(
group.perpMarketsMapByMarketIndex.values(),
)) {
await client.perpCancelAllOrders(
group,
mangoAccount,
perpMarket.perpMarketIndex,
10,
);
}
// Build and maintain an aggregate context object per market
const marketContexts: Map<PerpMarketIndex, MarketContext> = new Map();
for (const perpMarket of Array.from(
group.perpMarketsMapByMarketIndex.values(),
)) {
const [sequenceAccount, sequenceAccountBump] =
await PublicKey.findProgramAddress(
[
Buffer.from(perpMarket.name, 'utf-8'),
(
client.program.provider as AnchorProvider
).wallet.publicKey.toBytes(),
],
seqEnforcerProgramIds[CLUSTER],
);
marketContexts.set(perpMarket.perpMarketIndex, {
params: params.assets[perpMarket.name.replace('-PERP', '')].perp,
market: perpMarket,
bids: await perpMarket.loadBids(client),
asks: await perpMarket.loadAsks(client),
lastBookUpdate: 0,
sequenceAccount,
sequenceAccountBump,
sentBidPrice: 0,
sentAskPrice: 0,
lastOrderUpdate: 0,
// TODO
aggBid: undefined,
aggAsk: undefined,
ftxMid: undefined,
});
}
// Init sequence enforcer accounts
await initSequenceEnforcerAccounts(
client,
Array.from(marketContexts.values()),
);
// Load state first time
let state = await refreshState(client, group, mangoAccount, marketContexts);
// Add handler for e.g. CTRL+C
process.on('SIGINT', function () {
console.log('Caught keyboard interrupt. Canceling orders');
control.isRunning = false;
onExit(client, group, mangoAccount, Array.from(marketContexts.values()));
});
// Loop indefinitely
while (control.isRunning) {
try {
// TODO update this in a non blocking manner
state = await refreshState(client, group, mangoAccount, marketContexts);
mangoAccount = state.mangoAccount;
// Calculate pf level values
let pfQuoteValue: number | undefined = 0;
for (const mc of Array.from(marketContexts.values())) {
const pos = mangoAccount.getPerpPositionUi(
group,
mc.market.perpMarketIndex,
);
// TODO use ftx to get mid then also combine with books from other exchanges
const midWorkaround = mc.market.uiPrice;
if (midWorkaround) {
pfQuoteValue += pos * midWorkaround;
} else {
pfQuoteValue = undefined;
console.log(
`Breaking pfQuoteValue computation, since mid is undefined for ${mc.market.name}!`,
);
break;
}
}
// Don't proceed if we don't have pfQuoteValue yet
if (pfQuoteValue === undefined) {
console.log(
`Continuing control loop, since pfQuoteValue is undefined!`,
);
continue;
}
// Update all orders on all markets
for (const mc of Array.from(marketContexts.values())) {
const ixs = await makeMarketUpdateInstructions(
client,
group,
mangoAccount,
mc,
pfQuoteValue,
);
if (ixs.length === 0) {
continue;
}
// TODO: batch ixs
const sig = await sendTransaction(
client.program.provider as AnchorProvider,
ixs,
group.addressLookupTablesList,
);
console.log(
`Orders for market updated, sig https://explorer.solana.com/tx/${sig}?cluster=${
CLUSTER == 'devnet' ? 'devnet' : ''
}`,
);
}
} catch (e) {
console.log(e);
} finally {
console.log(
`${new Date().toUTCString()} sleeping for ${control.interval / 1000}s`,
);
await new Promise((r) => setTimeout(r, control.interval));
}
}
}
async function makeMarketUpdateInstructions(
client: MangoClient,
group: Group,
mangoAccount: MangoAccount,
mc: MarketContext,
pfQuoteValue: number,
): Promise<TransactionInstruction[]> {
const perpMarketIndex = mc.market.perpMarketIndex;
const perpMarket = mc.market;
const aggBid = perpMarket.uiPrice; // TODO mc.aggBid;
const aggAsk = perpMarket.uiPrice; // TODO mc.aggAsk;
if (aggBid === undefined || aggAsk === undefined) {
console.log(`No Aggregate Book for ${mc.market.name}!`);
return [];
}
const leanCoeff = mc.params.leanCoeff;
const fairValue = (aggBid + aggAsk) / 2;
const aggSpread = (aggAsk - aggBid) / fairValue;
const requoteThresh = mc.params.requoteThresh;
const equity = toUiDecimalsForQuote(mangoAccount.getEquity(group));
const sizePerc = mc.params.sizePerc;
const quoteSize = equity * sizePerc;
const size = quoteSize / fairValue;
// TODO look at event queue as well for unprocessed fills
const basePos = mangoAccount.getPerpPositionUi(group, perpMarketIndex);
const lean = (-leanCoeff * basePos) / size;
const pfQuoteLeanCoeff = params.pfQuoteLeanCoeff || 0.001; // How much to move if pf pos is equal to equity
const pfQuoteLean = (pfQuoteValue / equity) * -pfQuoteLeanCoeff;
const charge = (mc.params.charge || 0.0015) + aggSpread / 2;
const bias = mc.params.bias;
const bidPrice = fairValue * (1 - charge + lean + bias + pfQuoteLean);
const askPrice = fairValue * (1 + charge + lean + bias + pfQuoteLean);
// TODO volatility adjustment
const modelBidPrice = perpMarket.uiPriceToLots(bidPrice);
const nativeBidSize = perpMarket.uiBaseToLots(size);
const modelAskPrice = perpMarket.uiPriceToLots(askPrice);
const nativeAskSize = perpMarket.uiBaseToLots(size);
const bids = mc.bids;
const asks = mc.asks;
const bestBid = bids.best();
const bestAsk = asks.best();
const bookAdjBid =
bestAsk !== undefined
? BN.min(bestAsk.priceLots.sub(new BN(1)), modelBidPrice)
: modelBidPrice;
const bookAdjAsk =
bestBid !== undefined
? BN.max(bestBid.priceLots.add(new BN(1)), modelAskPrice)
: modelAskPrice;
// TODO use order book to requote if size has changed
// TODO
// const takeSpammers = mc.params.takeSpammers;
// const spammerCharge = mc.params.spammerCharge;
let moveOrders = false;
if (mc.lastBookUpdate >= mc.lastOrderUpdate + 2) {
// console.log(` - moveOrders - 303`);
// If mango book was updated recently, then MangoAccount was also updated
const openOrders = await mangoAccount.loadPerpOpenOrdersForMarket(
client,
group,
perpMarketIndex,
);
moveOrders = openOrders.length < 2 || openOrders.length > 2;
for (const o of openOrders) {
const refPrice = o.side === 'buy' ? bookAdjBid : bookAdjAsk;
moveOrders =
moveOrders ||
Math.abs(o.priceLots.toNumber() / refPrice.toNumber() - 1) >
requoteThresh;
}
} else {
// console.log(
// ` - moveOrders - 319, mc.lastBookUpdate ${mc.lastBookUpdate}, mc.lastOrderUpdate ${mc.lastOrderUpdate}`,
// );
// If order was updated before MangoAccount, then assume that sent order already executed
moveOrders =
moveOrders ||
Math.abs(mc.sentBidPrice / bookAdjBid.toNumber() - 1) > requoteThresh ||
Math.abs(mc.sentAskPrice / bookAdjAsk.toNumber() - 1) > requoteThresh;
}
// Start building the transaction
const instructions: TransactionInstruction[] = [
makeCheckAndSetSequenceNumberIx(
mc.sequenceAccount,
(client.program.provider as AnchorProvider).wallet.publicKey,
Date.now(),
CLUSTER,
),
];
// TODO Clear 1 lot size orders at the top of book that bad people use to manipulate the price
if (moveOrders) {
// Cancel all, requote
const cancelAllIx = await client.perpCancelAllOrdersIx(
group,
mangoAccount,
perpMarketIndex,
10,
);
const expiryTimestamp =
params.tif !== undefined ? Date.now() / 1000 + params.tif : 0;
const placeBidIx = await client.perpPlaceOrderIx(
group,
mangoAccount,
perpMarketIndex,
PerpOrderSide.bid,
// TODO fix this, native to ui to native
perpMarket.priceLotsToUi(bookAdjBid),
perpMarket.baseLotsToUi(nativeBidSize),
undefined,
Date.now(),
PerpOrderType.postOnlySlide,
expiryTimestamp,
20,
);
const placeAskIx = await client.perpPlaceOrderIx(
group,
mangoAccount,
perpMarketIndex,
PerpOrderSide.ask,
perpMarket.priceLotsToUi(bookAdjAsk),
perpMarket.baseLotsToUi(nativeAskSize),
undefined,
Date.now(),
PerpOrderType.postOnlySlide,
expiryTimestamp,
20,
);
instructions.push(cancelAllIx);
const posAsTradeSizes = basePos / size;
if (posAsTradeSizes < 15) {
instructions.push(placeBidIx);
}
if (posAsTradeSizes > -15) {
instructions.push(placeAskIx);
}
console.log(
`Requoting for market ${mc.market.name} sentBid: ${
mc.sentBidPrice
} newBid: ${bookAdjBid} sentAsk: ${
mc.sentAskPrice
} newAsk: ${bookAdjAsk} pfLean: ${(pfQuoteLean * 10000).toFixed(
1,
)} aggBid: ${aggBid} addAsk: ${aggAsk}`,
);
mc.sentBidPrice = bookAdjBid.toNumber();
mc.sentAskPrice = bookAdjAsk.toNumber();
mc.lastOrderUpdate = Date.now() / 1000;
} else {
console.log(
`Not requoting for market ${mc.market.name}. No need to move orders`,
);
}
// If instruction is only the sequence enforcement, then just send empty
if (instructions.length === 1) {
return [];
} else {
return instructions;
}
}
function startMarketMaker() {
if (control.isRunning) {
fullMarketMaker().finally(startMarketMaker);
}
}
startMarketMaker();

View File

@ -0,0 +1,16 @@
{
"interval": 1000,
"batch": 1,
"assets": {
"BTC": {
"perp": {
"sizePerc": 0.05,
"leanCoeff": 0.00025,
"bias": 0.0,
"requoteThresh": 0.0002,
"takeSpammers": true,
"spammerCharge": 2
}
}
}
}

View File

@ -0,0 +1,68 @@
import { BN } from '@project-serum/anchor';
import {
PublicKey,
SystemProgram,
TransactionInstruction,
} from '@solana/web3.js';
import { createHash } from 'crypto';
export const seqEnforcerProgramIds = {
devnet: new PublicKey('FBngRHN4s5cmHagqy3Zd6xcK3zPJBeX5DixtHFbBhyCn'),
testnet: new PublicKey('FThcgpaJM8WiEbK5rw3i31Ptb8Hm4rQ27TrhfzeR1uUy'),
'mainnet-beta': new PublicKey('GDDMwNyyx8uB6zrqwBFHjLLG3TBYk2F8Az4yrQC5RzMp'),
};
export function makeInitSequenceEnforcerAccountIx(
account: PublicKey,
ownerPk: PublicKey,
bump: number,
sym: string,
cluster: string,
): TransactionInstruction {
const keys = [
{ isSigner: false, isWritable: true, pubkey: account },
{ isSigner: true, isWritable: true, pubkey: ownerPk },
{ isSigner: false, isWritable: false, pubkey: SystemProgram.programId },
];
const variant = createHash('sha256')
.update('global:initialize')
.digest()
.slice(0, 8);
const bumpData = new BN(bump).toBuffer('le', 1);
const strLen = new BN(sym.length).toBuffer('le', 4);
const symEncoded = Buffer.from(sym);
const data = Buffer.concat([variant, bumpData, strLen, symEncoded]);
return new TransactionInstruction({
keys,
data,
programId: seqEnforcerProgramIds[cluster],
});
}
export function makeCheckAndSetSequenceNumberIx(
sequenceAccount: PublicKey,
ownerPk: PublicKey,
seqNum: number,
cluster,
): TransactionInstruction {
const keys = [
{ isSigner: false, isWritable: true, pubkey: sequenceAccount },
{ isSigner: true, isWritable: false, pubkey: ownerPk },
];
const variant = createHash('sha256')
.update('global:check_and_set_sequence_number')
.digest()
.slice(0, 8);
const seqNumBuffer = new BN(seqNum).toBuffer('le', 8);
const data = Buffer.concat([variant, seqNumBuffer]);
return new TransactionInstruction({
keys,
data,
programId: seqEnforcerProgramIds[cluster],
});
}

View File

@ -0,0 +1,111 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { Group } from '../../accounts/group';
import { MangoAccount } from '../../accounts/mangoAccount';
import { PerpMarket, PerpOrderSide, PerpOrderType } from '../../accounts/perp';
import { MangoClient } from '../../client';
import { MANGO_V4_ID } from '../../constants';
import { toUiDecimalsForQuote } from '../../utils';
// For easy switching between mainnet and devnet, default is mainnet
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 || '';
async function takeOrder(
client: MangoClient,
group: Group,
mangoAccount: MangoAccount,
perpMarket: PerpMarket,
side: PerpOrderSide,
) {
await mangoAccount.reload(client);
const size = Math.random() * 0.001;
const price =
side === PerpOrderSide.bid
? perpMarket.uiPrice * 1.01
: perpMarket.uiPrice * 0.99;
console.log(
`${perpMarket.name} taking with a ${
side === PerpOrderSide.bid ? 'bid' : 'ask'
} at price ${price.toFixed(4)} and size ${size.toFixed(6)}`,
);
const oldPosition = mangoAccount.getPerpPosition(perpMarket.perpMarketIndex);
if (oldPosition) {
console.log(
`- before base: ${perpMarket.baseLotsToUi(
oldPosition.basePositionLots,
)}, quote: ${toUiDecimalsForQuote(oldPosition.quotePositionNative)}`,
);
}
await client.perpPlaceOrder(
group,
mangoAccount,
perpMarket.perpMarketIndex,
side,
price,
size,
undefined,
Date.now(),
PerpOrderType.market,
0,
10,
);
// Sleep to see change, alternatively we could reload account with processed commitmment
await new Promise((r) => setTimeout(r, 5000));
await mangoAccount.reload(client);
const newPosition = mangoAccount.getPerpPosition(perpMarket.perpMarketIndex);
if (newPosition) {
console.log(
`- after base: ${perpMarket.baseLotsToUi(
newPosition.basePositionLots,
)}, quote: ${toUiDecimalsForQuote(newPosition.quotePositionNative)}`,
);
}
}
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(CLUSTER_URL!, options);
const user = Keypair.fromSecretKey(
Buffer.from(JSON.parse(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],
{},
'get-program-accounts',
);
// Load mango account
let mangoAccount = await client.getMangoAccountForPublicKey(
new PublicKey(MANGO_ACCOUNT_PK),
);
await mangoAccount.reload(client);
// Load group
const group = await client.getGroup(mangoAccount.group);
await group.reloadAll(client);
// Take on OB
const perpMarket = group.getPerpMarketByName('BTC-PERP');
while (true) {
await takeOrder(client, group, mangoAccount, perpMarket, PerpOrderSide.bid);
await takeOrder(client, group, mangoAccount, perpMarket, PerpOrderSide.ask);
}
}
main();

View File

@ -51,6 +51,7 @@ export async function sendTransaction(
latestBlockhash.blockhash != null &&
latestBlockhash.lastValidBlockHeight != null
) {
// TODO: tyler, can we remove these?
console.log('confirming via blockhash');
status = (
await connection.confirmTransaction(
@ -63,6 +64,7 @@ export async function sendTransaction(
)
).value;
} else {
// TODO: tyler, can we remove these?
console.log('confirming via timeout');
status = (await connection.confirmTransaction(signature, 'processed'))
.value;