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:
parent
6808171ee3
commit
8e919bb741
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)}`);
|
||||
|
|
|
@ -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();
|
|
@ -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();
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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],
|
||||
});
|
||||
}
|
|
@ -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();
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue