ts client sync with program (#311)

mostly health related code
This commit is contained in:
microwavedcola1 2022-12-02 15:48:43 +01:00 committed by GitHub
parent 9bfa574ee6
commit fa09c557a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1183 additions and 567 deletions

View File

@ -27,6 +27,7 @@ pub async fn runner(
.context
.tokens
.keys()
// TODO: grouping tokens whose oracle might have less confidencen e.g. ORCA with the rest, fails whole ix
// TokenUpdateIndexAndRate is known to take max 71k cu
// from cargo test-bpf local tests
// chunk size of 8 seems to be max before encountering "VersionedTransaction too large" issues

View File

@ -279,8 +279,6 @@ pub mod mango_v4 {
/// Serum
///
// TODO deposit/withdraw msrm
pub fn serum3_register_market(
ctx: Context<Serum3RegisterMarket>,
market_index: Serum3MarketIndex,

View File

@ -579,7 +579,9 @@ impl HealthCache {
for perp_info in self.perp_infos.iter() {
if perp_info.trusted_market {
let positive_contrib = perp_info.health_contribution(health_type).max(I80F48::ZERO);
let positive_contrib = perp_info
.uncapped_health_contribution(health_type)
.max(I80F48::ZERO);
cm!(health += positive_contrib);
}
}

View File

@ -14,6 +14,18 @@ export type OracleConfig = {
reserved: number[];
};
export type StablePriceModel = {
stablePrice: number;
lastUpdateTimestamp: BN;
delayPrices: number[];
delayAccumulatorPrice: number;
delayAccumulatorTime: number;
delayIntervalSeconds: number;
delayGrowthLimit: number;
stableGrowthLimit: number;
lastDelayIntervalIndex: number;
};
export interface BankForHealth {
tokenIndex: TokenIndex;
maintAssetWeight: I80F48;
@ -21,6 +33,7 @@ export interface BankForHealth {
maintLiabWeight: I80F48;
initLiabWeight: I80F48;
price: I80F48;
stablePriceModel: StablePriceModel;
}
export class Bank implements BankForHealth {
@ -53,6 +66,7 @@ export class Bank implements BankForHealth {
static from(
publicKey: PublicKey,
obj: {
// TODO: rearrange fields to have same order as in bank.rs
group: PublicKey;
name: number[];
mint: PublicKey;
@ -88,6 +102,14 @@ export class Bank implements BankForHealth {
tokenIndex: number;
mintDecimals: number;
bankNum: number;
stablePriceModel: StablePriceModel;
minVaultToDepositsRatio: number;
netBorrowsWindowSizeTs: BN;
lastNetBorrowsWindowStartTs: BN;
netBorrowsLimitQuote: BN;
netBorrowsInWindow: BN;
borrowLimitQuote: number;
collateralLimitQuote: number;
},
): Bank {
return new Bank(
@ -127,6 +149,14 @@ export class Bank implements BankForHealth {
obj.tokenIndex as TokenIndex,
obj.mintDecimals,
obj.bankNum,
obj.stablePriceModel,
obj.minVaultToDepositsRatio,
obj.netBorrowsWindowSizeTs,
obj.lastNetBorrowsWindowStartTs,
obj.netBorrowsLimitQuote,
obj.netBorrowsInWindow,
obj.borrowLimitQuote,
obj.collateralLimitQuote,
);
}
@ -167,6 +197,14 @@ export class Bank implements BankForHealth {
public tokenIndex: TokenIndex,
public mintDecimals: number,
public bankNum: number,
public stablePriceModel: StablePriceModel,
minVaultToDepositsRatio: number,
netBorrowsWindowSizeTs: BN,
lastNetBorrowsWindowStartTs: BN,
netBorrowsLimitQuote: BN,
netBorrowsInWindow: BN,
borrowLimitQuote: number,
collateralLimitQuote: number,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
this.depositIndex = I80F48.from(depositIndex);

View File

@ -1,12 +1,12 @@
import { BN } from '@project-serum/anchor';
import { OpenOrders } from '@project-serum/serum';
import { expect } from 'chai';
import _ from 'lodash';
import { I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { toUiDecimalsForQuote } from '../utils';
import { BankForHealth, TokenIndex } from './bank';
import { BankForHealth, StablePriceModel, TokenIndex } from './bank';
import { HealthCache, PerpInfo, Serum3Info, TokenInfo } from './healthCache';
import { HealthType, PerpPosition } from './mangoAccount';
import { PerpMarket } from './perp';
import { PerpMarket, PerpOrderSide } from './perp';
import { MarketIndex } from './serum3';
function mockBankAndOracle(
@ -22,6 +22,7 @@ function mockBankAndOracle(
maintLiabWeight: I80F48.fromNumber(1 + maintWeight),
initLiabWeight: I80F48.fromNumber(1 + initWeight),
price: I80F48.fromNumber(price),
stablePriceModel: { stablePrice: price } as StablePriceModel,
};
}
@ -29,7 +30,8 @@ function mockPerpMarket(
perpMarketIndex: number,
maintWeight: number,
initWeight: number,
price: I80F48,
baseLotSize: number,
price: number,
): PerpMarket {
return {
perpMarketIndex,
@ -37,9 +39,10 @@ function mockPerpMarket(
initAssetWeight: I80F48.fromNumber(1 - initWeight),
maintLiabWeight: I80F48.fromNumber(1 + maintWeight),
initLiabWeight: I80F48.fromNumber(1 + initWeight),
price,
price: I80F48.fromNumber(price),
stablePriceModel: { stablePrice: price } as StablePriceModel,
quoteLotSize: new BN(100),
baseLotSize: new BN(10),
baseLotSize: new BN(baseLotSize),
longFunding: ZERO_I80F48(),
shortFunding: ZERO_I80F48(),
} as unknown as PerpMarket;
@ -78,7 +81,7 @@ describe('Health Cache', () => {
} as any as OpenOrders,
);
const pM = mockPerpMarket(9, 0.1, 0.2, targetBank.price);
const pM = mockPerpMarket(9, 0.1, 0.2, 10, targetBank.price.toNumber());
const pp = new PerpPosition(
pM.perpMarketIndex,
new BN(3),
@ -112,7 +115,7 @@ describe('Health Cache', () => {
const health = hc.health(HealthType.init).toNumber();
console.log(
`health ${health
` - health ${health
.toFixed(3)
.padStart(
10,
@ -122,7 +125,7 @@ describe('Health Cache', () => {
expect(health - (health1 + health2 + health3)).lessThan(0.0000001);
});
it('test_health1', () => {
it('test_health1', (done) => {
function testFixture(fixture: {
name: string;
token1: number;
@ -186,17 +189,17 @@ describe('Health Cache', () => {
} as any as OpenOrders,
);
const pM = mockPerpMarket(9, 0.1, 0.2, bank2.price);
const pM = mockPerpMarket(9, 0.1, 0.2, 10, bank2.price.toNumber());
const pp = new PerpPosition(
pM.perpMarketIndex,
new BN(fixture.perp1[0]),
I80F48.fromNumber(fixture.perp1[1]),
new BN(0),
new BN(0),
I80F48.fromNumber(0),
I80F48.fromNumber(0),
new BN(fixture.perp1[2]),
new BN(fixture.perp1[3]),
I80F48.fromNumber(0),
I80F48.fromNumber(0),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
0,
@ -210,7 +213,7 @@ describe('Health Cache', () => {
const hc = new HealthCache([ti1, ti2, ti3], [si1, si2], [pi1]);
const health = hc.health(HealthType.init).toNumber();
console.log(
`health ${health.toFixed(3).padStart(10)}, case "${fixture.name}"`,
` - case "${fixture.name}" health ${health.toFixed(3).padStart(10)}`,
);
expect(health - fixture.expectedHealth).lessThan(0.0000001);
}
@ -374,76 +377,361 @@ describe('Health Cache', () => {
// oo_1_3 (-> token1)
20.0 * 0.8,
});
done();
});
it('max swap tokens for min ratio', () => {
// USDC like
const sourceBank: BankForHealth = {
tokenIndex: 0 as TokenIndex,
maintAssetWeight: I80F48.fromNumber(1),
initAssetWeight: I80F48.fromNumber(1),
maintLiabWeight: I80F48.fromNumber(1),
initLiabWeight: I80F48.fromNumber(1),
price: I80F48.fromNumber(1),
};
// BTC like
const targetBank: BankForHealth = {
tokenIndex: 1 as TokenIndex,
maintAssetWeight: I80F48.fromNumber(0.9),
initAssetWeight: I80F48.fromNumber(0.8),
maintLiabWeight: I80F48.fromNumber(1.1),
initLiabWeight: I80F48.fromNumber(1.2),
price: I80F48.fromNumber(20000),
};
it('test_max_swap', (done) => {
const b0 = mockBankAndOracle(0 as TokenIndex, 0.1, 0.1, 2);
const b1 = mockBankAndOracle(1 as TokenIndex, 0.2, 0.2, 3);
const b2 = mockBankAndOracle(2 as TokenIndex, 0.3, 0.3, 4);
const banks = [b0, b1, b2];
const hc = new HealthCache(
[
new TokenInfo(
0 as TokenIndex,
sourceBank.maintAssetWeight,
sourceBank.initAssetWeight,
sourceBank.maintLiabWeight,
sourceBank.initLiabWeight,
sourceBank.price!,
I80F48.fromNumber(-18 * Math.pow(10, 6)),
ZERO_I80F48(),
),
new TokenInfo(
1 as TokenIndex,
targetBank.maintAssetWeight,
targetBank.initAssetWeight,
targetBank.maintLiabWeight,
targetBank.initLiabWeight,
targetBank.price!,
I80F48.fromNumber(51 * Math.pow(10, 6)),
ZERO_I80F48(),
),
TokenInfo.fromBank(b0, I80F48.fromNumber(0)),
TokenInfo.fromBank(b1, I80F48.fromNumber(0)),
TokenInfo.fromBank(b2, I80F48.fromNumber(0)),
],
[],
[],
);
expect(
toUiDecimalsForQuote(
hc.getMaxSourceForTokenSwap(
targetBank,
sourceBank,
I80F48.fromNumber(1),
I80F48.fromNumber(0.95),
),
).toFixed(3),
).equals('0.008');
hc
.getMaxSourceForTokenSwap(
b0,
b1,
I80F48.fromNumber(2 / 3),
I80F48.fromNumber(50),
)
.toNumber(),
).lessThan(0.0000001);
function findMaxSwapActual(
hc: HealthCache,
source: TokenIndex,
target: TokenIndex,
ratio: number,
priceFactor: number,
): I80F48[] {
const clonedHc: HealthCache = _.cloneDeep(hc);
const sourcePrice = clonedHc.tokenInfos[source].prices;
const targetPrice = clonedHc.tokenInfos[target].prices;
const swapPrice = I80F48.fromNumber(priceFactor)
.mul(sourcePrice.oracle)
.div(targetPrice.oracle);
const sourceAmount = clonedHc.getMaxSourceForTokenSwap(
banks[source],
banks[target],
swapPrice,
I80F48.fromNumber(ratio),
);
// adjust token balance
clonedHc.tokenInfos[source].balanceNative.isub(sourceAmount);
clonedHc.tokenInfos[target].balanceNative.iadd(
sourceAmount.mul(swapPrice),
);
return [sourceAmount, clonedHc.healthRatio(HealthType.init)];
}
function checkMaxSwapResult(
hc: HealthCache,
source: TokenIndex,
target: TokenIndex,
ratio: number,
priceFactor: number,
): void {
const [sourceAmount, actualRatio] = findMaxSwapActual(
hc,
source,
target,
ratio,
priceFactor,
);
console.log(
` -- checking ${source} to ${target} for priceFactor: ${priceFactor}, target ratio ${ratio}: actual ratio: ${actualRatio}, amount: ${sourceAmount}`,
);
expect(Math.abs(actualRatio.toNumber() - ratio)).lessThan(1);
}
{
console.log(' - test 0');
// adjust by usdc
const clonedHc = _.cloneDeep(hc);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
);
for (const priceFactor of [0.1, 0.9, 1.1]) {
for (const target of _.range(1, 100, 1)) {
// checkMaxSwapResult(
// clonedHc,
// 0 as TokenIndex,
// 1 as TokenIndex,
// target,
// priceFactor,
// );
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
);
// checkMaxSwapResult(
// clonedHc,
// 0 as TokenIndex,
// 2 as TokenIndex,
// target,
// priceFactor,
// );
}
}
// At this unlikely price it's healthy to swap infinitely
expect(function () {
findMaxSwapActual(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
50.0,
1.5,
);
}).to.throw('Number out of range');
}
{
console.log(' - test 1');
const clonedHc = _.cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
);
for (const priceFactor of [0.1, 0.9, 1.1]) {
for (const target of _.range(1, 100, 1)) {
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
2 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
2 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
);
}
}
}
{
console.log(' - test 2');
const clonedHc = _.cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(-50).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
);
// possible even though the init ratio is <100
checkMaxSwapResult(clonedHc, 1 as TokenIndex, 0 as TokenIndex, 100, 1);
}
{
console.log(' - test 3');
const clonedHc = _.cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(-30).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
);
clonedHc.tokenInfos[2].balanceNative.iadd(
I80F48.fromNumber(-30).div(clonedHc.tokenInfos[2].prices.oracle),
);
// swapping with a high ratio advises paying back all liabs
// and then swapping even more because increasing assets in 0 has better asset weight
const initRatio = clonedHc.healthRatio(HealthType.init);
const [amount, actualRatio] = findMaxSwapActual(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
100,
1,
);
expect(actualRatio.div(I80F48.fromNumber(2)).toNumber()).greaterThan(
initRatio.toNumber(),
);
expect(amount.toNumber() - 100 / 3).lessThan(1);
}
{
console.log(' - test 4');
const clonedHc = _.cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(-2).div(clonedHc.tokenInfos[1].prices.oracle),
);
clonedHc.tokenInfos[2].balanceNative.iadd(
I80F48.fromNumber(-65).div(clonedHc.tokenInfos[2].prices.oracle),
);
const initRatio = clonedHc.healthRatio(HealthType.init);
expect(initRatio.toNumber()).greaterThan(3);
expect(initRatio.toNumber()).lessThan(4);
checkMaxSwapResult(clonedHc, 0 as TokenIndex, 1 as TokenIndex, 1, 1);
checkMaxSwapResult(clonedHc, 0 as TokenIndex, 1 as TokenIndex, 3, 1);
checkMaxSwapResult(clonedHc, 0 as TokenIndex, 1 as TokenIndex, 4, 1);
}
done();
});
it('test_max_perp', (done) => {
const baseLotSize = 100;
const b0 = mockBankAndOracle(0 as TokenIndex, 0.0, 0.0, 1);
const p0 = mockPerpMarket(0, 0.3, 0.3, baseLotSize, 2);
const hc = new HealthCache(
[TokenInfo.fromBank(b0, I80F48.fromNumber(0))],
[],
[PerpInfo.emptyFromPerpMarket(p0)],
);
expect(hc.health(HealthType.init).toNumber()).equals(0);
expect(
toUiDecimalsForQuote(
hc.getMaxSourceForTokenSwap(
sourceBank,
targetBank,
I80F48.fromNumber(1),
I80F48.fromNumber(0.95),
),
).toFixed(3),
).equals('90.176');
hc
.getMaxPerpForHealthRatio(
p0,
I80F48.fromNumber(2),
PerpOrderSide.bid,
I80F48.fromNumber(50),
)
.toNumber(),
).equals(0);
function findMaxTrade(
hc: HealthCache,
side: PerpOrderSide,
ratio: number,
priceFactor: number,
): number[] {
const prices = hc.perpInfos[0].prices;
const tradePrice = I80F48.fromNumber(priceFactor).mul(prices.oracle);
const baseLots0 = hc
.getMaxPerpForHealthRatio(
p0,
tradePrice,
side,
I80F48.fromNumber(ratio),
)
.toNumber();
const direction = side == PerpOrderSide.bid ? 1 : -1;
// compute the health ratio we'd get when executing the trade
const baseLots1 = direction * baseLots0;
let baseNative = I80F48.fromNumber(baseLots1).mul(
I80F48.fromNumber(baseLotSize),
);
let hcClone: HealthCache = _.cloneDeep(hc);
hcClone.perpInfos[0].baseLots.iadd(new BN(baseLots1));
hcClone.perpInfos[0].quote.isub(baseNative.mul(tradePrice));
const actualRatio = hcClone.healthRatio(HealthType.init);
// the ratio for trading just one base lot extra
const baseLots2 = direction * (baseLots0 + 1);
baseNative = I80F48.fromNumber(baseLots2 * baseLotSize);
hcClone = _.cloneDeep(hc);
hcClone.perpInfos[0].baseLots.iadd(new BN(baseLots2));
hcClone.perpInfos[0].quote.isub(baseNative.mul(tradePrice));
const plusRatio = hcClone.healthRatio(HealthType.init);
return [baseLots0, actualRatio.toNumber(), plusRatio.toNumber()];
}
function checkMaxTrade(
hc: HealthCache,
side: PerpOrderSide,
ratio: number,
priceFactor: number,
): void {
const [baseLots, actualRatio, plusRatio] = findMaxTrade(
hc,
side,
ratio,
priceFactor,
);
console.log(
`checking for price_factor: ${priceFactor}, target ratio ${ratio}: actual ratio: ${actualRatio}, plus ratio: ${plusRatio}, base_lots: ${baseLots}`,
);
expect(ratio).lessThan(actualRatio);
expect(plusRatio - 0.1).lessThanOrEqual(ratio);
}
// adjust token
hc.tokenInfos[0].balanceNative.iadd(I80F48.fromNumber(3000));
for (const existing of [-5, 0, 3]) {
const hcClone: HealthCache = _.cloneDeep(hc);
hcClone.perpInfos[0].baseLots.iadd(new BN(existing));
hcClone.perpInfos[0].quote.isub(
I80F48.fromNumber(existing * baseLotSize * 2),
);
for (const side of [PerpOrderSide.bid, PerpOrderSide.ask]) {
console.log(
`existing ${existing} ${side === PerpOrderSide.bid ? 'bid' : 'ask'}`,
);
for (const priceFactor of [0.8, 1.0, 1.1]) {
for (const ratio of _.range(1, 101, 1)) {
checkMaxTrade(hcClone, side, ratio, priceFactor);
}
}
}
}
// check some extremely bad prices
checkMaxTrade(hc, PerpOrderSide.bid, 50, 2);
checkMaxTrade(hc, PerpOrderSide.ask, 50, 0.1);
// and extremely good prices
expect(function () {
findMaxTrade(hc, PerpOrderSide.bid, 50, 0.1);
}).to.throw();
expect(function () {
findMaxTrade(hc, PerpOrderSide.ask, 50, 1.5);
}).to.throw();
done();
});
});

File diff suppressed because it is too large Load Diff

View File

@ -482,29 +482,28 @@ export class MangoAccount {
/**
* The max amount of given source ui token you can swap to a target token.
* PriceFactor is ratio between A - how many source tokens can be traded for target tokens
* and B - source native oracle price / target native oracle price.
* e.g. a slippage of 5% and some fees which are 1%, then priceFactor = 0.94
* the factor is used to compute how much target can be obtained by swapping source
* in reality, and not only relying on oracle prices, and taking in account e.g. slippage which
* can occur at large size
* Price is simply the source tokens price divided by target tokens price,
* it is supposed to give an indication of how many source tokens can be traded for target tokens,
* it can optionally contain information on slippage and fees.
* @returns max amount of given source ui token you can swap to a target token, in ui token
*/
getMaxSourceUiForTokenSwap(
group: Group,
sourceMintPk: PublicKey,
targetMintPk: PublicKey,
priceFactor: number,
price: number,
): number {
if (sourceMintPk.equals(targetMintPk)) {
return 0;
}
const s = group.getFirstBankByMint(sourceMintPk);
const t = group.getFirstBankByMint(targetMintPk);
const hc = HealthCache.fromMangoAccount(group, this);
const maxSource = hc.getMaxSourceForTokenSwap(
group.getFirstBankByMint(sourceMintPk),
group.getFirstBankByMint(targetMintPk),
s,
t,
I80F48.fromNumber(price * Math.pow(10, t.mintDecimals - s.mintDecimals)),
I80F48.fromNumber(2), // target 2% health
I80F48.fromNumber(priceFactor),
);
maxSource.idiv(
ONE_I80F48().add(
@ -609,6 +608,8 @@ export class MangoAccount {
}
/**
* TODO REWORK, know to break in binary search, also make work for limit orders
*
* @param group
* @param externalMarketPk
* @returns maximum ui quote which can be traded at oracle price for base token given current health
@ -646,6 +647,7 @@ export class MangoAccount {
}
/**
* TODO REWORK, know to break in binary search, also make work for limit orders
* @param group
* @param externalMarketPk
* @returns maximum ui base which can be traded at oracle price for quote token given current health
@ -673,7 +675,7 @@ export class MangoAccount {
// 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.
nativeAmount = nativeAmount
.div(baseBank.price)
// .div(baseBank.price)
.div(ONE_I80F48().add(baseBank.loanOriginationFeeRate))
.div(ONE_I80F48().add(I80F48.fromNumber(group.getSerum3FeeRates(false))));
return toUiDecimals(
@ -795,7 +797,10 @@ export class MangoAccount {
}
/**
* TODO: also think about limit orders
*
* The max ui quote you can place a market/ioc bid on the market,
* price is the ui price at which you think the order would materialiase.
* @param group
* @param perpMarketName
* @returns maximum ui quote which can be traded at oracle price for quote token given current health
@ -803,15 +808,13 @@ export class MangoAccount {
public getMaxQuoteForPerpBidUi(
group: Group,
perpMarketIndex: PerpMarketIndex,
price: 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),
I80F48.fromNumber(price),
PerpOrderSide.bid,
I80F48.fromNumber(2),
);
@ -821,7 +824,10 @@ export class MangoAccount {
}
/**
* TODO: also think about limit orders
*
* The max ui base you can place a market/ioc ask on the market,
* price is the ui price at which you think the order would materialiase.
* @param group
* @param perpMarketName
* @param uiPrice ui price at which ask would be placed at
@ -830,15 +836,13 @@ export class MangoAccount {
public getMaxBaseForPerpAskUi(
group: Group,
perpMarketIndex: PerpMarketIndex,
price: 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),
I80F48.fromNumber(price),
PerpOrderSide.ask,
I80F48.fromNumber(2),
);
@ -849,6 +853,7 @@ export class MangoAccount {
group: Group,
perpMarketIndex: PerpMarketIndex,
size: number,
price: number,
): number {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
@ -859,8 +864,9 @@ export class MangoAccount {
pp
? pp
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
perpMarket.uiBaseToLots(size),
PerpOrderSide.bid,
perpMarket.uiBaseToLots(size),
I80F48.fromNumber(price),
HealthType.init,
)
.toNumber();
@ -870,6 +876,7 @@ export class MangoAccount {
group: Group,
perpMarketIndex: PerpMarketIndex,
size: number,
price: number,
): number {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
@ -880,8 +887,9 @@ export class MangoAccount {
pp
? pp
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
perpMarket.uiBaseToLots(size),
PerpOrderSide.ask,
perpMarket.uiBaseToLots(size),
I80F48.fromNumber(price),
HealthType.init,
)
.toNumber();
@ -1228,6 +1236,7 @@ export class PerpPosition {
);
}
// TODO FUTURE: double check with program side code that this is in sycn with latest changes in program
public getEntryPrice(perpMarket: PerpMarket): BN {
if (this.basePositionLots.eq(new BN(0))) {
return new BN(0);
@ -1237,6 +1246,7 @@ export class PerpPosition {
.abs();
}
// TODO FUTURE: double check with program side code that this is in sycn with latest changes in program
public getBreakEvenPrice(perpMarket: PerpMarket): BN {
if (this.basePositionLots.eq(new BN(0))) {
return new BN(0);

View File

@ -5,7 +5,12 @@ import Big from 'big.js';
import { MangoClient } from '../client';
import { I80F48, I80F48Dto, ZERO_I80F48 } from '../numbers/I80F48';
import { As, toNative, U64_MAX_BN } from '../utils';
import { OracleConfig, QUOTE_DECIMALS, TokenIndex } from './bank';
import {
OracleConfig,
QUOTE_DECIMALS,
StablePriceModel,
TokenIndex,
} from './bank';
import { Group } from './group';
import { MangoAccount } from './mangoAccount';
@ -73,6 +78,7 @@ export class PerpMarket {
settleFeeFlat: number;
settleFeeAmountThreshold: number;
settleFeeFractionLowHealth: number;
stablePriceModel: StablePriceModel;
},
): PerpMarket {
return new PerpMarket(
@ -112,6 +118,7 @@ export class PerpMarket {
obj.settleFeeFlat,
obj.settleFeeAmountThreshold,
obj.settleFeeFractionLowHealth,
obj.stablePriceModel,
);
}
@ -152,6 +159,7 @@ export class PerpMarket {
public settleFeeFlat: number,
public settleFeeAmountThreshold: number,
public settleFeeFractionLowHealth: number,
public stablePriceModel: StablePriceModel,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
this.maintAssetWeight = I80F48.from(maintAssetWeight);

View File

@ -285,7 +285,7 @@ export class MangoClient {
public async tokenEdit(
group: Group,
mintPk: PublicKey,
oracle: PublicKey | null,
oracle: PublicKey, // TODO: do we need an extra param for resetting stable_price_model?
oracleConfig: OracleConfigParams | null,
groupInsuranceFund: boolean | null,
interestRateParams: InterestRateParams | null,
@ -332,6 +332,7 @@ export class MangoClient {
)
.accounts({
group: group.publicKey,
oracle,
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
mintInfo: mintInfo.publicKey,
})
@ -342,7 +343,7 @@ export class MangoClient {
isSigner: false,
} as AccountMeta,
])
.rpc({ skipPreflight: true });
.rpc();
}
public async tokenDeregister(
@ -1469,7 +1470,7 @@ export class MangoClient {
public async perpEditMarket(
group: Group,
perpMarketIndex: PerpMarketIndex,
oracle: PublicKey | null,
oracle: PublicKey, // TODO: do we need an extra param for resetting stable_price_model
oracleConfig: OracleConfigParams | null,
baseDecimals: number | null,
maintAssetWeight: number | null,
@ -1527,6 +1528,7 @@ export class MangoClient {
)
.accounts({
group: group.publicKey,
oracle,
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
perpMarket: perpMarket.publicKey,
})
@ -1775,7 +1777,7 @@ export class MangoClient {
new BN(clientOrderId ?? Date.now()),
orderType ? orderType : PerpOrderType.limit,
reduceOnly ? reduceOnly : false,
new BN(expiryTimestamp ? expiryTimestamp : 0),
new BN(expiryTimestamp ?? 0),
limit ? limit : 10,
-1,
)

View File

@ -1,5 +1,6 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Cluster, Connection, Keypair } from '@solana/web3.js';
import { expect } from 'chai';
import fs from 'fs';
import { Group } from '../accounts/group';
import { HealthCache } from '../accounts/healthCache';
@ -95,50 +96,83 @@ async function debugUser(
await getMaxWithdrawWithBorrowForTokenUiWrapper(srcToken);
}
function simHealthRatioWithTokenPositionChangesWrapper(debug, change): void {
console.log(
`mangoAccount.simHealthRatioWithTokenPositionChanges ${debug}` +
mangoAccount.simHealthRatioWithTokenPositionUiChanges(group, [change]),
);
}
for (const srcToken of Array.from(group.banksMapByName.keys())) {
simHealthRatioWithTokenPositionChangesWrapper(`${srcToken} 1 `, {
mintPk: group.banksMapByName.get(srcToken)![0].mint,
uiTokenAmount: 1,
});
simHealthRatioWithTokenPositionChangesWrapper(`${srcToken} -1 `, {
mintPk: group.banksMapByName.get(srcToken)![0].mint,
uiTokenAmount: -1,
});
}
function getMaxSourceForTokenSwapWrapper(src, tgt): void {
const maxSourceUi = mangoAccount.getMaxSourceUiForTokenSwap(
group,
group.banksMapByName.get(src)![0].mint,
group.banksMapByName.get(tgt)![0].mint,
group.banksMapByName.get(src)![0].uiPrice /
group.banksMapByName.get(tgt)![0].uiPrice,
);
const maxTargetUi =
maxSourceUi *
(group.banksMapByName.get(src)![0].uiPrice /
group.banksMapByName.get(tgt)![0].uiPrice);
const sim = mangoAccount.simHealthRatioWithTokenPositionUiChanges(group, [
{
mintPk: group.banksMapByName.get(src)![0].mint,
uiTokenAmount: -maxSourceUi,
},
{
mintPk: group.banksMapByName.get(tgt)![0].mint,
uiTokenAmount: maxTargetUi,
},
]);
if (maxSourceUi > 0) {
expect(sim).gt(2);
expect(sim).lt(3);
}
console.log(
`getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` +
mangoAccount.getMaxSourceUiForTokenSwap(
group,
group.banksMapByName.get(src)![0].mint,
group.banksMapByName.get(tgt)![0].mint,
1,
),
maxSourceUi.toFixed(3).padStart(10) +
`, health ratio after (${sim.toFixed(3).padStart(10)})`,
);
}
for (const srcToken of Array.from(group.banksMapByName.keys())) {
for (const tgtToken of Array.from(group.banksMapByName.keys())) {
// if (srcToken === 'SOL')
// if (tgtToken === 'MSOL')
for (const srcToken of Array.from(group.banksMapByName.keys()).sort()) {
for (const tgtToken of Array.from(group.banksMapByName.keys()).sort()) {
getMaxSourceForTokenSwapWrapper(srcToken, tgtToken);
}
}
function getMaxForPerpWrapper(perpMarket: PerpMarket): void {
console.log(
`getMaxQuoteForPerpBidUi ${perpMarket.perpMarketIndex} ` +
mangoAccount.getMaxQuoteForPerpBidUi(group, perpMarket.perpMarketIndex),
const maxQuoteUi = mangoAccount.getMaxQuoteForPerpBidUi(
group,
perpMarket.perpMarketIndex,
perpMarket.uiPrice,
);
const simMaxQuote = mangoAccount.simHealthRatioWithPerpBidUiChanges(
group,
perpMarket.perpMarketIndex,
maxQuoteUi / perpMarket.uiPrice,
perpMarket.uiPrice,
);
expect(simMaxQuote).gt(2);
expect(simMaxQuote).lt(3);
const maxBaseUi = mangoAccount.getMaxBaseForPerpAskUi(
group,
perpMarket.perpMarketIndex,
perpMarket.uiPrice,
);
const simMaxBase = mangoAccount.simHealthRatioWithPerpAskUiChanges(
group,
perpMarket.perpMarketIndex,
maxBaseUi,
perpMarket.uiPrice,
);
expect(simMaxBase).gt(2);
expect(simMaxBase).lt(3);
console.log(
`getMaxBaseForPerpAskUi ${perpMarket.perpMarketIndex} ` +
mangoAccount.getMaxBaseForPerpAskUi(group, perpMarket.perpMarketIndex),
`getMaxPerp ${perpMarket.name.padStart(
10,
)} getMaxQuoteForPerpBidUi ${maxQuoteUi
.toFixed(3)
.padStart(10)} health ratio after (${simMaxQuote
.toFixed(3)
.padStart(10)}), getMaxBaseForPerpAskUi ${maxBaseUi
.toFixed(3)
.padStart(10)} health ratio after (${simMaxBase
.toFixed(3)
.padStart(10)})`,
);
}
for (const perpMarket of Array.from(
@ -148,7 +182,6 @@ async function debugUser(
}
function getMaxForSerum3Wrapper(serum3Market: Serum3Market): void {
// if (serum3Market.name !== 'SOL/USDC') return;
console.log(
`getMaxQuoteForSerum3BidUi ${serum3Market.name} ` +
mangoAccount.getMaxQuoteForSerum3BidUi(

View File

@ -3667,12 +3667,37 @@ export type MangoV4 = {
],
"type": "i64"
},
{
"name": "borrowLimitQuote",
"docs": [
"Soft borrow limit in native quote",
"",
"Once the borrows on the bank exceed this quote value, init_liab_weight is scaled up.",
"Set to f64::MAX to disable.",
"",
"See scaled_init_liab_weight()."
],
"type": "f64"
},
{
"name": "collateralLimitQuote",
"docs": [
"Limit for collateral of deposits",
"",
"Once the deposits in the bank exceed this quote value, init_asset_weight is scaled",
"down to keep the total collateral value constant.",
"Set to f64::MAX to disable.",
"",
"See scaled_init_asset_weight()."
],
"type": "f64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
2136
2120
]
}
}
@ -4319,10 +4344,17 @@ export type MangoV4 = {
},
{
"name": "settlePnlLimitFactor",
"docs": [
"Fraction of perp base value that can be settled each window.",
"Set to a negative value to disable the limit."
],
"type": "f32"
},
{
"name": "settlePnlLimitWindowSizeTs",
"docs": [
"Window size in seconds for the perp settlement limit"
],
"type": "u64"
},
{
@ -10894,12 +10926,37 @@ export const IDL: MangoV4 = {
],
"type": "i64"
},
{
"name": "borrowLimitQuote",
"docs": [
"Soft borrow limit in native quote",
"",
"Once the borrows on the bank exceed this quote value, init_liab_weight is scaled up.",
"Set to f64::MAX to disable.",
"",
"See scaled_init_liab_weight()."
],
"type": "f64"
},
{
"name": "collateralLimitQuote",
"docs": [
"Limit for collateral of deposits",
"",
"Once the deposits in the bank exceed this quote value, init_asset_weight is scaled",
"down to keep the total collateral value constant.",
"Set to f64::MAX to disable.",
"",
"See scaled_init_asset_weight()."
],
"type": "f64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
2136
2120
]
}
}
@ -11546,10 +11603,17 @@ export const IDL: MangoV4 = {
},
{
"name": "settlePnlLimitFactor",
"docs": [
"Fraction of perp base value that can be settled each window.",
"Set to a negative value to disable the limit."
],
"type": "f32"
},
{
"name": "settlePnlLimitWindowSizeTs",
"docs": [
"Window size in seconds for the perp settlement limit"
],
"type": "u64"
},
{

View File

@ -20,6 +20,7 @@ import { buildVersionedTx } from '../utils';
// * solana airdrop 1 -k ~/.config/solana/admin.json
//
// TODO: switch out with devnet openbook markets
const DEVNET_SERUM3_MARKETS = new Map([
['BTC/USDC', 'DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB'],
['SOL/USDC', '5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR'],
@ -44,6 +45,7 @@ const DEVNET_ORACLES = new Map([
['SRM', '992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs'],
]);
// TODO: should these constants be baked right into client.ts or even program?
const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2;
const NET_BORROWS_WINDOW_SIZE_TS = 24 * 60 * 60;
const NET_BORROWS_LIMIT_NATIVE = 1 * Math.pow(10, 7) * Math.pow(10, 6);

View File

@ -5,11 +5,6 @@ import fs from 'fs';
import { Group } from '../accounts/group';
import { HealthType } from '../accounts/mangoAccount';
import { PerpOrderSide, PerpOrderType } from '../accounts/perp';
import {
Serum3OrderType,
Serum3SelfTradeBehavior,
Serum3Side,
} from '../accounts/serum3';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
import { toUiDecimalsForQuote } from '../utils';
@ -193,131 +188,132 @@ async function main() {
await mangoAccount.reload(client);
}
if (true) {
// serum3
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(bids!)![0];
// Note: Disable for now until we have openbook devnet markets
// if (true) {
// // serum3
// 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(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,
);
}
// 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;
console.log(
`...placing serum3 bid which would not be settled since its relatively low then midprice at ${price} for ${qty}`,
);
await client.serum3PlaceOrder(
group,
mangoAccount,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
Serum3Side.bid,
price,
qty,
Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.limit,
Date.now(),
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);
// let price = 20;
// let qty = 0.0001;
// console.log(
// `...placing serum3 bid which would not be settled since its relatively low then midprice at ${price} for ${qty}`,
// );
// await client.serum3PlaceOrder(
// group,
// mangoAccount,
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
// Serum3Side.bid,
// price,
// qty,
// Serum3SelfTradeBehavior.decrementTake,
// Serum3OrderType.limit,
// Date.now(),
// 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;
console.log(
`...placing serum3 bid way above midprice at ${price} for ${qty}`,
);
await client.serum3PlaceOrder(
group,
mangoAccount,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
Serum3Side.bid,
price,
qty,
Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.limit,
Date.now(),
10,
);
await mangoAccount.reload(client);
// 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,
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
// Serum3Side.bid,
// price,
// qty,
// Serum3SelfTradeBehavior.decrementTake,
// Serum3OrderType.limit,
// Date.now(),
// 10,
// );
// await mangoAccount.reload(client);
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,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
Serum3Side.ask,
price,
qty,
Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.limit,
Date.now(),
10,
);
// 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,
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
// Serum3Side.ask,
// price,
// qty,
// Serum3SelfTradeBehavior.decrementTake,
// Serum3OrderType.limit,
// Date.now(),
// 10,
// );
console.log(`...current own orders on OB`);
orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
client,
group,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
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,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
order.orderId,
);
}
// console.log(`...current own orders on OB`);
// orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
// client,
// group,
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
// );
// 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,
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
// order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
// order.orderId,
// );
// }
console.log(`...current own orders on OB`);
orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
client,
group,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
for (const order of orders) {
console.log(order);
}
// console.log(`...current own orders on OB`);
// orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
// client,
// group,
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
// );
// for (const order of orders) {
// console.log(order);
// }
console.log(`...settling funds`);
await client.serum3SettleFunds(
group,
mangoAccount,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
}
// console.log(`...settling funds`);
// await client.serum3SettleFunds(
// group,
// mangoAccount,
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
// );
// }
if (true) {
await mangoAccount.reload(client);
@ -505,6 +501,7 @@ async function main() {
const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi(
group,
perpMarket.perpMarketIndex,
perpMarket.uiPrice,
);
const baseQty = quoteQty / price;
console.log(
@ -512,6 +509,7 @@ async function main() {
group,
perpMarket.perpMarketIndex,
baseQty,
perpMarket.uiPrice,
)}`,
);
console.log(
@ -554,6 +552,7 @@ async function main() {
mangoAccount.getMaxQuoteForPerpBidUi(
group,
perpMarket.perpMarketIndex,
perpMarket.uiPrice,
) * 1.02;
const baseQty = quoteQty / price;
@ -589,12 +588,14 @@ async function main() {
const baseQty = mangoAccount.getMaxBaseForPerpAskUi(
group,
perpMarket.perpMarketIndex,
perpMarket.uiPrice,
);
console.log(
` simHealthRatioWithPerpAskUiChanges - ${mangoAccount.simHealthRatioWithPerpAskUiChanges(
group,
perpMarket.perpMarketIndex,
baseQty,
perpMarket.uiPrice,
)}`,
);
const quoteQty = baseQty * price;
@ -627,8 +628,11 @@ async function main() {
group.banksMapByName.get('BTC')![0].uiPrice! +
Math.floor(Math.random() * 100);
const baseQty =
mangoAccount.getMaxBaseForPerpAskUi(group, perpMarket.perpMarketIndex) *
1.02;
mangoAccount.getMaxBaseForPerpAskUi(
group,
perpMarket.perpMarketIndex,
perpMarket.uiPrice,
) * 1.02;
const quoteQty = baseQty * price;
console.log(
`...placing max qty perp ask * 1.02 clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,

View File

@ -43,6 +43,7 @@ const MAINNET_ORACLES = new Map([
// External markets are matched with those in https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json
// and verified to have best liquidity for pair on https://openserum.io/
// TODO: replace with markets from https://github.com/openbook-dex/resources/blob/main/markets.json
const MAINNET_SERUM3_MARKETS = new Map([
['BTC/USDC', 'A8YFbxQYFVqKZaoYJLLUVcQiWP7G2MeEgW5wsAQgMvFw'],
['SOL/USDC', '9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT'],

View File

@ -156,7 +156,7 @@ async function main() {
await client.tokenEdit(
group,
buyMint,
null,
group.getFirstBankByMint(buyMint).oracle,
null,
null,
null,
@ -196,7 +196,7 @@ async function main() {
await client.tokenEdit(
group,
buyMint,
null,
group.getFirstBankByMint(buyMint).oracle,
null,
null,
null,