ts: Additional serum3 support (#196)
* get bids and asks for a user on a serum3 market * get orderbook for a market * get max bid or ask that a user can place for a market * simulate health if a bid or ask were to be placed misc: * fix remaining accounts list for health when placing perp bids Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> format Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> format Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> remove testing code Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> script adjustment Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> comments Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
parent
4a1865be6c
commit
c66dd882b6
|
@ -22,3 +22,5 @@ ts/client/**/*.js
|
|||
ts/client/**/*.js.map
|
||||
migrations/*.js
|
||||
migrations/*.js.map
|
||||
|
||||
ts/client/src/scripts/archive/ts.ts
|
|
@ -224,10 +224,10 @@ export class I80F48 {
|
|||
}
|
||||
|
||||
/** @internal */
|
||||
export const ONE_I80F48 = I80F48.fromString('1');
|
||||
export const ONE_I80F48 = I80F48.fromNumber(1);
|
||||
/** @internal */
|
||||
export const ZERO_I80F48 = I80F48.fromString('0');
|
||||
export const ZERO_I80F48 = I80F48.fromNumber(0);
|
||||
/** @internal */
|
||||
export const NEG_ONE_I80F48 = I80F48.fromString('-1');
|
||||
export const HUNDRED_I80F48 = I80F48.fromString('100');
|
||||
export const NEG_ONE_I80F48 = I80F48.fromNumber(-1);
|
||||
export const HUNDRED_I80F48 = I80F48.fromNumber(100);
|
||||
export const MAX_I80F48 = new I80F48(I80F48.MAX_BN);
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { BorshAccountsCoder } from '@project-serum/anchor';
|
||||
import { coder } from '@project-serum/anchor/dist/cjs/spl/token';
|
||||
import { Market } from '@project-serum/serum';
|
||||
import {
|
||||
getFeeRates,
|
||||
getFeeTier,
|
||||
Market,
|
||||
Orderbook,
|
||||
} from '@project-serum/serum';
|
||||
import { parsePriceData, PriceData } from '@pythnetwork/client';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import BN from 'bn.js';
|
||||
|
@ -68,8 +73,9 @@ export class Group {
|
|||
public banksMapByName: Map<string, Bank[]>,
|
||||
public banksMapByMint: Map<string, Bank[]>,
|
||||
public banksMapByTokenIndex: Map<number, Bank[]>,
|
||||
public serum3MarketsMap: Map<string, Serum3Market>,
|
||||
public serum3MarketsMapByExternal: Map<string, Serum3Market>,
|
||||
public serum3MarketExternalsMap: Map<string, Market>,
|
||||
// TODO rethink key
|
||||
public perpMarketsMap: Map<string, PerpMarket>,
|
||||
public mintInfosMapByTokenIndex: Map<number, MintInfo>,
|
||||
public mintInfosMapByMint: Map<string, MintInfo>,
|
||||
|
@ -77,12 +83,6 @@ export class Group {
|
|||
public vaultAmountsMap: Map<string, number>,
|
||||
) {}
|
||||
|
||||
public findSerum3Market(marketIndex: number): Serum3Market | undefined {
|
||||
return Array.from(this.serum3MarketsMap.values()).find(
|
||||
(serum3Market) => serum3Market.marketIndex === marketIndex,
|
||||
);
|
||||
}
|
||||
|
||||
public async reloadAll(client: MangoClient) {
|
||||
let ids: Id | undefined = undefined;
|
||||
|
||||
|
@ -180,14 +180,17 @@ export class Group {
|
|||
serum3Markets = await client.serum3GetMarkets(this);
|
||||
}
|
||||
|
||||
this.serum3MarketsMap = new Map(
|
||||
serum3Markets.map((serum3Market) => [serum3Market.name, serum3Market]),
|
||||
this.serum3MarketsMapByExternal = new Map(
|
||||
serum3Markets.map((serum3Market) => [
|
||||
serum3Market.serumMarketExternal.toBase58(),
|
||||
serum3Market,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
public async reloadSerum3ExternalMarkets(client: MangoClient, ids?: Id) {
|
||||
const externalMarkets = await Promise.all(
|
||||
Array.from(this.serum3MarketsMap.values()).map((serum3Market) =>
|
||||
Array.from(this.serum3MarketsMapByExternal.values()).map((serum3Market) =>
|
||||
Market.load(
|
||||
client.program.provider.connection,
|
||||
serum3Market.serumMarketExternal,
|
||||
|
@ -198,10 +201,12 @@ export class Group {
|
|||
);
|
||||
|
||||
this.serum3MarketExternalsMap = new Map(
|
||||
Array.from(this.serum3MarketsMap.values()).map((serum3Market, index) => [
|
||||
serum3Market.name,
|
||||
externalMarkets[index],
|
||||
]),
|
||||
Array.from(this.serum3MarketsMapByExternal.values()).map(
|
||||
(serum3Market, index) => [
|
||||
serum3Market.serumMarketExternal.toBase58(),
|
||||
externalMarkets[index],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -316,6 +321,37 @@ export class Group {
|
|||
return I80F48.fromNumber(amount);
|
||||
}
|
||||
|
||||
public findSerum3Market(marketIndex: number): Serum3Market | undefined {
|
||||
return Array.from(this.serum3MarketsMapByExternal.values()).find(
|
||||
(serum3Market) => serum3Market.marketIndex === marketIndex,
|
||||
);
|
||||
}
|
||||
|
||||
public async loadSerum3BidsForMarket(
|
||||
client: MangoClient,
|
||||
externalMarketPk: PublicKey,
|
||||
): Promise<Orderbook> {
|
||||
return await this.serum3MarketsMapByExternal
|
||||
.get(externalMarketPk.toBase58())
|
||||
.loadBids(client, this);
|
||||
}
|
||||
|
||||
public async loadSerum3AsksForMarket(
|
||||
client: MangoClient,
|
||||
externalMarketPk: PublicKey,
|
||||
): Promise<Orderbook> {
|
||||
return await this.serum3MarketsMapByExternal
|
||||
.get(externalMarketPk.toBase58())
|
||||
.loadAsks(client, this);
|
||||
}
|
||||
|
||||
public getFeeRate(maker = true) {
|
||||
// TODO: fetch msrm/srm vault balance
|
||||
const feeTier = getFeeTier(0, 0);
|
||||
const rates = getFeeRates(feeTier);
|
||||
return maker ? rates.maker : rates.taker;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param mintPk
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
ZERO_I80F48,
|
||||
} from './I80F48';
|
||||
import { HealthType } from './mangoAccount';
|
||||
import { Serum3Market, Serum3Side } from './serum3';
|
||||
|
||||
// ░░░░
|
||||
//
|
||||
|
@ -37,14 +38,18 @@ import { HealthType } from './mangoAccount';
|
|||
// warning: this code is copy pasta from rust, keep in sync with health.rs
|
||||
|
||||
export class HealthCache {
|
||||
tokenInfos: TokenInfo[];
|
||||
serum3Infos: Serum3Info[];
|
||||
perpInfos: PerpInfo[];
|
||||
constructor(
|
||||
public tokenInfos: TokenInfo[],
|
||||
public serum3Infos: Serum3Info[],
|
||||
public perpInfos: PerpInfo[],
|
||||
) {}
|
||||
|
||||
constructor(dto: HealthCacheDto) {
|
||||
this.tokenInfos = dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto));
|
||||
this.serum3Infos = dto.serum3Infos.map((dto) => new Serum3Info(dto));
|
||||
this.perpInfos = dto.perpInfos.map((dto) => new PerpInfo(dto));
|
||||
static fromDto(dto) {
|
||||
return new HealthCache(
|
||||
dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)),
|
||||
dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)),
|
||||
dto.perpInfos.map((dto) => new PerpInfo(dto)),
|
||||
);
|
||||
}
|
||||
|
||||
public health(healthType: HealthType): I80F48 {
|
||||
|
@ -172,10 +177,54 @@ export class HealthCache {
|
|||
return this.findTokenInfoIndex(bank.tokenIndex);
|
||||
}
|
||||
|
||||
private static logHealthCache(debug: string, healthCache: HealthCache) {
|
||||
console.log(debug);
|
||||
adjustSerum3Reserved(
|
||||
// todo change indices to types from numbers
|
||||
marketIndex: number,
|
||||
baseTokenIndex: number,
|
||||
reservedBaseChange: I80F48,
|
||||
freeBaseChange: I80F48,
|
||||
quoteTokenIndex: number,
|
||||
reservedQuoteChange: I80F48,
|
||||
freeQuoteChange: I80F48,
|
||||
) {
|
||||
const baseEntryIndex = this.findTokenInfoIndex(baseTokenIndex);
|
||||
const quoteEntryIndex = this.findTokenInfoIndex(quoteTokenIndex);
|
||||
let reservedAmount = ZERO_I80F48;
|
||||
|
||||
const baseEntry = this.tokenInfos[baseEntryIndex];
|
||||
reservedAmount = reservedBaseChange.mul(baseEntry.oraclePrice);
|
||||
|
||||
const quoteEntry = this.tokenInfos[quoteEntryIndex];
|
||||
reservedAmount = reservedAmount.add(
|
||||
reservedQuoteChange.mul(quoteEntry.oraclePrice),
|
||||
);
|
||||
|
||||
// Apply it to the tokens
|
||||
baseEntry.serum3MaxReserved =
|
||||
baseEntry.serum3MaxReserved.add(reservedAmount);
|
||||
baseEntry.balance = baseEntry.balance.add(
|
||||
freeBaseChange.mul(baseEntry.oraclePrice),
|
||||
);
|
||||
quoteEntry.serum3MaxReserved =
|
||||
quoteEntry.serum3MaxReserved.add(reservedAmount);
|
||||
quoteEntry.balance = quoteEntry.balance.add(
|
||||
freeQuoteChange.mul(quoteEntry.oraclePrice),
|
||||
);
|
||||
|
||||
// Apply it to the serum3 info
|
||||
const serum3Info = this.serum3Infos.find(
|
||||
(serum3Info) => serum3Info.marketIndex === marketIndex,
|
||||
);
|
||||
serum3Info.reserved = serum3Info.reserved.add(reservedAmount);
|
||||
}
|
||||
|
||||
public static logHealthCache(debug: string, healthCache: HealthCache) {
|
||||
if (debug) console.log(debug);
|
||||
for (const token of healthCache.tokenInfos) {
|
||||
console.log(`${token.toString()}`);
|
||||
console.log(` {token.toString()}`);
|
||||
}
|
||||
for (const serum3Info of healthCache.serum3Infos) {
|
||||
console.log(` {serum3Info.toString(healthCache.tokenInfos)}`);
|
||||
}
|
||||
console.log(
|
||||
` assets ${healthCache.assets(
|
||||
|
@ -213,6 +262,118 @@ export class HealthCache {
|
|||
return adjustedCache.healthRatio(healthType);
|
||||
}
|
||||
|
||||
simHealthRatioWithSerum3BidChanges(
|
||||
group: Group,
|
||||
bidNativeQuoteAmount: I80F48,
|
||||
serum3Market: Serum3Market,
|
||||
healthType: HealthType = HealthType.init,
|
||||
): I80F48 {
|
||||
const adjustedCache: HealthCache = _.cloneDeep(this);
|
||||
const quoteBank = group.banksMapByTokenIndex.get(
|
||||
serum3Market.quoteTokenIndex,
|
||||
)[0];
|
||||
const quoteIndex = adjustedCache.getOrCreateTokenInfoIndex(quoteBank);
|
||||
const quote = adjustedCache.tokenInfos[quoteIndex];
|
||||
|
||||
// Move token balance to reserved funds in open orders,
|
||||
// essentially simulating a place order
|
||||
|
||||
// Reduce token balance for quote
|
||||
adjustedCache.tokenInfos[quoteIndex].balance = adjustedCache.tokenInfos[
|
||||
quoteIndex
|
||||
].balance.sub(bidNativeQuoteAmount.mul(quote.oraclePrice));
|
||||
|
||||
// Increase reserved in Serum3Info for quote
|
||||
adjustedCache.adjustSerum3Reserved(
|
||||
serum3Market.marketIndex,
|
||||
serum3Market.baseTokenIndex,
|
||||
ZERO_I80F48,
|
||||
ZERO_I80F48,
|
||||
serum3Market.quoteTokenIndex,
|
||||
bidNativeQuoteAmount,
|
||||
ZERO_I80F48,
|
||||
);
|
||||
return adjustedCache.healthRatio(healthType);
|
||||
}
|
||||
|
||||
simHealthRatioWithSerum3AskChanges(
|
||||
group: Group,
|
||||
askNativeBaseAmount: I80F48,
|
||||
serum3Market: Serum3Market,
|
||||
healthType: HealthType = HealthType.init,
|
||||
): I80F48 {
|
||||
const adjustedCache: HealthCache = _.cloneDeep(this);
|
||||
const baseBank = group.banksMapByTokenIndex.get(
|
||||
serum3Market.baseTokenIndex,
|
||||
)[0];
|
||||
const baseIndex = adjustedCache.getOrCreateTokenInfoIndex(baseBank);
|
||||
const base = adjustedCache.tokenInfos[baseIndex];
|
||||
|
||||
// Move token balance to reserved funds in open orders,
|
||||
// essentially simulating a place order
|
||||
|
||||
// Reduce token balance for base
|
||||
adjustedCache.tokenInfos[baseIndex].balance = adjustedCache.tokenInfos[
|
||||
baseIndex
|
||||
].balance.sub(askNativeBaseAmount.mul(base.oraclePrice));
|
||||
|
||||
// Increase reserved in Serum3Info for base
|
||||
adjustedCache.adjustSerum3Reserved(
|
||||
serum3Market.marketIndex,
|
||||
serum3Market.baseTokenIndex,
|
||||
askNativeBaseAmount,
|
||||
ZERO_I80F48,
|
||||
serum3Market.quoteTokenIndex,
|
||||
ZERO_I80F48,
|
||||
ZERO_I80F48,
|
||||
);
|
||||
return adjustedCache.healthRatio(healthType);
|
||||
}
|
||||
|
||||
private static binaryApproximationSearch(
|
||||
left: I80F48,
|
||||
leftRatio: I80F48,
|
||||
right: I80F48,
|
||||
rightRatio: I80F48,
|
||||
targetRatio: I80F48,
|
||||
healthRatioAfterActionFn: (I80F48) => I80F48,
|
||||
) {
|
||||
const maxIterations = 40;
|
||||
// TODO: make relative to health ratio decimals? Might be over engineering
|
||||
const targetError = I80F48.fromNumber(0.001);
|
||||
|
||||
if (
|
||||
(leftRatio.sub(targetRatio).isPos() &&
|
||||
rightRatio.sub(targetRatio).isPos()) ||
|
||||
(leftRatio.sub(targetRatio).isNeg() &&
|
||||
rightRatio.sub(targetRatio).isNeg())
|
||||
) {
|
||||
throw new Error(
|
||||
`internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}`,
|
||||
);
|
||||
}
|
||||
|
||||
let newAmount;
|
||||
for (const key of Array(maxIterations).fill(0).keys()) {
|
||||
newAmount = left.add(right).mul(I80F48.fromNumber(0.5));
|
||||
const newAmountRatio = healthRatioAfterActionFn(newAmount);
|
||||
const error = newAmountRatio.sub(targetRatio);
|
||||
if (error.isPos() && error.lt(targetError)) {
|
||||
return newAmount;
|
||||
}
|
||||
if (newAmountRatio.gt(targetRatio) != rightRatio.gt(targetRatio)) {
|
||||
left = newAmount;
|
||||
} else {
|
||||
right = newAmount;
|
||||
rightRatio = newAmountRatio;
|
||||
}
|
||||
}
|
||||
console.error(
|
||||
`Unable to get targetRatio within ${maxIterations} iterations`,
|
||||
);
|
||||
return newAmount;
|
||||
}
|
||||
|
||||
getMaxSourceForTokenSwap(
|
||||
group: Group,
|
||||
sourceMintPk: PublicKey,
|
||||
|
@ -287,54 +448,10 @@ export class HealthCache {
|
|||
.max(ZERO_I80F48);
|
||||
const cache0 = cacheAfterSwap(point0Amount);
|
||||
const point0Ratio = cache0.healthRatio(HealthType.init);
|
||||
const point0Health = cache0.health(HealthType.init);
|
||||
const cache1 = cacheAfterSwap(point1Amount);
|
||||
const point1Ratio = cache1.healthRatio(HealthType.init);
|
||||
const point1Health = cache1.health(HealthType.init);
|
||||
|
||||
function binaryApproximationSearch(
|
||||
left: I80F48,
|
||||
leftRatio: I80F48,
|
||||
right: I80F48,
|
||||
rightRatio: I80F48,
|
||||
targetRatio: I80F48,
|
||||
) {
|
||||
const maxIterations = 20;
|
||||
// TODO: make relative to health ratio decimals? Might be over engineering
|
||||
const targetError = I80F48.fromString('0.001');
|
||||
|
||||
if (
|
||||
(leftRatio.sub(targetRatio).isPos() &&
|
||||
rightRatio.sub(targetRatio).isPos()) ||
|
||||
(leftRatio.sub(targetRatio).isNeg() &&
|
||||
rightRatio.sub(targetRatio).isNeg())
|
||||
) {
|
||||
throw new Error(
|
||||
`internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}`,
|
||||
);
|
||||
}
|
||||
|
||||
let newAmount;
|
||||
for (const key of Array(maxIterations).fill(0).keys()) {
|
||||
newAmount = left.add(right).mul(I80F48.fromString('0.5'));
|
||||
const newAmountRatio = healthRatioAfterSwap(newAmount);
|
||||
const error = newAmountRatio.sub(targetRatio);
|
||||
if (error.isPos() && error.lt(targetError)) {
|
||||
return newAmount;
|
||||
}
|
||||
if (newAmountRatio.gt(targetRatio) != rightRatio.gt(targetRatio)) {
|
||||
left = newAmount;
|
||||
} else {
|
||||
right = newAmount;
|
||||
rightRatio = newAmountRatio;
|
||||
}
|
||||
}
|
||||
console.error(
|
||||
`Unable to get targetRatio within ${maxIterations} iterations`,
|
||||
);
|
||||
return newAmount;
|
||||
}
|
||||
|
||||
let amount: I80F48;
|
||||
|
||||
if (
|
||||
|
@ -365,30 +482,24 @@ export class HealthCache {
|
|||
const zeroHealthAmount = point1Amount.add(
|
||||
point1Health.div(source.initLiabWeight.sub(target.initAssetWeight)),
|
||||
);
|
||||
// console.log(`point1Amount ${point1Amount}`);
|
||||
// console.log(`point1Health ${point1Health}`);
|
||||
// console.log(`point1Ratio ${point1Ratio}`);
|
||||
// console.log(`point0Amount ${point0Amount}`);
|
||||
// console.log(`point0Health ${point0Health}`);
|
||||
// console.log(`point0Ratio ${point0Ratio}`);
|
||||
// console.log(`zeroHealthAmount ${zeroHealthAmount}`);
|
||||
const zeroHealthRatio = healthRatioAfterSwap(zeroHealthAmount);
|
||||
// console.log(`zeroHealthRatio ${zeroHealthRatio}`);
|
||||
amount = binaryApproximationSearch(
|
||||
amount = HealthCache.binaryApproximationSearch(
|
||||
point1Amount,
|
||||
point1Ratio,
|
||||
zeroHealthAmount,
|
||||
zeroHealthRatio,
|
||||
minRatio,
|
||||
healthRatioAfterSwap,
|
||||
);
|
||||
} else if (point0Ratio.gte(minRatio)) {
|
||||
// Must be between point0Amount and point1Amount.
|
||||
amount = binaryApproximationSearch(
|
||||
amount = HealthCache.binaryApproximationSearch(
|
||||
point0Amount,
|
||||
point0Ratio,
|
||||
point1Amount,
|
||||
point1Ratio,
|
||||
minRatio,
|
||||
healthRatioAfterSwap,
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
|
@ -404,6 +515,122 @@ export class HealthCache {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
getMaxForSerum3Order(
|
||||
group: Group,
|
||||
serum3Market: Serum3Market,
|
||||
side: Serum3Side,
|
||||
minRatio: I80F48,
|
||||
) {
|
||||
const baseBank = group.banksMapByTokenIndex.get(
|
||||
serum3Market.baseTokenIndex,
|
||||
)[0];
|
||||
const quoteBank = group.banksMapByTokenIndex.get(
|
||||
serum3Market.quoteTokenIndex,
|
||||
)[0];
|
||||
|
||||
const healthCacheClone: HealthCache = _.cloneDeep(this);
|
||||
|
||||
const baseIndex = healthCacheClone.getOrCreateTokenInfoIndex(baseBank);
|
||||
const quoteIndex = healthCacheClone.getOrCreateTokenInfoIndex(quoteBank);
|
||||
const base = healthCacheClone.tokenInfos[baseIndex];
|
||||
const quote = healthCacheClone.tokenInfos[quoteIndex];
|
||||
|
||||
// Binary search between current health (0 sized new order) and
|
||||
// an amount to trade which will bring health to 0.
|
||||
|
||||
// Current health and amount i.e. 0
|
||||
const initialAmount = ZERO_I80F48;
|
||||
const initialHealth = this.health(HealthType.init);
|
||||
const initialRatio = this.healthRatio(HealthType.init);
|
||||
if (initialRatio.lte(ZERO_I80F48)) {
|
||||
return ZERO_I80F48;
|
||||
}
|
||||
|
||||
// Amount which would bring health to 0
|
||||
// amount = max(A_deposits, B_borrows) + init_health / (A_liab_weight - B_asset_weight)
|
||||
// A is what we would be essentially swapping for B
|
||||
// So when its an ask, then base->quote,
|
||||
// and when its a bid, then quote->bid
|
||||
let zeroAmount;
|
||||
if (side == Serum3Side.ask) {
|
||||
const quoteBorrows = quote.balance.lt(ZERO_I80F48)
|
||||
? quote.balance.abs()
|
||||
: ZERO_I80F48;
|
||||
zeroAmount = base.balance
|
||||
.max(quoteBorrows)
|
||||
.add(
|
||||
initialHealth.div(
|
||||
base
|
||||
.liabWeight(HealthType.init)
|
||||
.sub(quote.assetWeight(HealthType.init)),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
const baseBorrows = base.balance.lt(ZERO_I80F48)
|
||||
? base.balance.abs()
|
||||
: ZERO_I80F48;
|
||||
zeroAmount = quote.balance
|
||||
.max(baseBorrows)
|
||||
.add(
|
||||
initialHealth.div(
|
||||
quote
|
||||
.liabWeight(HealthType.init)
|
||||
.sub(base.assetWeight(HealthType.init)),
|
||||
),
|
||||
);
|
||||
}
|
||||
const cache = cacheAfterPlacingOrder(zeroAmount);
|
||||
const zeroAmountRatio = cache.healthRatio(HealthType.init);
|
||||
|
||||
function cacheAfterPlacingOrder(amount: I80F48) {
|
||||
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
|
||||
|
||||
side === Serum3Side.ask
|
||||
? (adjustedCache.tokenInfos[baseIndex].balance =
|
||||
adjustedCache.tokenInfos[baseIndex].balance.sub(amount))
|
||||
: (adjustedCache.tokenInfos[quoteIndex].balance =
|
||||
adjustedCache.tokenInfos[quoteIndex].balance.sub(amount));
|
||||
|
||||
adjustedCache.adjustSerum3Reserved(
|
||||
serum3Market.marketIndex,
|
||||
serum3Market.baseTokenIndex,
|
||||
side === Serum3Side.ask ? amount.div(base.oraclePrice) : ZERO_I80F48,
|
||||
ZERO_I80F48,
|
||||
serum3Market.quoteTokenIndex,
|
||||
side === Serum3Side.bid ? amount.div(quote.oraclePrice) : ZERO_I80F48,
|
||||
ZERO_I80F48,
|
||||
);
|
||||
|
||||
return adjustedCache;
|
||||
}
|
||||
|
||||
function healthRatioAfterPlacingOrder(amount: I80F48): I80F48 {
|
||||
return cacheAfterPlacingOrder(amount).healthRatio(HealthType.init);
|
||||
}
|
||||
|
||||
const amount = HealthCache.binaryApproximationSearch(
|
||||
initialAmount,
|
||||
initialRatio,
|
||||
zeroAmount,
|
||||
zeroAmountRatio,
|
||||
minRatio,
|
||||
healthRatioAfterPlacingOrder,
|
||||
);
|
||||
|
||||
// If its a bid then the reserved fund and potential loan is in quote,
|
||||
// If its a ask then the reserved fund and potential loan is in base,
|
||||
// also keep some buffer for fees, use taker fees for worst case simulation.
|
||||
return side === Serum3Side.bid
|
||||
? amount
|
||||
.div(quote.oraclePrice)
|
||||
.div(ONE_I80F48.add(baseBank.loanOriginationFeeRate))
|
||||
.div(ONE_I80F48.add(I80F48.fromNumber(group.getFeeRate(false))))
|
||||
: amount
|
||||
.div(base.oraclePrice)
|
||||
.div(ONE_I80F48.add(quoteBank.loanOriginationFeeRate))
|
||||
.div(ONE_I80F48.add(I80F48.fromNumber(group.getFeeRate(false))));
|
||||
}
|
||||
}
|
||||
|
||||
export class TokenInfo {
|
||||
|
@ -468,20 +695,30 @@ export class TokenInfo {
|
|||
}
|
||||
|
||||
toString() {
|
||||
return ` tokenIndex: ${this.tokenIndex}, balance: ${this.balance}`;
|
||||
return ` tokenIndex: ${this.tokenIndex}, balance: ${
|
||||
this.balance
|
||||
}, serum3MaxReserved: ${
|
||||
this.serum3MaxReserved
|
||||
}, initHealth ${this.healthContribution(HealthType.init)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class Serum3Info {
|
||||
constructor(dto: Serum3InfoDto) {
|
||||
this.reserved = I80F48.from(dto.reserved);
|
||||
this.baseIndex = dto.baseIndex;
|
||||
this.quoteIndex = dto.quoteIndex;
|
||||
}
|
||||
constructor(
|
||||
public reserved: I80F48,
|
||||
public baseIndex: number,
|
||||
public quoteIndex: number,
|
||||
public marketIndex: number,
|
||||
) {}
|
||||
|
||||
reserved: I80F48;
|
||||
baseIndex: number;
|
||||
quoteIndex: number;
|
||||
static fromDto(dto: Serum3InfoDto) {
|
||||
return new Serum3Info(
|
||||
I80F48.from(dto.reserved),
|
||||
dto.baseIndex,
|
||||
dto.quoteIndex,
|
||||
dto.marketIndex,
|
||||
);
|
||||
}
|
||||
|
||||
healthContribution(healthType: HealthType, tokenInfos: TokenInfo[]): I80F48 {
|
||||
const baseInfo = tokenInfos[this.baseIndex];
|
||||
|
@ -512,7 +749,6 @@ export class Serum3Info {
|
|||
assetPart = maxBalance;
|
||||
liabPart = reserved.sub(maxBalance);
|
||||
}
|
||||
|
||||
const assetWeight = tokenInfo.assetWeight(healthType);
|
||||
const liabWeight = tokenInfo.liabWeight(healthType);
|
||||
return assetWeight.mul(assetPart).add(liabWeight.mul(liabPart));
|
||||
|
@ -522,6 +758,14 @@ export class Serum3Info {
|
|||
const reservedAsQuote = computeHealthEffect(quoteInfo);
|
||||
return reservedAsBase.min(reservedAsQuote);
|
||||
}
|
||||
|
||||
toString(tokenInfos: TokenInfo[]) {
|
||||
return ` marketIndex: ${this.marketIndex}, baseIndex: ${
|
||||
this.baseIndex
|
||||
}, quoteIndex: ${this.quoteIndex}, reserved: ${
|
||||
this.reserved
|
||||
}, initHealth ${this.healthContribution(HealthType.init, tokenInfos)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class PerpInfo {
|
||||
|
@ -578,12 +822,39 @@ export class TokenInfoDto {
|
|||
balance: I80F48Dto;
|
||||
// in health-reference-token native units
|
||||
serum3MaxReserved: I80F48Dto;
|
||||
|
||||
constructor(
|
||||
tokenIndex: number,
|
||||
maintAssetWeight: I80F48Dto,
|
||||
initAssetWeight: I80F48Dto,
|
||||
maintLiabWeight: I80F48Dto,
|
||||
initLiabWeight: I80F48Dto,
|
||||
oraclePrice: I80F48Dto,
|
||||
balance: I80F48Dto,
|
||||
serum3MaxReserved: I80F48Dto,
|
||||
) {
|
||||
this.tokenIndex = tokenIndex;
|
||||
this.maintAssetWeight = maintAssetWeight;
|
||||
this.initAssetWeight = initAssetWeight;
|
||||
this.maintLiabWeight = maintLiabWeight;
|
||||
this.initLiabWeight = initLiabWeight;
|
||||
this.oraclePrice = oraclePrice;
|
||||
this.balance = balance;
|
||||
this.serum3MaxReserved = serum3MaxReserved;
|
||||
}
|
||||
}
|
||||
|
||||
export class Serum3InfoDto {
|
||||
reserved: I80F48Dto;
|
||||
baseIndex: number;
|
||||
quoteIndex: number;
|
||||
marketIndex: number;
|
||||
|
||||
constructor(reserved: I80F48Dto, baseIndex: number, quoteIndex: number) {
|
||||
this.reserved = reserved;
|
||||
this.baseIndex = baseIndex;
|
||||
this.quoteIndex = quoteIndex;
|
||||
}
|
||||
}
|
||||
|
||||
export class PerpInfoDto {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { BN } from '@project-serum/anchor';
|
||||
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
|
||||
import { Order, Orderbook } from '@project-serum/serum/lib/market';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import { MangoClient } from '../client';
|
||||
import { nativeI80F48ToUi, toNative, toUiDecimals } from '../utils';
|
||||
|
@ -7,6 +8,7 @@ import { Bank } from './bank';
|
|||
import { Group } from './group';
|
||||
import { HealthCache, HealthCacheDto } from './healthCache';
|
||||
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48';
|
||||
import { Serum3Market, Serum3Side } from './serum3';
|
||||
export class MangoAccount {
|
||||
public tokens: TokenPosition[];
|
||||
public serum3: Serum3Orders[];
|
||||
|
@ -89,6 +91,18 @@ export class MangoAccount {
|
|||
return this;
|
||||
}
|
||||
|
||||
tokensActive(): TokenPosition[] {
|
||||
return this.tokens.filter((token) => token.isActive());
|
||||
}
|
||||
|
||||
serum3Active(): Serum3Orders[] {
|
||||
return this.serum3.filter((serum3) => serum3.isActive());
|
||||
}
|
||||
|
||||
perpActive(): PerpPosition[] {
|
||||
return this.perps.filter((perp) => perp.isActive());
|
||||
}
|
||||
|
||||
findToken(tokenIndex: number): TokenPosition | undefined {
|
||||
return this.tokens.find((ta) => ta.tokenIndex == tokenIndex);
|
||||
}
|
||||
|
@ -406,21 +420,184 @@ export class MangoAccount {
|
|||
.toNumber();
|
||||
}
|
||||
|
||||
public async loadSerum3OpenOrdersForMarket(
|
||||
client: MangoClient,
|
||||
group: Group,
|
||||
externalMarketPk: PublicKey,
|
||||
): Promise<Order[]> {
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
);
|
||||
const serum3OO = this.serum3Active().find(
|
||||
(s) => s.marketIndex === serum3Market.marketIndex,
|
||||
);
|
||||
if (!serum3OO) {
|
||||
throw new Error(`No open orders account found for ${externalMarketPk}`);
|
||||
}
|
||||
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
const [bidsInfo, asksInfo] =
|
||||
await client.program.provider.connection.getMultipleAccountsInfo([
|
||||
serum3MarketExternal.bidsAddress,
|
||||
serum3MarketExternal.asksAddress,
|
||||
]);
|
||||
const bids = Orderbook.decode(serum3MarketExternal, bidsInfo.data);
|
||||
const asks = Orderbook.decode(serum3MarketExternal, asksInfo.data);
|
||||
return [...bids, ...asks].filter((o) =>
|
||||
o.openOrdersAddress.equals(serum3OO.openOrders),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The remaining native quote margin available for given market.
|
||||
*
|
||||
* TODO: this is a very bad estimation atm.
|
||||
* It assumes quote asset is always quote,
|
||||
* it assumes that there are no interaction effects,
|
||||
* it assumes that there are no existing borrows for either of the tokens in the market.
|
||||
* @param group
|
||||
* @param serum3Market
|
||||
* @returns maximum native quote which can be traded for base token given current health
|
||||
*/
|
||||
getSerum3MarketMarginAvailable(group: Group, marketName: string): I80F48 {
|
||||
const initHealth = (this.accountData as MangoAccountData).initHealth;
|
||||
const serum3Market = group.serum3MarketsMap.get(marketName)!;
|
||||
const marketAssetWeight = group.getFirstBankByTokenIndex(
|
||||
serum3Market.baseTokenIndex,
|
||||
).initAssetWeight;
|
||||
return initHealth.div(ONE_I80F48.sub(marketAssetWeight));
|
||||
public getMaxQuoteForSerum3Bid(
|
||||
group: Group,
|
||||
serum3Market: Serum3Market,
|
||||
): I80F48 {
|
||||
return this.accountData.healthCache.getMaxForSerum3Order(
|
||||
group,
|
||||
serum3Market,
|
||||
Serum3Side.bid,
|
||||
I80F48.fromNumber(3),
|
||||
);
|
||||
}
|
||||
|
||||
public getMaxQuoteForSerum3BidUi(
|
||||
group: Group,
|
||||
externalMarketPk: PublicKey,
|
||||
): number {
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
);
|
||||
const nativeAmount = this.getMaxQuoteForSerum3Bid(group, serum3Market);
|
||||
return toUiDecimals(
|
||||
nativeAmount,
|
||||
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex).mintDecimals,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param group
|
||||
* @param serum3Market
|
||||
* @returns maximum native base which can be traded for quote token given current health
|
||||
*/
|
||||
public getMaxBaseForSerum3Ask(
|
||||
group: Group,
|
||||
serum3Market: Serum3Market,
|
||||
): I80F48 {
|
||||
return this.accountData.healthCache.getMaxForSerum3Order(
|
||||
group,
|
||||
serum3Market,
|
||||
Serum3Side.ask,
|
||||
I80F48.fromNumber(3),
|
||||
);
|
||||
}
|
||||
|
||||
public getMaxBaseForSerum3AskUi(
|
||||
group: Group,
|
||||
externalMarketPk: PublicKey,
|
||||
): number {
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
);
|
||||
const nativeAmount = this.getMaxBaseForSerum3Ask(group, serum3Market);
|
||||
return toUiDecimals(
|
||||
nativeAmount,
|
||||
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex).mintDecimals,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param group
|
||||
* @param nativeQuoteAmount
|
||||
* @param serum3Market
|
||||
* @param healthType
|
||||
* @returns health ratio after a bid with nativeQuoteAmount is placed
|
||||
*/
|
||||
simHealthRatioWithSerum3BidChanges(
|
||||
group: Group,
|
||||
nativeQuoteAmount: I80F48,
|
||||
serum3Market: Serum3Market,
|
||||
healthType: HealthType = HealthType.init,
|
||||
): I80F48 {
|
||||
return this.accountData.healthCache.simHealthRatioWithSerum3BidChanges(
|
||||
group,
|
||||
nativeQuoteAmount,
|
||||
serum3Market,
|
||||
healthType,
|
||||
);
|
||||
}
|
||||
|
||||
simHealthRatioWithSerum3BidUiChanges(
|
||||
group: Group,
|
||||
uiQuoteAmount: number,
|
||||
externalMarketPk: PublicKey,
|
||||
healthType: HealthType = HealthType.init,
|
||||
): number {
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
);
|
||||
return this.simHealthRatioWithSerum3BidChanges(
|
||||
group,
|
||||
toNative(
|
||||
uiQuoteAmount,
|
||||
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex)
|
||||
.mintDecimals,
|
||||
),
|
||||
serum3Market,
|
||||
healthType,
|
||||
).toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param group
|
||||
* @param nativeBaseAmount
|
||||
* @param serum3Market
|
||||
* @param healthType
|
||||
* @returns health ratio after an ask with nativeBaseAmount is placed
|
||||
*/
|
||||
simHealthRatioWithSerum3AskChanges(
|
||||
group: Group,
|
||||
nativeBaseAmount: I80F48,
|
||||
serum3Market: Serum3Market,
|
||||
healthType: HealthType = HealthType.init,
|
||||
): I80F48 {
|
||||
return this.accountData.healthCache.simHealthRatioWithSerum3AskChanges(
|
||||
group,
|
||||
nativeBaseAmount,
|
||||
serum3Market,
|
||||
healthType,
|
||||
);
|
||||
}
|
||||
|
||||
simHealthRatioWithSerum3AskUiChanges(
|
||||
group: Group,
|
||||
uiBaseAmount: number,
|
||||
externalMarketPk: PublicKey,
|
||||
healthType: HealthType = HealthType.init,
|
||||
): number {
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
);
|
||||
return this.simHealthRatioWithSerum3AskChanges(
|
||||
group,
|
||||
toNative(
|
||||
uiBaseAmount,
|
||||
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
|
||||
.mintDecimals,
|
||||
),
|
||||
serum3Market,
|
||||
healthType,
|
||||
).toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -438,18 +615,6 @@ export class MangoAccount {
|
|||
return initHealth.div(ONE_I80F48.sub(marketAssetWeight));
|
||||
}
|
||||
|
||||
tokensActive(): TokenPosition[] {
|
||||
return this.tokens.filter((token) => token.isActive());
|
||||
}
|
||||
|
||||
serum3Active(): Serum3Orders[] {
|
||||
return this.serum3.filter((serum3) => serum3.isActive());
|
||||
}
|
||||
|
||||
perpActive(): PerpPosition[] {
|
||||
return this.perps.filter((perp) => perp.isActive());
|
||||
}
|
||||
|
||||
toString(group?: Group): string {
|
||||
let res = 'MangoAccount';
|
||||
res = res + '\n pk: ' + this.publicKey.toString();
|
||||
|
@ -701,7 +866,7 @@ export class MangoAccountData {
|
|||
tokenAssets: any;
|
||||
}) {
|
||||
return new MangoAccountData(
|
||||
new HealthCache(event.healthCache),
|
||||
HealthCache.fromDto(event.healthCache),
|
||||
I80F48.from(event.initHealth),
|
||||
I80F48.from(event.maintHealth),
|
||||
Equity.from(event.equity),
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import { Market, Orderbook } from '@project-serum/serum/lib/market';
|
||||
import { Cluster, PublicKey } from '@solana/web3.js';
|
||||
import BN from 'bn.js';
|
||||
import { MangoClient } from '../client';
|
||||
import { SERUM3_PROGRAM_ID } from '../constants';
|
||||
import { Group } from './group';
|
||||
|
||||
export class Serum3Market {
|
||||
public name: string;
|
||||
|
@ -44,6 +48,43 @@ export class Serum3Market {
|
|||
) {
|
||||
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
||||
}
|
||||
|
||||
async loadBids(client: MangoClient, group: Group): Promise<Orderbook> {
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
this.serumMarketExternal.toBase58(),
|
||||
);
|
||||
return await serum3MarketExternal.loadBids(
|
||||
client.program.provider.connection,
|
||||
);
|
||||
}
|
||||
|
||||
async loadAsks(client: MangoClient, group: Group): Promise<Orderbook> {
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
this.serumMarketExternal.toBase58(),
|
||||
);
|
||||
return await serum3MarketExternal.loadAsks(
|
||||
client.program.provider.connection,
|
||||
);
|
||||
}
|
||||
|
||||
async logOb(client: MangoClient, group: Group): Promise<string> {
|
||||
let res = ``;
|
||||
res += ` ${this.name} OrderBook`;
|
||||
let orders = await this?.loadAsks(client, group);
|
||||
for (const order of orders!.items(true)) {
|
||||
res += `\n ${order.price.toString().padStart(10)}, ${order.size
|
||||
.toString()
|
||||
.padStart(10)}`;
|
||||
}
|
||||
res += `\n --------------------------`;
|
||||
orders = await this?.loadBids(client, group);
|
||||
for (const order of orders!.items(true)) {
|
||||
res += `\n ${order.price.toString().padStart(10)}, ${order.size
|
||||
.toString()
|
||||
.padStart(10)}`;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export class Serum3SelfTradeBehavior {
|
||||
|
@ -62,3 +103,21 @@ export class Serum3Side {
|
|||
static bid = { bid: {} };
|
||||
static ask = { ask: {} };
|
||||
}
|
||||
|
||||
export async function generateSerum3MarketExternalVaultSignerAddress(
|
||||
cluster: Cluster,
|
||||
serum3Market: Serum3Market,
|
||||
serum3MarketExternal: Market,
|
||||
): Promise<PublicKey> {
|
||||
return await PublicKey.createProgramAddress(
|
||||
[
|
||||
serum3Market.serumMarketExternal.toBuffer(),
|
||||
serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike(
|
||||
Buffer,
|
||||
'le',
|
||||
8,
|
||||
),
|
||||
],
|
||||
SERUM3_PROGRAM_ID[cluster],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { AnchorProvider, BN, Program, Provider } from '@project-serum/anchor';
|
||||
import { getFeeRates, getFeeTier } from '@project-serum/serum';
|
||||
import { Order } from '@project-serum/serum/lib/market';
|
||||
import {
|
||||
closeAccount,
|
||||
initializeAccount,
|
||||
|
@ -37,6 +35,7 @@ import {
|
|||
import { StubOracle } from './accounts/oracle';
|
||||
import { OrderType, PerpMarket, Side } from './accounts/perp';
|
||||
import {
|
||||
generateSerum3MarketExternalVaultSignerAddress,
|
||||
Serum3Market,
|
||||
Serum3OrderType,
|
||||
Serum3SelfTradeBehavior,
|
||||
|
@ -678,9 +677,13 @@ export class MangoClient {
|
|||
mangoAccount: MangoAccount,
|
||||
): Promise<MangoAccountData> {
|
||||
const healthRemainingAccounts: PublicKey[] =
|
||||
this.buildHealthRemainingAccounts(AccountRetriever.Fixed, group, [
|
||||
mangoAccount,
|
||||
]);
|
||||
this.buildHealthRemainingAccounts(
|
||||
AccountRetriever.Fixed,
|
||||
group,
|
||||
[mangoAccount],
|
||||
[],
|
||||
[],
|
||||
);
|
||||
|
||||
// Use our custom simulate fn in utils/anchor.ts so signing the tx is not required
|
||||
this.program.provider.simulate = simulate;
|
||||
|
@ -778,6 +781,7 @@ export class MangoClient {
|
|||
group,
|
||||
[mangoAccount],
|
||||
[bank],
|
||||
[],
|
||||
);
|
||||
|
||||
const transaction = await this.program.methods
|
||||
|
@ -852,6 +856,7 @@ export class MangoClient {
|
|||
group,
|
||||
[mangoAccount],
|
||||
[bank],
|
||||
[],
|
||||
);
|
||||
|
||||
const transaction = await this.program.methods
|
||||
|
@ -917,9 +922,11 @@ export class MangoClient {
|
|||
|
||||
public async serum3deregisterMarket(
|
||||
group: Group,
|
||||
serum3MarketName: string,
|
||||
externalMarketPk: PublicKey,
|
||||
): Promise<TransactionSignature> {
|
||||
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
|
||||
return await this.program.methods
|
||||
.serum3DeregisterMarket()
|
||||
|
@ -981,7 +988,8 @@ export class MangoClient {
|
|||
mangoAccount: MangoAccount,
|
||||
marketName: string,
|
||||
): Promise<TransactionSignature> {
|
||||
const serum3Market: Serum3Market = group.serum3MarketsMap.get(marketName)!;
|
||||
const serum3Market: Serum3Market =
|
||||
group.serum3MarketsMapByExternal.get(marketName)!;
|
||||
|
||||
return await this.program.methods
|
||||
.serum3CreateOpenOrders()
|
||||
|
@ -1000,9 +1008,11 @@ export class MangoClient {
|
|||
public async serum3CloseOpenOrders(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
serum3MarketName: string,
|
||||
externalMarketPk: PublicKey,
|
||||
): Promise<TransactionSignature> {
|
||||
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
|
||||
const openOrders = mangoAccount.serum3.find(
|
||||
(account) => account.marketIndex === serum3Market.marketIndex,
|
||||
|
@ -1026,7 +1036,7 @@ export class MangoClient {
|
|||
public async serum3PlaceOrder(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
serum3MarketName: string,
|
||||
externalMarketPk: PublicKey,
|
||||
side: Serum3Side,
|
||||
price: number,
|
||||
size: number,
|
||||
|
@ -1035,46 +1045,41 @@ export class MangoClient {
|
|||
clientOrderId: number,
|
||||
limit: number,
|
||||
) {
|
||||
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
|
||||
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
if (!mangoAccount.findSerum3Account(serum3Market.marketIndex)) {
|
||||
await this.serum3CreateOpenOrders(group, mangoAccount, 'BTC/USDC');
|
||||
mangoAccount = await this.getMangoAccount(mangoAccount);
|
||||
}
|
||||
|
||||
const serum3MarketExternal =
|
||||
group.serum3MarketExternalsMap.get(serum3MarketName)!;
|
||||
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
const serum3MarketExternalVaultSigner =
|
||||
await PublicKey.createProgramAddress(
|
||||
[
|
||||
serum3Market.serumMarketExternal.toBuffer(),
|
||||
serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike(
|
||||
Buffer,
|
||||
'le',
|
||||
8,
|
||||
),
|
||||
],
|
||||
SERUM3_PROGRAM_ID[this.cluster],
|
||||
await generateSerum3MarketExternalVaultSignerAddress(
|
||||
this.cluster,
|
||||
serum3Market,
|
||||
serum3MarketExternal,
|
||||
);
|
||||
|
||||
const healthRemainingAccounts: PublicKey[] =
|
||||
this.buildHealthRemainingAccounts(AccountRetriever.Fixed, group, [
|
||||
mangoAccount,
|
||||
]);
|
||||
this.buildHealthRemainingAccounts(
|
||||
AccountRetriever.Fixed,
|
||||
group,
|
||||
[mangoAccount],
|
||||
[],
|
||||
[],
|
||||
);
|
||||
|
||||
const limitPrice = serum3MarketExternal.priceNumberToLots(price);
|
||||
const maxBaseQuantity = serum3MarketExternal.baseSizeNumberToLots(size);
|
||||
const feeTier = getFeeTier(0, 0 /** TODO: fix msrm/srm balance */);
|
||||
const rates = getFeeRates(feeTier);
|
||||
const maxQuoteQuantity = new BN(
|
||||
serum3MarketExternal.decoded.quoteLotSize.toNumber() *
|
||||
(1 + rates.taker) /** TODO: fix taker/maker */,
|
||||
).mul(
|
||||
serum3MarketExternal
|
||||
.baseSizeNumberToLots(size)
|
||||
.mul(serum3MarketExternal.priceNumberToLots(price)),
|
||||
);
|
||||
const maxQuoteQuantity = serum3MarketExternal.decoded.quoteLotSize
|
||||
.mul(new BN(1 + group.getFeeRate(orderType === Serum3OrderType.postOnly)))
|
||||
.mul(
|
||||
serum3MarketExternal
|
||||
.baseSizeNumberToLots(size)
|
||||
.mul(serum3MarketExternal.priceNumberToLots(price)),
|
||||
);
|
||||
const payerTokenIndex = (() => {
|
||||
if (side == Serum3Side.bid) {
|
||||
return serum3Market.quoteTokenIndex;
|
||||
|
@ -1125,13 +1130,16 @@ export class MangoClient {
|
|||
async serum3CancelAllorders(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
serum3MarketName: string,
|
||||
externalMarketPk: PublicKey,
|
||||
limit: number,
|
||||
) {
|
||||
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
|
||||
const serum3MarketExternal =
|
||||
group.serum3MarketExternalsMap.get(serum3MarketName)!;
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
|
||||
return await this.program.methods
|
||||
.serum3CancelAllOrders(limit)
|
||||
|
@ -1154,25 +1162,19 @@ export class MangoClient {
|
|||
async serum3SettleFunds(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
serum3MarketName: string,
|
||||
externalMarketPk: PublicKey,
|
||||
): Promise<TransactionSignature> {
|
||||
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
|
||||
|
||||
const serum3MarketExternal =
|
||||
group.serum3MarketExternalsMap.get(serum3MarketName)!;
|
||||
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
const serum3MarketExternalVaultSigner =
|
||||
// TODO: put into a helper method, and remove copy pasta
|
||||
await PublicKey.createProgramAddress(
|
||||
[
|
||||
serum3Market.serumMarketExternal.toBuffer(),
|
||||
serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike(
|
||||
Buffer,
|
||||
'le',
|
||||
8,
|
||||
),
|
||||
],
|
||||
SERUM3_PROGRAM_ID[this.cluster],
|
||||
await generateSerum3MarketExternalVaultSignerAddress(
|
||||
this.cluster,
|
||||
serum3Market,
|
||||
serum3MarketExternal,
|
||||
);
|
||||
|
||||
return await this.program.methods
|
||||
|
@ -1204,14 +1206,17 @@ export class MangoClient {
|
|||
async serum3CancelOrder(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
serum3MarketName: string,
|
||||
externalMarketPk: PublicKey,
|
||||
side: Serum3Side,
|
||||
orderId: BN,
|
||||
): Promise<TransactionSignature> {
|
||||
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
|
||||
const serum3MarketExternal =
|
||||
group.serum3MarketExternalsMap.get(serum3MarketName)!;
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
|
||||
return await this.program.methods
|
||||
.serum3CancelOrder(side, orderId)
|
||||
|
@ -1230,20 +1235,6 @@ export class MangoClient {
|
|||
.rpc();
|
||||
}
|
||||
|
||||
async getSerum3Orders(
|
||||
group: Group,
|
||||
serum3MarketName: string,
|
||||
): Promise<Order[]> {
|
||||
const serum3MarketExternal =
|
||||
group.serum3MarketExternalsMap.get(serum3MarketName)!;
|
||||
|
||||
// TODO: filter for mango account
|
||||
return await serum3MarketExternal.loadOrdersForOwner(
|
||||
this.program.provider.connection,
|
||||
group.publicKey,
|
||||
);
|
||||
}
|
||||
|
||||
/// perps
|
||||
|
||||
async perpCreateMarket(
|
||||
|
@ -1466,9 +1457,13 @@ export class MangoClient {
|
|||
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
|
||||
|
||||
const healthRemainingAccounts: PublicKey[] =
|
||||
this.buildHealthRemainingAccounts(AccountRetriever.Fixed, group, [
|
||||
mangoAccount,
|
||||
]);
|
||||
this.buildHealthRemainingAccounts(
|
||||
AccountRetriever.Fixed,
|
||||
group,
|
||||
[mangoAccount],
|
||||
[],
|
||||
[perpMarket],
|
||||
);
|
||||
|
||||
const [nativePrice, nativeQuantity] = perpMarket.uiToNativePriceQuantity(
|
||||
price,
|
||||
|
@ -1539,6 +1534,7 @@ export class MangoClient {
|
|||
group,
|
||||
[mangoAccount],
|
||||
[inputBank, outputBank],
|
||||
[],
|
||||
);
|
||||
const parsedHealthAccounts = healthRemainingAccounts.map(
|
||||
(pk) =>
|
||||
|
@ -1723,6 +1719,7 @@ export class MangoClient {
|
|||
group,
|
||||
[liqor, liqee],
|
||||
[assetBank, liabBank],
|
||||
[],
|
||||
);
|
||||
|
||||
const parsedHealthAccounts = healthRemainingAccounts.map(
|
||||
|
@ -1798,31 +1795,39 @@ export class MangoClient {
|
|||
|
||||
/// private
|
||||
|
||||
// todo make private
|
||||
public buildHealthRemainingAccounts(
|
||||
retriever: AccountRetriever,
|
||||
group: Group,
|
||||
mangoAccounts: MangoAccount[],
|
||||
banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */,
|
||||
banks: Bank[],
|
||||
perpMarkets: PerpMarket[],
|
||||
): PublicKey[] {
|
||||
if (retriever === AccountRetriever.Fixed) {
|
||||
return this.buildFixedAccountRetrieverHealthAccounts(
|
||||
group,
|
||||
mangoAccounts[0],
|
||||
banks,
|
||||
perpMarkets,
|
||||
);
|
||||
} else {
|
||||
return this.buildScanningAccountRetrieverHealthAccounts(
|
||||
group,
|
||||
mangoAccounts,
|
||||
banks,
|
||||
perpMarkets,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// todo make private
|
||||
public buildFixedAccountRetrieverHealthAccounts(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */,
|
||||
// Banks and perpMarkets for whom positions don't exist on mango account,
|
||||
// but user would potentially open new positions.
|
||||
banks: Bank[],
|
||||
perpMarkets: PerpMarket[],
|
||||
): PublicKey[] {
|
||||
const healthRemainingAccounts: PublicKey[] = [];
|
||||
|
||||
|
@ -1853,11 +1858,7 @@ export class MangoClient {
|
|||
healthRemainingAccounts.push(
|
||||
...mintInfos.map((mintInfo) => mintInfo.oracle),
|
||||
);
|
||||
healthRemainingAccounts.push(
|
||||
...mangoAccount.serum3
|
||||
.filter((serum3Account) => serum3Account.marketIndex !== 65535)
|
||||
.map((serum3Account) => serum3Account.openOrders),
|
||||
);
|
||||
|
||||
healthRemainingAccounts.push(
|
||||
...mangoAccount.perps
|
||||
.filter((perp) => perp.marketIndex !== 65535)
|
||||
|
@ -1868,14 +1869,36 @@ export class MangoClient {
|
|||
)[0].publicKey,
|
||||
),
|
||||
);
|
||||
for (const perpMarket of perpMarkets) {
|
||||
const alreadyAdded = mangoAccount.perps.find(
|
||||
(p) => p.marketIndex === perpMarket.perpMarketIndex,
|
||||
);
|
||||
if (!alreadyAdded) {
|
||||
healthRemainingAccounts.push(
|
||||
Array.from(group.perpMarketsMap.values()).filter(
|
||||
(p) => p.perpMarketIndex === perpMarket.perpMarketIndex,
|
||||
)[0].publicKey,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
healthRemainingAccounts.push(
|
||||
...mangoAccount.serum3
|
||||
.filter((serum3Account) => serum3Account.marketIndex !== 65535)
|
||||
.map((serum3Account) => serum3Account.openOrders),
|
||||
);
|
||||
|
||||
// debugHealthAccounts(group, mangoAccount, healthRemainingAccounts);
|
||||
|
||||
return healthRemainingAccounts;
|
||||
}
|
||||
|
||||
// todo make private
|
||||
public buildScanningAccountRetrieverHealthAccounts(
|
||||
group: Group,
|
||||
mangoAccounts: MangoAccount[],
|
||||
banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */,
|
||||
banks: Bank[],
|
||||
perpMarkets: PerpMarket[],
|
||||
): PublicKey[] {
|
||||
const healthRemainingAccounts: PublicKey[] = [];
|
||||
|
||||
|
@ -1903,6 +1926,7 @@ export class MangoClient {
|
|||
healthRemainingAccounts.push(
|
||||
...mintInfos.map((mintInfo) => mintInfo.oracle),
|
||||
);
|
||||
|
||||
for (const mangoAccount of mangoAccounts) {
|
||||
healthRemainingAccounts.push(
|
||||
...mangoAccount.serum3
|
||||
|
@ -1910,6 +1934,7 @@ export class MangoClient {
|
|||
.map((serum3Account) => serum3Account.openOrders),
|
||||
);
|
||||
}
|
||||
|
||||
for (const mangoAccount of mangoAccounts) {
|
||||
healthRemainingAccounts.push(
|
||||
...mangoAccount.perps
|
||||
|
@ -1922,6 +1947,20 @@ export class MangoClient {
|
|||
),
|
||||
);
|
||||
}
|
||||
for (const mangoAccount of mangoAccounts) {
|
||||
for (const perpMarket of perpMarkets) {
|
||||
const alreadyAdded = mangoAccount.perps.find(
|
||||
(p) => p.marketIndex === perpMarket.perpMarketIndex,
|
||||
);
|
||||
if (!alreadyAdded) {
|
||||
healthRemainingAccounts.push(
|
||||
Array.from(group.perpMarketsMap.values()).filter(
|
||||
(p) => p.perpMarketIndex === perpMarket.perpMarketIndex,
|
||||
)[0].publicKey,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return healthRemainingAccounts;
|
||||
}
|
||||
|
|
|
@ -20,10 +20,15 @@ async function debugUser(
|
|||
console.log(
|
||||
'buildFixedAccountRetrieverHealthAccounts ' +
|
||||
client
|
||||
.buildFixedAccountRetrieverHealthAccounts(group, mangoAccount, [
|
||||
group.banksMapByName.get('BTC')[0],
|
||||
group.banksMapByName.get('USDC')[0],
|
||||
])
|
||||
.buildFixedAccountRetrieverHealthAccounts(
|
||||
group,
|
||||
mangoAccount,
|
||||
[
|
||||
group.banksMapByName.get('BTC')[0],
|
||||
group.banksMapByName.get('USDC')[0],
|
||||
],
|
||||
[],
|
||||
)
|
||||
.map((pk) => pk.toBase58())
|
||||
.join(', '),
|
||||
);
|
||||
|
|
|
@ -17,6 +17,8 @@ import { MANGO_V4_ID } from '../constants';
|
|||
const DEVNET_SERUM3_MARKETS = new Map([
|
||||
['BTC/USDC', 'DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB'],
|
||||
['SOL/USDC', '5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR'],
|
||||
['ETH/USDC', 'BkAraCyL9TTLbeMY3L1VWrPcv32DvSi5QDDQjik1J6Ac'],
|
||||
['SRM/USDC', '249LDNPLLL29nRq8kjBTg9hKdXMcZf4vK2UvxszZYcuZ'],
|
||||
]);
|
||||
const DEVNET_MINTS = new Map([
|
||||
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
|
||||
|
@ -24,12 +26,16 @@ const DEVNET_MINTS = new Map([
|
|||
['SOL', 'So11111111111111111111111111111111111111112'],
|
||||
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
|
||||
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
|
||||
['ETH', 'Cu84KB3tDL6SbFgToHMLYVDJJXdJjenNzSKikeAvzmkA'],
|
||||
['SRM', 'AvtB6w9xboLwA145E221vhof5TddhqsChYcx7Fy3xVMH'],
|
||||
]);
|
||||
const DEVNET_ORACLES = new Map([
|
||||
['BTC', 'HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J'],
|
||||
['SOL', 'J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix'],
|
||||
['ORCA', 'A1WttWF7X3Rg6ZRpB2YQUFHCRh1kiXV8sKKLV3S9neJV'],
|
||||
['MNGO', '8k7F9Xb36oFJsjpCKpsXvg4cgBRoZtwNTc3EzG5Ttd2o'],
|
||||
['ETH', 'EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw'],
|
||||
['SRM', '992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs'],
|
||||
]);
|
||||
|
||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
|
||||
|
@ -88,19 +94,19 @@ async function main() {
|
|||
0.1,
|
||||
0, // tokenIndex
|
||||
'USDC',
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
0.9,
|
||||
1.5,
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.0005,
|
||||
0.8,
|
||||
0.6,
|
||||
1.2,
|
||||
1.4,
|
||||
0.02,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
);
|
||||
await group.reloadAll(client);
|
||||
} catch (error) {}
|
||||
|
@ -117,19 +123,19 @@ async function main() {
|
|||
0.1,
|
||||
1, // tokenIndex
|
||||
'BTC',
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.9,
|
||||
0.88,
|
||||
0.0005,
|
||||
0.0005,
|
||||
0.8,
|
||||
0.6,
|
||||
1.1,
|
||||
1.2,
|
||||
1.4,
|
||||
0.02,
|
||||
0.05,
|
||||
);
|
||||
await group.reloadAll(client);
|
||||
} catch (error) {
|
||||
|
@ -148,19 +154,19 @@ async function main() {
|
|||
0.1,
|
||||
2, // tokenIndex
|
||||
'SOL',
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.9,
|
||||
0.63,
|
||||
0.0005,
|
||||
0.0005,
|
||||
0.8,
|
||||
0.6,
|
||||
1.1,
|
||||
1.2,
|
||||
1.4,
|
||||
0.02,
|
||||
0.05,
|
||||
);
|
||||
await group.reloadAll(client);
|
||||
} catch (error) {
|
||||
|
@ -198,14 +204,75 @@ async function main() {
|
|||
console.log(error);
|
||||
}
|
||||
|
||||
// register token 4
|
||||
// register token 7
|
||||
console.log(`Registering ETH...`);
|
||||
const ethDevnetMint = new PublicKey(DEVNET_MINTS.get('ETH')!);
|
||||
const ethDevnetOracle = new PublicKey(DEVNET_ORACLES.get('ETH')!);
|
||||
try {
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
ethDevnetMint,
|
||||
ethDevnetOracle,
|
||||
0.1,
|
||||
7, // tokenIndex
|
||||
'ETH',
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.9,
|
||||
0.8,
|
||||
1.1,
|
||||
1.2,
|
||||
0.05,
|
||||
);
|
||||
await group.reloadAll(client);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
// register token 5
|
||||
console.log(`Registering SRM...`);
|
||||
const srmDevnetMint = new PublicKey(DEVNET_MINTS.get('SRM')!);
|
||||
const srmDevnetOracle = new PublicKey(DEVNET_ORACLES.get('SRM')!);
|
||||
try {
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
srmDevnetMint,
|
||||
srmDevnetOracle,
|
||||
0.1,
|
||||
5, // tokenIndex
|
||||
'SRM',
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.9,
|
||||
0.8,
|
||||
1.1,
|
||||
1.2,
|
||||
0.05,
|
||||
);
|
||||
await group.reloadAll(client);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Editing group, setting existing admin as fastListingAdmin to be able to add MNGO truslessly...`,
|
||||
);
|
||||
let sig = await client.groupEdit(
|
||||
group,
|
||||
group.admin,
|
||||
new PublicKey('Efhak3qj3MiyzgJr3cUUqXXz5wr3oYHt9sPzuqJf9eBN'),
|
||||
group.admin,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
@ -231,7 +298,7 @@ async function main() {
|
|||
|
||||
// register serum market
|
||||
console.log(`Registering serum3 market...`);
|
||||
const serumMarketExternalPk = new PublicKey(
|
||||
let serumMarketExternalPk = new PublicKey(
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
);
|
||||
try {
|
||||
|
@ -251,7 +318,35 @@ async function main() {
|
|||
group.getFirstBankByMint(btcDevnetMint).tokenIndex,
|
||||
group.getFirstBankByMint(usdcDevnetMint).tokenIndex,
|
||||
);
|
||||
console.log(`...registerd serum3 market ${markets[0].publicKey}`);
|
||||
console.log(`...registered serum3 market ${markets[0].publicKey}`);
|
||||
|
||||
serumMarketExternalPk = new PublicKey(DEVNET_SERUM3_MARKETS.get('ETH/USDC')!);
|
||||
try {
|
||||
await client.serum3RegisterMarket(
|
||||
group,
|
||||
serumMarketExternalPk,
|
||||
group.getFirstBankByMint(ethDevnetMint),
|
||||
group.getFirstBankByMint(usdcDevnetMint),
|
||||
0,
|
||||
'ETH/USDC',
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
serumMarketExternalPk = new PublicKey(DEVNET_SERUM3_MARKETS.get('SRM/USDC')!);
|
||||
try {
|
||||
await client.serum3RegisterMarket(
|
||||
group,
|
||||
serumMarketExternalPk,
|
||||
group.getFirstBankByMint(srmDevnetMint),
|
||||
group.getFirstBankByMint(usdcDevnetMint),
|
||||
0,
|
||||
'SRM/USDC',
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
// register perp market
|
||||
console.log(`Registering perp market...`);
|
||||
|
@ -292,114 +387,93 @@ async function main() {
|
|||
// edit
|
||||
//
|
||||
|
||||
console.log(`Editing USDC...`);
|
||||
try {
|
||||
let sig = await client.tokenEdit(
|
||||
group,
|
||||
usdcDevnetMint,
|
||||
btcDevnetOracle,
|
||||
0.1,
|
||||
undefined,
|
||||
0.01,
|
||||
0.3,
|
||||
0.08,
|
||||
0.81,
|
||||
0.91,
|
||||
0.75,
|
||||
0.0007,
|
||||
1.7,
|
||||
0.9,
|
||||
0.7,
|
||||
1.3,
|
||||
1.5,
|
||||
0.04,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.getFirstBankByMint(btcDevnetMint).toString());
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
console.log(`Resetting USDC...`);
|
||||
try {
|
||||
let sig = await client.tokenEdit(
|
||||
group,
|
||||
usdcDevnetMint,
|
||||
usdcDevnetOracle.publicKey,
|
||||
0.1,
|
||||
undefined,
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
0.9,
|
||||
1.5,
|
||||
0.0005,
|
||||
0.0005,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
0.02,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.getFirstBankByMint(usdcDevnetMint).toString());
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
if (true) {
|
||||
console.log(`Editing USDC...`);
|
||||
try {
|
||||
let sig = await client.tokenEdit(
|
||||
group,
|
||||
usdcDevnetMint,
|
||||
usdcDevnetOracle.publicKey,
|
||||
0.1,
|
||||
undefined,
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.getFirstBankByMint(btcDevnetMint).toString());
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log(`Editing perp market...`);
|
||||
try {
|
||||
let sig = await client.perpEditMarket(
|
||||
group,
|
||||
'BTC-PERP',
|
||||
btcDevnetOracle,
|
||||
0.2,
|
||||
0,
|
||||
6,
|
||||
0.9,
|
||||
0.9,
|
||||
1.035,
|
||||
1.06,
|
||||
0.013,
|
||||
0.0003,
|
||||
0.1,
|
||||
0.07,
|
||||
0.07,
|
||||
1001,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.perpMarketsMap.get('BTC-PERP')!.toString());
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
console.log(`Resetting perp market...`);
|
||||
try {
|
||||
let sig = await client.perpEditMarket(
|
||||
group,
|
||||
'BTC-PERP',
|
||||
btcDevnetOracle,
|
||||
0.1,
|
||||
1,
|
||||
6,
|
||||
1,
|
||||
0.95,
|
||||
1.025,
|
||||
1.05,
|
||||
0.012,
|
||||
0.0002,
|
||||
0.0,
|
||||
0.05,
|
||||
0.05,
|
||||
100,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.perpMarketsMap.get('BTC-PERP')!.toString());
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.log(`Editing BTC...`);
|
||||
try {
|
||||
let sig = await client.tokenEdit(
|
||||
group,
|
||||
usdcDevnetMint,
|
||||
usdcDevnetOracle.publicKey,
|
||||
0.1,
|
||||
undefined,
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.9,
|
||||
0.8,
|
||||
1.1,
|
||||
1.2,
|
||||
0.05,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.getFirstBankByMint(btcDevnetMint).toString());
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log(`Editing SOL...`);
|
||||
try {
|
||||
let sig = await client.tokenEdit(
|
||||
group,
|
||||
usdcDevnetMint,
|
||||
usdcDevnetOracle.publicKey,
|
||||
0.1,
|
||||
undefined,
|
||||
0.004,
|
||||
0.7,
|
||||
0.1,
|
||||
0.85,
|
||||
0.2,
|
||||
2.0,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.9,
|
||||
0.8,
|
||||
1.1,
|
||||
1.2,
|
||||
0.05,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.getFirstBankByMint(btcDevnetMint).toString());
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
process.exit();
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { Serum3Side } from '../accounts/serum3';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
// script which shows how to close a mango account cleanly i.e. close all active positions, withdraw all tokens, etc.
|
||||
//
|
||||
|
||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
|
||||
|
||||
// note: either use finalized or expect closing certain things to fail and having to runs scrript multiple times
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
|
||||
// note: see note above
|
||||
// options.commitment = 'finalized';
|
||||
|
||||
const connection = new Connection(
|
||||
'https://mango.devnet.rpcpool.com',
|
||||
options,
|
||||
);
|
||||
|
||||
// user
|
||||
const user = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(fs.readFileSync(process.env.USER2_KEYPAIR!, 'utf-8')),
|
||||
),
|
||||
);
|
||||
const userWallet = new Wallet(user);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
const client = await MangoClient.connect(
|
||||
userProvider,
|
||||
'devnet',
|
||||
MANGO_V4_ID['devnet'],
|
||||
{},
|
||||
'get-program-accounts',
|
||||
);
|
||||
console.log(`User ${userWallet.publicKey.toBase58()}`);
|
||||
|
||||
try {
|
||||
// fetch group
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
|
||||
),
|
||||
);
|
||||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
console.log(`Found group ${group.publicKey.toBase58()}`);
|
||||
|
||||
// fetch account
|
||||
const mangoAccount = (
|
||||
await client.getMangoAccountsForOwner(group, user.publicKey)
|
||||
)[0];
|
||||
console.log(`...found mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString());
|
||||
|
||||
// close mango account serum3 positions, closing might require cancelling orders and settling
|
||||
for (const serum3Account of mangoAccount.serum3Active()) {
|
||||
let orders = await client.getSerum3Orders(
|
||||
group,
|
||||
group.findSerum3Market(serum3Account.marketIndex)!.name,
|
||||
);
|
||||
for (const order of orders) {
|
||||
console.log(
|
||||
` - Order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`,
|
||||
);
|
||||
console.log(` - Cancelling order with ${order.orderId}`);
|
||||
await client.serum3CancelOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
||||
'BTC/USDC',
|
||||
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
|
||||
order.orderId,
|
||||
);
|
||||
}
|
||||
await client.serum3SettleFunds(
|
||||
group,
|
||||
mangoAccount,
|
||||
group.findSerum3Market(serum3Account.marketIndex)!.name,
|
||||
);
|
||||
await client.serum3CloseOpenOrders(
|
||||
group,
|
||||
mangoAccount,
|
||||
group.findSerum3Market(serum3Account.marketIndex)!.name,
|
||||
);
|
||||
}
|
||||
|
||||
// we closed a serum account, this changes the health accounts we are passing in for future ixs
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
// withdraw all tokens
|
||||
for (const token of mangoAccount.tokensActive()) {
|
||||
let native = token.balance(
|
||||
group.getFirstBankByTokenIndex(token.tokenIndex),
|
||||
);
|
||||
|
||||
// to avoid rounding issues
|
||||
if (native.toNumber() < 1) {
|
||||
continue;
|
||||
}
|
||||
let nativeFlooredNumber = Math.floor(native.toNumber());
|
||||
console.log(
|
||||
`withdrawing token ${
|
||||
group.getFirstBankByTokenIndex(token.tokenIndex).name
|
||||
} native amount ${nativeFlooredNumber} `,
|
||||
);
|
||||
|
||||
await client.tokenWithdrawNative(
|
||||
group,
|
||||
mangoAccount,
|
||||
group.getFirstBankByTokenIndex(token.tokenIndex).mint,
|
||||
nativeFlooredNumber - 1 /* see comment in token_withdraw in program */,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
// reload and print current positions
|
||||
await mangoAccount.reload(client, group);
|
||||
console.log(`...mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString());
|
||||
|
||||
// close account
|
||||
console.log(`Close mango account...`);
|
||||
const res = await client.closeMangoAccount(group, mangoAccount);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
main();
|
|
@ -0,0 +1,134 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
// An example for users based on high level api i.e. the client
|
||||
// Create
|
||||
// process.env.USER_KEYPAIR - mango account owner keypair path
|
||||
// process.env.ADMIN_KEYPAIR - group admin keypair path (useful for automatically finding the group)
|
||||
//
|
||||
// This script deposits some tokens, places some serum orders, cancels them, places some perp orders
|
||||
//
|
||||
|
||||
const DEVNET_MINTS = new Map([
|
||||
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
|
||||
['BTC', '3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU'],
|
||||
['SOL', 'So11111111111111111111111111111111111111112'],
|
||||
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
|
||||
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
|
||||
['ETH', 'Cu84KB3tDL6SbFgToHMLYVDJJXdJjenNzSKikeAvzmkA'],
|
||||
['SRM', 'AvtB6w9xboLwA145E221vhof5TddhqsChYcx7Fy3xVMH'],
|
||||
]);
|
||||
const DEVNET_ORACLES = new Map([
|
||||
['BTC', 'HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J'],
|
||||
['SOL', 'J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix'],
|
||||
['ORCA', 'A1WttWF7X3Rg6ZRpB2YQUFHCRh1kiXV8sKKLV3S9neJV'],
|
||||
['MNGO', '8k7F9Xb36oFJsjpCKpsXvg4cgBRoZtwNTc3EzG5Ttd2o'],
|
||||
['ETH', 'EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw'],
|
||||
['SRM', '992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs'],
|
||||
]);
|
||||
export const DEVNET_SERUM3_MARKETS = new Map([
|
||||
['BTC/USDC', new PublicKey('DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB')],
|
||||
['SOL/USDC', new PublicKey('5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR')],
|
||||
]);
|
||||
|
||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
|
||||
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(
|
||||
'https://mango.devnet.rpcpool.com',
|
||||
options,
|
||||
);
|
||||
|
||||
const user = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(fs.readFileSync(process.env.USER2_KEYPAIR!, 'utf-8')),
|
||||
),
|
||||
);
|
||||
const userWallet = new Wallet(user);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
const client = await MangoClient.connect(
|
||||
userProvider,
|
||||
'devnet',
|
||||
MANGO_V4_ID['devnet'],
|
||||
{},
|
||||
'get-program-accounts',
|
||||
);
|
||||
console.log(`User ${userWallet.publicKey.toBase58()}`);
|
||||
|
||||
// fetch group
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
|
||||
),
|
||||
);
|
||||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
console.log(`${group}`);
|
||||
|
||||
// create + fetch account
|
||||
console.log(`Creating mangoaccount...`);
|
||||
const mangoAccount = await client.getOrCreateMangoAccount(
|
||||
group,
|
||||
user.publicKey,
|
||||
);
|
||||
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString(group));
|
||||
|
||||
if (true) {
|
||||
// deposit and withdraw
|
||||
|
||||
try {
|
||||
console.log(`...depositing`);
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
mangoAccount,
|
||||
new PublicKey(DEVNET_MINTS.get('USDC')!),
|
||||
1000,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
mangoAccount,
|
||||
new PublicKey(DEVNET_MINTS.get('MNGO')!),
|
||||
100,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
mangoAccount,
|
||||
new PublicKey(DEVNET_MINTS.get('ETH')!),
|
||||
500,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
mangoAccount,
|
||||
new PublicKey(DEVNET_MINTS.get('SRM')!),
|
||||
500,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
mangoAccount,
|
||||
new PublicKey(DEVNET_MINTS.get('BTC')!),
|
||||
1,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(mangoAccount.toString(group));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
process.exit();
|
||||
}
|
||||
|
||||
main();
|
|
@ -94,7 +94,7 @@ async function main() {
|
|||
|
||||
// withdraw all tokens
|
||||
for (const token of mangoAccount.tokensActive()) {
|
||||
let native = token.native(
|
||||
let native = token.balance(
|
||||
group.getFirstBankByTokenIndex(token.tokenIndex),
|
||||
);
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { I80F48 } from '../accounts/I80F48';
|
||||
import { HealthType } from '../accounts/mangoAccount';
|
||||
import { OrderType, Side } from '../accounts/perp';
|
||||
import {
|
||||
|
@ -28,6 +29,10 @@ const DEVNET_MINTS = new Map([
|
|||
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
|
||||
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
|
||||
]);
|
||||
export const DEVNET_SERUM3_MARKETS = new Map([
|
||||
['BTC/USDC', new PublicKey('DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB')],
|
||||
['SOL/USDC', new PublicKey('5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR')],
|
||||
]);
|
||||
|
||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
|
||||
|
||||
|
@ -61,7 +66,7 @@ async function main() {
|
|||
),
|
||||
);
|
||||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
console.log(group.toString());
|
||||
console.log(`${group}`);
|
||||
|
||||
// create + fetch account
|
||||
console.log(`Creating mangoaccount...`);
|
||||
|
@ -70,9 +75,11 @@ async function main() {
|
|||
user.publicKey,
|
||||
);
|
||||
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString());
|
||||
console.log(mangoAccount.toString(group));
|
||||
|
||||
if (true) {
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
if (false) {
|
||||
// set delegate, and change name
|
||||
console.log(`...changing mango account name, and setting a delegate`);
|
||||
const randomKey = new PublicKey(
|
||||
|
@ -99,7 +106,8 @@ async function main() {
|
|||
console.log(mangoAccount.toString());
|
||||
}
|
||||
|
||||
if (true) {
|
||||
if (false) {
|
||||
// expand account
|
||||
console.log(
|
||||
`...expanding mango account to have serum3 and perp position slots`,
|
||||
);
|
||||
|
@ -107,11 +115,11 @@ async function main() {
|
|||
await mangoAccount.reload(client, group);
|
||||
}
|
||||
|
||||
if (true) {
|
||||
if (false) {
|
||||
// deposit and withdraw
|
||||
|
||||
try {
|
||||
console.log(`...depositing 50 USDC`);
|
||||
console.log(`...depositing 50 USDC, 1 SOL, 1 MNGO`);
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
@ -120,6 +128,22 @@ async function main() {
|
|||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
mangoAccount,
|
||||
new PublicKey(DEVNET_MINTS.get('SOL')!),
|
||||
1,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
mangoAccount,
|
||||
new PublicKey(DEVNET_MINTS.get('MNGO')!),
|
||||
1,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(`...withdrawing 1 USDC`);
|
||||
await client.tokenWithdraw(
|
||||
group,
|
||||
|
@ -138,21 +162,44 @@ async function main() {
|
|||
0.0005,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(mangoAccount.toString(group));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (false) {
|
||||
// serum3
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')?.toBase58()!,
|
||||
);
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')?.toBase58()!,
|
||||
);
|
||||
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];
|
||||
|
||||
let price = 20;
|
||||
let qty = 0.0001;
|
||||
console.log(
|
||||
`...placing serum3 bid which would not be settled since its relatively low then midprice`,
|
||||
`...placing serum3 bid which would not be settled since its relatively low then midprice at ${price} for ${qty}`,
|
||||
);
|
||||
await client.serum3PlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
'BTC/USDC',
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
Serum3Side.bid,
|
||||
20,
|
||||
0.0001,
|
||||
price,
|
||||
qty,
|
||||
Serum3SelfTradeBehavior.decrementTake,
|
||||
Serum3OrderType.limit,
|
||||
Date.now(),
|
||||
|
@ -160,15 +207,18 @@ async function main() {
|
|||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(`...placing serum3 bid way above midprice`);
|
||||
price = lowestAsk.price + lowestAsk.price / 2;
|
||||
qty = 0.0001;
|
||||
console.log(
|
||||
`...placing serum3 bid way above midprice at ${price} for ${qty}`,
|
||||
);
|
||||
await client.serum3PlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
||||
'BTC/USDC',
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
Serum3Side.bid,
|
||||
90000,
|
||||
0.0001,
|
||||
price,
|
||||
qty,
|
||||
Serum3SelfTradeBehavior.decrementTake,
|
||||
Serum3OrderType.limit,
|
||||
Date.now(),
|
||||
|
@ -176,15 +226,18 @@ async function main() {
|
|||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(`...placing serum3 ask way below midprice`);
|
||||
price = highestBid.price - highestBid.price / 2;
|
||||
qty = 0.0001;
|
||||
console.log(
|
||||
`...placing serum3 ask way below midprice at ${price} for ${qty}`,
|
||||
);
|
||||
await client.serum3PlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
||||
'BTC/USDC',
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
Serum3Side.ask,
|
||||
30000,
|
||||
0.0001,
|
||||
price,
|
||||
qty,
|
||||
Serum3SelfTradeBehavior.decrementTake,
|
||||
Serum3OrderType.limit,
|
||||
Date.now(),
|
||||
|
@ -192,10 +245,10 @@ async function main() {
|
|||
);
|
||||
|
||||
console.log(`...current own orders on OB`);
|
||||
let orders = await client.getSerum3Orders(
|
||||
let orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
|
||||
client,
|
||||
group,
|
||||
|
||||
'BTC/USDC',
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
);
|
||||
for (const order of orders) {
|
||||
console.log(
|
||||
|
@ -205,18 +258,17 @@ async function main() {
|
|||
await client.serum3CancelOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
||||
'BTC/USDC',
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
|
||||
order.orderId,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`...current own orders on OB`);
|
||||
orders = await client.getSerum3Orders(
|
||||
orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
|
||||
client,
|
||||
group,
|
||||
|
||||
'BTC/USDC',
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
);
|
||||
for (const order of orders) {
|
||||
console.log(order);
|
||||
|
@ -226,12 +278,19 @@ async function main() {
|
|||
await client.serum3SettleFunds(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
||||
'BTC/USDC',
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
);
|
||||
}
|
||||
|
||||
if (true) {
|
||||
if (false) {
|
||||
// serum3 market
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!.toBase58(),
|
||||
);
|
||||
console.log(await serum3Market?.logOb(client, group));
|
||||
}
|
||||
|
||||
if (false) {
|
||||
await mangoAccount.reload(client, group);
|
||||
console.log(
|
||||
'...mangoAccount.getEquity() ' +
|
||||
|
@ -252,13 +311,13 @@ async function main() {
|
|||
console.log(
|
||||
'...mangoAccount.getAssetsVal() ' +
|
||||
toUiDecimalsForQuote(
|
||||
mangoAccount.getAssetsVal(HealthType.init).toNumber(),
|
||||
mangoAccount.getAssetsValue(HealthType.init).toNumber(),
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
'...mangoAccount.getLiabsVal() ' +
|
||||
toUiDecimalsForQuote(
|
||||
mangoAccount.getLiabsVal(HealthType.init).toNumber(),
|
||||
mangoAccount.getLiabsValue(HealthType.init).toNumber(),
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
|
@ -272,14 +331,80 @@ async function main() {
|
|||
).toNumber(),
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
"...mangoAccount.getSerum3MarketMarginAvailable(group, 'BTC/USDC') " +
|
||||
toUiDecimalsForQuote(
|
||||
mangoAccount
|
||||
.getSerum3MarketMarginAvailable(group, 'BTC/USDC')
|
||||
.toNumber(),
|
||||
),
|
||||
}
|
||||
|
||||
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
|
||||
.getMaxSourceForTokenSwap(
|
||||
group,
|
||||
group.banksMapByName.get(src)![0].mint,
|
||||
group.banksMapByName.get(tgt)![0].mint,
|
||||
1,
|
||||
)
|
||||
.div(
|
||||
I80F48.fromNumber(
|
||||
Math.pow(10, group.banksMapByName.get(src)![0].mintDecimals),
|
||||
),
|
||||
)
|
||||
.toNumber(),
|
||||
);
|
||||
}
|
||||
for (const srcToken of Array.from(group.banksMapByName.keys())) {
|
||||
for (const tgtToken of Array.from(group.banksMapByName.keys())) {
|
||||
getMaxSourceForTokenSwapWrapper(srcToken, tgtToken);
|
||||
}
|
||||
}
|
||||
|
||||
const maxQuoteForSerum3BidUi = mangoAccount.getMaxQuoteForSerum3BidUi(
|
||||
group,
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
);
|
||||
console.log(
|
||||
"...mangoAccount.getMaxQuoteForSerum3BidUi(group, 'BTC/USDC') " +
|
||||
maxQuoteForSerum3BidUi,
|
||||
);
|
||||
|
||||
const maxBaseForSerum3AskUi = mangoAccount.getMaxBaseForSerum3AskUi(
|
||||
group,
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
);
|
||||
console.log(
|
||||
"...mangoAccount.getMaxBaseForSerum3AskUi(group, 'BTC/USDC') " +
|
||||
maxBaseForSerum3AskUi,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`simHealthRatioWithSerum3BidUiChanges ${mangoAccount.simHealthRatioWithSerum3BidUiChanges(
|
||||
group,
|
||||
785,
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
)}`,
|
||||
);
|
||||
console.log(
|
||||
`simHealthRatioWithSerum3AskUiChanges ${mangoAccount.simHealthRatioWithSerum3AskUiChanges(
|
||||
group,
|
||||
0.033,
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (false) {
|
||||
console.log(
|
||||
"...mangoAccount.getPerpMarketMarginAvailable(group, 'BTC-PERP') " +
|
||||
toUiDecimalsForQuote(
|
||||
|
@ -290,7 +415,7 @@ async function main() {
|
|||
);
|
||||
}
|
||||
|
||||
if (true) {
|
||||
if (false) {
|
||||
// perps
|
||||
console.log(`...placing perp bid`);
|
||||
try {
|
||||
|
|
|
@ -9,8 +9,11 @@ import {
|
|||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import BN from 'bn.js';
|
||||
import { QUOTE_DECIMALS } from './accounts/bank';
|
||||
import { Bank, QUOTE_DECIMALS } from './accounts/bank';
|
||||
import { Group } from './accounts/group';
|
||||
import { I80F48 } from './accounts/I80F48';
|
||||
import { MangoAccount, Serum3Orders } from './accounts/mangoAccount';
|
||||
import { PerpMarket } from './accounts/perp';
|
||||
|
||||
export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64);
|
||||
|
||||
|
@ -26,6 +29,58 @@ export function debugAccountMetas(ams: AccountMeta[]) {
|
|||
}
|
||||
}
|
||||
|
||||
export function debugHealthAccounts(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
publicKeys: PublicKey[],
|
||||
) {
|
||||
const banks = new Map(
|
||||
Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [
|
||||
banks[0].publicKey.toBase58(),
|
||||
`${banks[0].name} bank`,
|
||||
]),
|
||||
);
|
||||
const oracles = new Map(
|
||||
Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [
|
||||
banks[0].oracle.toBase58(),
|
||||
`${banks[0].name} oracle`,
|
||||
]),
|
||||
);
|
||||
const serum3 = new Map(
|
||||
mangoAccount.serum3Active().map((serum3: Serum3Orders) => {
|
||||
return [
|
||||
serum3.openOrders.toBase58(),
|
||||
`${
|
||||
Array.from(group.serum3MarketsMapByExternal.values()).find(
|
||||
(serum3Market) => serum3Market.marketIndex === serum3.marketIndex,
|
||||
).name
|
||||
} spot oo`,
|
||||
];
|
||||
}),
|
||||
);
|
||||
const perps = new Map(
|
||||
Array.from(group.perpMarketsMap.values()).map((perpMarket: PerpMarket) => [
|
||||
perpMarket.publicKey.toBase58(),
|
||||
`${perpMarket.name} perp market`,
|
||||
]),
|
||||
);
|
||||
|
||||
publicKeys.map((pk) => {
|
||||
if (banks.get(pk.toBase58())) {
|
||||
console.log(banks.get(pk.toBase58()));
|
||||
}
|
||||
if (oracles.get(pk.toBase58())) {
|
||||
console.log(oracles.get(pk.toBase58()));
|
||||
}
|
||||
if (serum3.get(pk.toBase58())) {
|
||||
console.log(serum3.get(pk.toBase58()));
|
||||
}
|
||||
if (perps.get(pk.toBase58())) {
|
||||
console.log(perps.get(pk.toBase58()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function findOrCreate<T>(
|
||||
entityName: string,
|
||||
findMethod: (...x: any) => any,
|
||||
|
|
Loading…
Reference in New Issue