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 .context
.tokens .tokens
.keys() .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 // TokenUpdateIndexAndRate is known to take max 71k cu
// from cargo test-bpf local tests // from cargo test-bpf local tests
// chunk size of 8 seems to be max before encountering "VersionedTransaction too large" issues // 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 /// Serum
/// ///
// TODO deposit/withdraw msrm
pub fn serum3_register_market( pub fn serum3_register_market(
ctx: Context<Serum3RegisterMarket>, ctx: Context<Serum3RegisterMarket>,
market_index: Serum3MarketIndex, market_index: Serum3MarketIndex,

View File

@ -579,7 +579,9 @@ impl HealthCache {
for perp_info in self.perp_infos.iter() { for perp_info in self.perp_infos.iter() {
if perp_info.trusted_market { 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); cm!(health += positive_contrib);
} }
} }

View File

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

View File

@ -1,12 +1,12 @@
import { BN } from '@project-serum/anchor'; import { BN } from '@project-serum/anchor';
import { OpenOrders } from '@project-serum/serum'; import { OpenOrders } from '@project-serum/serum';
import { expect } from 'chai'; import { expect } from 'chai';
import _ from 'lodash';
import { I80F48, ZERO_I80F48 } from '../numbers/I80F48'; import { I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { toUiDecimalsForQuote } from '../utils'; import { BankForHealth, StablePriceModel, TokenIndex } from './bank';
import { BankForHealth, TokenIndex } from './bank';
import { HealthCache, PerpInfo, Serum3Info, TokenInfo } from './healthCache'; import { HealthCache, PerpInfo, Serum3Info, TokenInfo } from './healthCache';
import { HealthType, PerpPosition } from './mangoAccount'; import { HealthType, PerpPosition } from './mangoAccount';
import { PerpMarket } from './perp'; import { PerpMarket, PerpOrderSide } from './perp';
import { MarketIndex } from './serum3'; import { MarketIndex } from './serum3';
function mockBankAndOracle( function mockBankAndOracle(
@ -22,6 +22,7 @@ function mockBankAndOracle(
maintLiabWeight: I80F48.fromNumber(1 + maintWeight), maintLiabWeight: I80F48.fromNumber(1 + maintWeight),
initLiabWeight: I80F48.fromNumber(1 + initWeight), initLiabWeight: I80F48.fromNumber(1 + initWeight),
price: I80F48.fromNumber(price), price: I80F48.fromNumber(price),
stablePriceModel: { stablePrice: price } as StablePriceModel,
}; };
} }
@ -29,7 +30,8 @@ function mockPerpMarket(
perpMarketIndex: number, perpMarketIndex: number,
maintWeight: number, maintWeight: number,
initWeight: number, initWeight: number,
price: I80F48, baseLotSize: number,
price: number,
): PerpMarket { ): PerpMarket {
return { return {
perpMarketIndex, perpMarketIndex,
@ -37,9 +39,10 @@ function mockPerpMarket(
initAssetWeight: I80F48.fromNumber(1 - initWeight), initAssetWeight: I80F48.fromNumber(1 - initWeight),
maintLiabWeight: I80F48.fromNumber(1 + maintWeight), maintLiabWeight: I80F48.fromNumber(1 + maintWeight),
initLiabWeight: I80F48.fromNumber(1 + initWeight), initLiabWeight: I80F48.fromNumber(1 + initWeight),
price, price: I80F48.fromNumber(price),
stablePriceModel: { stablePrice: price } as StablePriceModel,
quoteLotSize: new BN(100), quoteLotSize: new BN(100),
baseLotSize: new BN(10), baseLotSize: new BN(baseLotSize),
longFunding: ZERO_I80F48(), longFunding: ZERO_I80F48(),
shortFunding: ZERO_I80F48(), shortFunding: ZERO_I80F48(),
} as unknown as PerpMarket; } as unknown as PerpMarket;
@ -78,7 +81,7 @@ describe('Health Cache', () => {
} as any as OpenOrders, } 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( const pp = new PerpPosition(
pM.perpMarketIndex, pM.perpMarketIndex,
new BN(3), new BN(3),
@ -112,7 +115,7 @@ describe('Health Cache', () => {
const health = hc.health(HealthType.init).toNumber(); const health = hc.health(HealthType.init).toNumber();
console.log( console.log(
`health ${health ` - health ${health
.toFixed(3) .toFixed(3)
.padStart( .padStart(
10, 10,
@ -122,7 +125,7 @@ describe('Health Cache', () => {
expect(health - (health1 + health2 + health3)).lessThan(0.0000001); expect(health - (health1 + health2 + health3)).lessThan(0.0000001);
}); });
it('test_health1', () => { it('test_health1', (done) => {
function testFixture(fixture: { function testFixture(fixture: {
name: string; name: string;
token1: number; token1: number;
@ -186,17 +189,17 @@ describe('Health Cache', () => {
} as any as OpenOrders, } 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( const pp = new PerpPosition(
pM.perpMarketIndex, pM.perpMarketIndex,
new BN(fixture.perp1[0]), new BN(fixture.perp1[0]),
I80F48.fromNumber(fixture.perp1[1]), 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[2]),
new BN(fixture.perp1[3]), new BN(fixture.perp1[3]),
I80F48.fromNumber(0),
I80F48.fromNumber(0),
new BN(0),
new BN(0),
new BN(0), new BN(0),
new BN(0), new BN(0),
0, 0,
@ -210,7 +213,7 @@ describe('Health Cache', () => {
const hc = new HealthCache([ti1, ti2, ti3], [si1, si2], [pi1]); const hc = new HealthCache([ti1, ti2, ti3], [si1, si2], [pi1]);
const health = hc.health(HealthType.init).toNumber(); const health = hc.health(HealthType.init).toNumber();
console.log( 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); expect(health - fixture.expectedHealth).lessThan(0.0000001);
} }
@ -374,76 +377,361 @@ describe('Health Cache', () => {
// oo_1_3 (-> token1) // oo_1_3 (-> token1)
20.0 * 0.8, 20.0 * 0.8,
}); });
done();
}); });
it('max swap tokens for min ratio', () => { it('test_max_swap', (done) => {
// USDC like const b0 = mockBankAndOracle(0 as TokenIndex, 0.1, 0.1, 2);
const sourceBank: BankForHealth = { const b1 = mockBankAndOracle(1 as TokenIndex, 0.2, 0.2, 3);
tokenIndex: 0 as TokenIndex, const b2 = mockBankAndOracle(2 as TokenIndex, 0.3, 0.3, 4);
maintAssetWeight: I80F48.fromNumber(1), const banks = [b0, b1, b2];
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),
};
const hc = new HealthCache( const hc = new HealthCache(
[ [
new TokenInfo( TokenInfo.fromBank(b0, I80F48.fromNumber(0)),
0 as TokenIndex, TokenInfo.fromBank(b1, I80F48.fromNumber(0)),
sourceBank.maintAssetWeight, TokenInfo.fromBank(b2, I80F48.fromNumber(0)),
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(),
),
], ],
[], [],
[], [],
); );
expect( expect(
toUiDecimalsForQuote( hc
hc.getMaxSourceForTokenSwap( .getMaxSourceForTokenSwap(
targetBank, b0,
sourceBank, b1,
I80F48.fromNumber(1), I80F48.fromNumber(2 / 3),
I80F48.fromNumber(0.95), I80F48.fromNumber(50),
), )
).toFixed(3), .toNumber(),
).equals('0.008'); ).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( expect(
toUiDecimalsForQuote( hc
hc.getMaxSourceForTokenSwap( .getMaxPerpForHealthRatio(
sourceBank, p0,
targetBank, I80F48.fromNumber(2),
I80F48.fromNumber(1), PerpOrderSide.bid,
I80F48.fromNumber(0.95), I80F48.fromNumber(50),
), )
).toFixed(3), .toNumber(),
).equals('90.176'); ).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. * 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 * Price is simply the source tokens price divided by target tokens price,
* and B - source native oracle price / target native oracle price. * it is supposed to give an indication of how many source tokens can be traded for target tokens,
* e.g. a slippage of 5% and some fees which are 1%, then priceFactor = 0.94 * it can optionally contain information on slippage and fees.
* 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
* @returns max amount of given source ui token you can swap to a target token, in ui token * @returns max amount of given source ui token you can swap to a target token, in ui token
*/ */
getMaxSourceUiForTokenSwap( getMaxSourceUiForTokenSwap(
group: Group, group: Group,
sourceMintPk: PublicKey, sourceMintPk: PublicKey,
targetMintPk: PublicKey, targetMintPk: PublicKey,
priceFactor: number, price: number,
): number { ): number {
if (sourceMintPk.equals(targetMintPk)) { if (sourceMintPk.equals(targetMintPk)) {
return 0; return 0;
} }
const s = group.getFirstBankByMint(sourceMintPk);
const t = group.getFirstBankByMint(targetMintPk);
const hc = HealthCache.fromMangoAccount(group, this); const hc = HealthCache.fromMangoAccount(group, this);
const maxSource = hc.getMaxSourceForTokenSwap( const maxSource = hc.getMaxSourceForTokenSwap(
group.getFirstBankByMint(sourceMintPk), s,
group.getFirstBankByMint(targetMintPk), t,
I80F48.fromNumber(price * Math.pow(10, t.mintDecimals - s.mintDecimals)),
I80F48.fromNumber(2), // target 2% health I80F48.fromNumber(2), // target 2% health
I80F48.fromNumber(priceFactor),
); );
maxSource.idiv( maxSource.idiv(
ONE_I80F48().add( 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 group
* @param externalMarketPk * @param externalMarketPk
* @returns maximum ui quote which can be traded at oracle price for base token given current health * @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 group
* @param externalMarketPk * @param externalMarketPk
* @returns maximum ui base which can be traded at oracle price for quote token given current health * @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 // 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. // also keep some buffer for fees, use taker fees for worst case simulation.
nativeAmount = nativeAmount nativeAmount = nativeAmount
.div(baseBank.price) // .div(baseBank.price)
.div(ONE_I80F48().add(baseBank.loanOriginationFeeRate)) .div(ONE_I80F48().add(baseBank.loanOriginationFeeRate))
.div(ONE_I80F48().add(I80F48.fromNumber(group.getSerum3FeeRates(false)))); .div(ONE_I80F48().add(I80F48.fromNumber(group.getSerum3FeeRates(false))));
return toUiDecimals( 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 group
* @param perpMarketName * @param perpMarketName
* @returns maximum ui quote which can be traded at oracle price for quote token given current health * @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( public getMaxQuoteForPerpBidUi(
group: Group, group: Group,
perpMarketIndex: PerpMarketIndex, perpMarketIndex: PerpMarketIndex,
price: number,
): number { ): number {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
const hc = HealthCache.fromMangoAccount(group, this); const hc = HealthCache.fromMangoAccount(group, this);
const baseLots = hc.getMaxPerpForHealthRatio( const baseLots = hc.getMaxPerpForHealthRatio(
perpMarket, perpMarket,
pp I80F48.fromNumber(price),
? pp
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
PerpOrderSide.bid, PerpOrderSide.bid,
I80F48.fromNumber(2), 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 group
* @param perpMarketName * @param perpMarketName
* @param uiPrice ui price at which ask would be placed at * @param uiPrice ui price at which ask would be placed at
@ -830,15 +836,13 @@ export class MangoAccount {
public getMaxBaseForPerpAskUi( public getMaxBaseForPerpAskUi(
group: Group, group: Group,
perpMarketIndex: PerpMarketIndex, perpMarketIndex: PerpMarketIndex,
price: number,
): number { ): number {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
const hc = HealthCache.fromMangoAccount(group, this); const hc = HealthCache.fromMangoAccount(group, this);
const baseLots = hc.getMaxPerpForHealthRatio( const baseLots = hc.getMaxPerpForHealthRatio(
perpMarket, perpMarket,
pp I80F48.fromNumber(price),
? pp
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
PerpOrderSide.ask, PerpOrderSide.ask,
I80F48.fromNumber(2), I80F48.fromNumber(2),
); );
@ -849,6 +853,7 @@ export class MangoAccount {
group: Group, group: Group,
perpMarketIndex: PerpMarketIndex, perpMarketIndex: PerpMarketIndex,
size: number, size: number,
price: number,
): number { ): number {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const pp = this.getPerpPosition(perpMarket.perpMarketIndex); const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
@ -859,8 +864,9 @@ export class MangoAccount {
pp pp
? pp ? pp
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex), : PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
perpMarket.uiBaseToLots(size),
PerpOrderSide.bid, PerpOrderSide.bid,
perpMarket.uiBaseToLots(size),
I80F48.fromNumber(price),
HealthType.init, HealthType.init,
) )
.toNumber(); .toNumber();
@ -870,6 +876,7 @@ export class MangoAccount {
group: Group, group: Group,
perpMarketIndex: PerpMarketIndex, perpMarketIndex: PerpMarketIndex,
size: number, size: number,
price: number,
): number { ): number {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const pp = this.getPerpPosition(perpMarket.perpMarketIndex); const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
@ -880,8 +887,9 @@ export class MangoAccount {
pp pp
? pp ? pp
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex), : PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
perpMarket.uiBaseToLots(size),
PerpOrderSide.ask, PerpOrderSide.ask,
perpMarket.uiBaseToLots(size),
I80F48.fromNumber(price),
HealthType.init, HealthType.init,
) )
.toNumber(); .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 { public getEntryPrice(perpMarket: PerpMarket): BN {
if (this.basePositionLots.eq(new BN(0))) { if (this.basePositionLots.eq(new BN(0))) {
return new BN(0); return new BN(0);
@ -1237,6 +1246,7 @@ export class PerpPosition {
.abs(); .abs();
} }
// TODO FUTURE: double check with program side code that this is in sycn with latest changes in program
public getBreakEvenPrice(perpMarket: PerpMarket): BN { public getBreakEvenPrice(perpMarket: PerpMarket): BN {
if (this.basePositionLots.eq(new BN(0))) { if (this.basePositionLots.eq(new BN(0))) {
return new BN(0); return new BN(0);

View File

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

View File

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

View File

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

View File

@ -3667,12 +3667,37 @@ export type MangoV4 = {
], ],
"type": "i64" "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", "name": "reserved",
"type": { "type": {
"array": [ "array": [
"u8", "u8",
2136 2120
] ]
} }
} }
@ -4319,10 +4344,17 @@ export type MangoV4 = {
}, },
{ {
"name": "settlePnlLimitFactor", "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" "type": "f32"
}, },
{ {
"name": "settlePnlLimitWindowSizeTs", "name": "settlePnlLimitWindowSizeTs",
"docs": [
"Window size in seconds for the perp settlement limit"
],
"type": "u64" "type": "u64"
}, },
{ {
@ -10894,12 +10926,37 @@ export const IDL: MangoV4 = {
], ],
"type": "i64" "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", "name": "reserved",
"type": { "type": {
"array": [ "array": [
"u8", "u8",
2136 2120
] ]
} }
} }
@ -11546,10 +11603,17 @@ export const IDL: MangoV4 = {
}, },
{ {
"name": "settlePnlLimitFactor", "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" "type": "f32"
}, },
{ {
"name": "settlePnlLimitWindowSizeTs", "name": "settlePnlLimitWindowSizeTs",
"docs": [
"Window size in seconds for the perp settlement limit"
],
"type": "u64" "type": "u64"
}, },
{ {

View File

@ -20,6 +20,7 @@ import { buildVersionedTx } from '../utils';
// * solana airdrop 1 -k ~/.config/solana/admin.json // * solana airdrop 1 -k ~/.config/solana/admin.json
// //
// TODO: switch out with devnet openbook markets
const DEVNET_SERUM3_MARKETS = new Map([ const DEVNET_SERUM3_MARKETS = new Map([
['BTC/USDC', 'DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB'], ['BTC/USDC', 'DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB'],
['SOL/USDC', '5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR'], ['SOL/USDC', '5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR'],
@ -44,6 +45,7 @@ const DEVNET_ORACLES = new Map([
['SRM', '992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs'], ['SRM', '992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs'],
]); ]);
// TODO: should these constants be baked right into client.ts or even program?
const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2; const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2;
const NET_BORROWS_WINDOW_SIZE_TS = 24 * 60 * 60; const NET_BORROWS_WINDOW_SIZE_TS = 24 * 60 * 60;
const NET_BORROWS_LIMIT_NATIVE = 1 * Math.pow(10, 7) * Math.pow(10, 6); 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 { Group } from '../accounts/group';
import { HealthType } from '../accounts/mangoAccount'; import { HealthType } from '../accounts/mangoAccount';
import { PerpOrderSide, PerpOrderType } from '../accounts/perp'; import { PerpOrderSide, PerpOrderType } from '../accounts/perp';
import {
Serum3OrderType,
Serum3SelfTradeBehavior,
Serum3Side,
} from '../accounts/serum3';
import { MangoClient } from '../client'; import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants'; import { MANGO_V4_ID } from '../constants';
import { toUiDecimalsForQuote } from '../utils'; import { toUiDecimalsForQuote } from '../utils';
@ -193,131 +188,132 @@ async function main() {
await mangoAccount.reload(client); await mangoAccount.reload(client);
} }
if (true) { // Note: Disable for now until we have openbook devnet markets
// serum3 // if (true) {
const asks = await group.loadSerum3AsksForMarket( // // serum3
client, // const asks = await group.loadSerum3AsksForMarket(
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, // client,
); // DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
const lowestAsk = Array.from(asks!)[0]; // );
const bids = await group.loadSerum3BidsForMarket( // const lowestAsk = Array.from(asks!)[0];
client, // const bids = await group.loadSerum3BidsForMarket(
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, // client,
); // DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
const highestBid = Array.from(bids!)![0]; // );
// const highestBid = Array.from(bids!)![0];
console.log(`...cancelling all existing serum3 orders`); // console.log(`...cancelling all existing serum3 orders`);
if ( // if (
Array.from(mangoAccount.serum3OosMapByMarketIndex.values()).length > 0 // Array.from(mangoAccount.serum3OosMapByMarketIndex.values()).length > 0
) { // ) {
await client.serum3CancelAllOrders( // await client.serum3CancelAllOrders(
group, // group,
mangoAccount, // mangoAccount,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, // DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
10, // 10,
); // );
} // }
let price = 20; // let price = 20;
let qty = 0.0001; // let qty = 0.0001;
console.log( // console.log(
`...placing serum3 bid which would not be settled since its relatively low then midprice at ${price} for ${qty}`, // `...placing serum3 bid which would not be settled since its relatively low then midprice at ${price} for ${qty}`,
); // );
await client.serum3PlaceOrder( // await client.serum3PlaceOrder(
group, // group,
mangoAccount, // mangoAccount,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, // DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
Serum3Side.bid, // Serum3Side.bid,
price, // price,
qty, // qty,
Serum3SelfTradeBehavior.decrementTake, // Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.limit, // Serum3OrderType.limit,
Date.now(), // Date.now(),
10, // 10,
); // );
await mangoAccount.reload(client); // await mangoAccount.reload(client);
let orders = await mangoAccount.loadSerum3OpenOrdersForMarket( // let orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
client, // client,
group, // group,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, // DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
); // );
expect(orders[0].price).equals(20); // expect(orders[0].price).equals(20);
expect(orders[0].size).equals(qty); // expect(orders[0].size).equals(qty);
price = lowestAsk.price + lowestAsk.price / 2; // price = lowestAsk.price + lowestAsk.price / 2;
qty = 0.0001; // qty = 0.0001;
console.log( // console.log(
`...placing serum3 bid way above midprice at ${price} for ${qty}`, // `...placing serum3 bid way above midprice at ${price} for ${qty}`,
); // );
await client.serum3PlaceOrder( // await client.serum3PlaceOrder(
group, // group,
mangoAccount, // mangoAccount,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, // DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
Serum3Side.bid, // Serum3Side.bid,
price, // price,
qty, // qty,
Serum3SelfTradeBehavior.decrementTake, // Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.limit, // Serum3OrderType.limit,
Date.now(), // Date.now(),
10, // 10,
); // );
await mangoAccount.reload(client); // await mangoAccount.reload(client);
price = highestBid.price - highestBid.price / 2; // price = highestBid.price - highestBid.price / 2;
qty = 0.0001; // qty = 0.0001;
console.log( // console.log(
`...placing serum3 ask way below midprice at ${price} for ${qty}`, // `...placing serum3 ask way below midprice at ${price} for ${qty}`,
); // );
await client.serum3PlaceOrder( // await client.serum3PlaceOrder(
group, // group,
mangoAccount, // mangoAccount,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, // DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
Serum3Side.ask, // Serum3Side.ask,
price, // price,
qty, // qty,
Serum3SelfTradeBehavior.decrementTake, // Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.limit, // Serum3OrderType.limit,
Date.now(), // Date.now(),
10, // 10,
); // );
console.log(`...current own orders on OB`); // console.log(`...current own orders on OB`);
orders = await mangoAccount.loadSerum3OpenOrdersForMarket( // orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
client, // client,
group, // group,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, // DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
); // );
for (const order of orders) { // for (const order of orders) {
console.log( // console.log(
` - order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`, // ` - order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`,
); // );
console.log(` - cancelling order with ${order.orderId}`); // console.log(` - cancelling order with ${order.orderId}`);
await client.serum3CancelOrder( // await client.serum3CancelOrder(
group, // group,
mangoAccount, // mangoAccount,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, // DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask, // order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
order.orderId, // order.orderId,
); // );
} // }
console.log(`...current own orders on OB`); // console.log(`...current own orders on OB`);
orders = await mangoAccount.loadSerum3OpenOrdersForMarket( // orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
client, // client,
group, // group,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, // DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
); // );
for (const order of orders) { // for (const order of orders) {
console.log(order); // console.log(order);
} // }
console.log(`...settling funds`); // console.log(`...settling funds`);
await client.serum3SettleFunds( // await client.serum3SettleFunds(
group, // group,
mangoAccount, // mangoAccount,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, // DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
); // );
} // }
if (true) { if (true) {
await mangoAccount.reload(client); await mangoAccount.reload(client);
@ -505,6 +501,7 @@ async function main() {
const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi( const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi(
group, group,
perpMarket.perpMarketIndex, perpMarket.perpMarketIndex,
perpMarket.uiPrice,
); );
const baseQty = quoteQty / price; const baseQty = quoteQty / price;
console.log( console.log(
@ -512,6 +509,7 @@ async function main() {
group, group,
perpMarket.perpMarketIndex, perpMarket.perpMarketIndex,
baseQty, baseQty,
perpMarket.uiPrice,
)}`, )}`,
); );
console.log( console.log(
@ -554,6 +552,7 @@ async function main() {
mangoAccount.getMaxQuoteForPerpBidUi( mangoAccount.getMaxQuoteForPerpBidUi(
group, group,
perpMarket.perpMarketIndex, perpMarket.perpMarketIndex,
perpMarket.uiPrice,
) * 1.02; ) * 1.02;
const baseQty = quoteQty / price; const baseQty = quoteQty / price;
@ -589,12 +588,14 @@ async function main() {
const baseQty = mangoAccount.getMaxBaseForPerpAskUi( const baseQty = mangoAccount.getMaxBaseForPerpAskUi(
group, group,
perpMarket.perpMarketIndex, perpMarket.perpMarketIndex,
perpMarket.uiPrice,
); );
console.log( console.log(
` simHealthRatioWithPerpAskUiChanges - ${mangoAccount.simHealthRatioWithPerpAskUiChanges( ` simHealthRatioWithPerpAskUiChanges - ${mangoAccount.simHealthRatioWithPerpAskUiChanges(
group, group,
perpMarket.perpMarketIndex, perpMarket.perpMarketIndex,
baseQty, baseQty,
perpMarket.uiPrice,
)}`, )}`,
); );
const quoteQty = baseQty * price; const quoteQty = baseQty * price;
@ -627,8 +628,11 @@ async function main() {
group.banksMapByName.get('BTC')![0].uiPrice! + group.banksMapByName.get('BTC')![0].uiPrice! +
Math.floor(Math.random() * 100); Math.floor(Math.random() * 100);
const baseQty = const baseQty =
mangoAccount.getMaxBaseForPerpAskUi(group, perpMarket.perpMarketIndex) * mangoAccount.getMaxBaseForPerpAskUi(
1.02; group,
perpMarket.perpMarketIndex,
perpMarket.uiPrice,
) * 1.02;
const quoteQty = baseQty * price; const quoteQty = baseQty * price;
console.log( console.log(
`...placing max qty perp ask * 1.02 clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, `...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 // 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/ // 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([ const MAINNET_SERUM3_MARKETS = new Map([
['BTC/USDC', 'A8YFbxQYFVqKZaoYJLLUVcQiWP7G2MeEgW5wsAQgMvFw'], ['BTC/USDC', 'A8YFbxQYFVqKZaoYJLLUVcQiWP7G2MeEgW5wsAQgMvFw'],
['SOL/USDC', '9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT'], ['SOL/USDC', '9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT'],

View File

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