From fa09c557a13ccd64f36ffcee9e240a1c52c01f3d Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Fri, 2 Dec 2022 15:48:43 +0100 Subject: [PATCH] ts client sync with program (#311) mostly health related code --- keeper/src/crank.rs | 1 + programs/mango-v4/src/lib.rs | 2 - programs/mango-v4/src/state/health/cache.rs | 4 +- ts/client/src/accounts/bank.ts | 38 + ts/client/src/accounts/healthCache.spec.ts | 436 ++++++++-- ts/client/src/accounts/healthCache.ts | 769 +++++++++++------- ts/client/src/accounts/mangoAccount.ts | 52 +- ts/client/src/accounts/perp.ts | 10 +- ts/client/src/client.ts | 10 +- ts/client/src/debug-scripts/mb-debug-user.ts | 99 ++- ts/client/src/mango_v4.ts | 68 +- ts/client/src/scripts/devnet-admin.ts | 2 + ts/client/src/scripts/devnet-user.ts | 254 +++--- ts/client/src/scripts/mb-example1-admin.ts | 1 + .../src/scripts/mb-liqtest-make-candidates.ts | 4 +- 15 files changed, 1183 insertions(+), 567 deletions(-) diff --git a/keeper/src/crank.rs b/keeper/src/crank.rs index 9db62af0f..20fd8ebe3 100644 --- a/keeper/src/crank.rs +++ b/keeper/src/crank.rs @@ -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 diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 8f5b4dfd6..7c05c8c79 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -279,8 +279,6 @@ pub mod mango_v4 { /// Serum /// - // TODO deposit/withdraw msrm - pub fn serum3_register_market( ctx: Context, market_index: Serum3MarketIndex, diff --git a/programs/mango-v4/src/state/health/cache.rs b/programs/mango-v4/src/state/health/cache.rs index 29cf7969e..1d1433198 100644 --- a/programs/mango-v4/src/state/health/cache.rs +++ b/programs/mango-v4/src/state/health/cache.rs @@ -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); } } diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index c14b49af8..bb267bee2 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -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); diff --git a/ts/client/src/accounts/healthCache.spec.ts b/ts/client/src/accounts/healthCache.spec.ts index 65f47cb23..b021175ae 100644 --- a/ts/client/src/accounts/healthCache.spec.ts +++ b/ts/client/src/accounts/healthCache.spec.ts @@ -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(); }); }); diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index 9369a15bd..d1f4ac0a9 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -7,6 +7,7 @@ import { I80F48, I80F48Dto, MAX_I80F48, + ONE_I80F48, ZERO_I80F48, } from '../numbers/I80F48'; import { Bank, BankForHealth, TokenIndex } from './bank'; @@ -111,6 +112,53 @@ export class HealthCache { ); } + computeSerum3Reservations(healthType: HealthType): { + tokenMaxReserved: I80F48[]; + serum3Reserved: Serum3Reserved[]; + } { + // For each token, compute the sum of serum-reserved amounts over all markets. + const tokenMaxReserved = new Array(this.tokenInfos.length) + .fill(null) + .map((ignored) => ZERO_I80F48()); + + // For each serum market, compute what happened if reserved_base was converted to quote + // or reserved_quote was converted to base. + const serum3Reserved: Serum3Reserved[] = []; + + for (const info of this.serum3Infos) { + const quote = this.tokenInfos[info.quoteIndex]; + const base = this.tokenInfos[info.baseIndex]; + + const reservedBase = info.reservedBase; + const reservedQuote = info.reservedQuote; + + const quoteAsset = quote.prices.asset(healthType); + const baseLiab = base.prices.liab(healthType); + const allReservedAsBase = reservedBase.add( + reservedQuote.mul(quoteAsset).div(baseLiab), + ); + const baseAsset = base.prices.asset(healthType); + const quoteLiab = quote.prices.liab(healthType); + const allReservedAsQuote = reservedQuote.add( + reservedBase.mul(baseAsset).div(quoteLiab), + ); + + const baseMaxReserved = tokenMaxReserved[info.baseIndex]; + baseMaxReserved.iadd(allReservedAsBase); + const quoteMaxReserved = tokenMaxReserved[info.quoteIndex]; + quoteMaxReserved.iadd(allReservedAsQuote); + + serum3Reserved.push( + new Serum3Reserved(allReservedAsBase, allReservedAsQuote), + ); + } + + return { + tokenMaxReserved: tokenMaxReserved, + serum3Reserved: serum3Reserved, + }; + } + public health(healthType: HealthType): I80F48 { const health = ZERO_I80F48(); for (const tokenInfo of this.tokenInfos) { @@ -118,10 +166,13 @@ export class HealthCache { // console.log(` - ti ${contrib}`); health.iadd(contrib); } - for (const serum3Info of this.serum3Infos) { + const res = this.computeSerum3Reservations(healthType); + for (const [index, serum3Info] of this.serum3Infos.entries()) { const contrib = serum3Info.healthContribution( healthType, this.tokenInfos, + res.tokenMaxReserved, + res.serum3Reserved[index], ); // console.log(` - si ${contrib}`); health.iadd(contrib); @@ -142,10 +193,13 @@ export class HealthCache { // console.log(` - ti ${contrib}`); health.iadd(contrib); } - for (const serum3Info of this.serum3Infos) { + const res = this.computeSerum3Reservations(HealthType.maint); + for (const [index, serum3Info] of this.serum3Infos.entries()) { const contrib = serum3Info.healthContribution( HealthType.maint, this.tokenInfos, + res.tokenMaxReserved, + res.serum3Reserved[index], ); // console.log(` - si ${contrib}`); health.iadd(contrib); @@ -153,7 +207,7 @@ export class HealthCache { for (const perpInfo of this.perpInfos) { if (perpInfo.trustedMarket) { const positiveContrib = perpInfo - .healthContribution(HealthType.maint) + .uncappedHealthContribution(HealthType.maint) .max(ZERO_I80F48()); // console.log(` - pi ${positiveContrib}`); health.iadd(positiveContrib); @@ -170,10 +224,13 @@ export class HealthCache { assets.iadd(contrib); } } - for (const serum3Info of this.serum3Infos) { + const res = this.computeSerum3Reservations(HealthType.maint); + for (const [index, serum3Info] of this.serum3Infos.entries()) { const contrib = serum3Info.healthContribution( healthType, this.tokenInfos, + res.tokenMaxReserved, + res.serum3Reserved[index], ); if (contrib.isPos()) { assets.iadd(contrib); @@ -196,10 +253,13 @@ export class HealthCache { liabs.isub(contrib); } } - for (const serum3Info of this.serum3Infos) { + const res = this.computeSerum3Reservations(HealthType.maint); + for (const [index, serum3Info] of this.serum3Infos.entries()) { const contrib = serum3Info.healthContribution( healthType, this.tokenInfos, + res.tokenMaxReserved, + res.serum3Reserved[index], ); if (contrib.isNeg()) { liabs.isub(contrib); @@ -226,10 +286,13 @@ export class HealthCache { liabs.isub(contrib); } } - for (const serum3Info of this.serum3Infos) { + const res = this.computeSerum3Reservations(HealthType.maint); + for (const [index, serum3Info] of this.serum3Infos.entries()) { const contrib = serum3Info.healthContribution( healthType, this.tokenInfos, + res.tokenMaxReserved, + res.serum3Reserved[index], ); if (contrib.isPos()) { assets.iadd(contrib); @@ -246,6 +309,8 @@ export class HealthCache { } } + // console.log(` - assets ${assets}, liabs ${liabs}`); + if (liabs.gt(I80F48.fromNumber(0.001))) { return HUNDRED_I80F48().mul(assets.sub(liabs).div(liabs)); } else { @@ -280,8 +345,9 @@ export class HealthCache { for (const change of nativeTokenChanges) { const bank: Bank = group.getFirstBankByMint(change.mintPk); const changeIndex = adjustedCache.getOrCreateTokenInfoIndex(bank); - adjustedCache.tokenInfos[changeIndex].balance.iadd( - change.nativeTokenAmount.mul(bank.price), + // TODO: this will no longer work as easily because of the health weight changes + adjustedCache.tokenInfos[changeIndex].balanceNative.iadd( + change.nativeTokenAmount, ); } // HealthCache.logHealthCache('afterChange', adjustedCache); @@ -327,16 +393,11 @@ export class HealthCache { const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank); const baseEntry = this.tokenInfos[baseEntryIndex]; - const reservedAmount = reservedBaseChange.mul(baseEntry.oraclePrice); - const quoteEntry = this.tokenInfos[quoteEntryIndex]; - reservedAmount.iadd(reservedQuoteChange.mul(quoteEntry.oraclePrice)); // Apply it to the tokens - baseEntry.serum3MaxReserved.iadd(reservedAmount); - baseEntry.balance.iadd(freeBaseChange.mul(baseEntry.oraclePrice)); - quoteEntry.serum3MaxReserved.iadd(reservedAmount); - quoteEntry.balance.iadd(freeQuoteChange.mul(quoteEntry.oraclePrice)); + baseEntry.balanceNative.iadd(freeBaseChange); + quoteEntry.balanceNative.iadd(freeQuoteChange); // Apply it to the serum3 info const index = this.getOrCreateSerum3InfoIndex( @@ -345,7 +406,8 @@ export class HealthCache { serum3Market, ); const serum3Info = this.serum3Infos[index]; - serum3Info.reserved = serum3Info.reserved.add(reservedAmount); + serum3Info.reservedBase.iadd(reservedBaseChange); + serum3Info.reservedQuote.iadd(reservedQuoteChange); } simHealthRatioWithSerum3BidChanges( @@ -357,14 +419,13 @@ export class HealthCache { ): I80F48 { const adjustedCache: HealthCache = _.cloneDeep(this); 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.isub( - bidNativeQuoteAmount.mul(quote.oraclePrice), + adjustedCache.tokenInfos[quoteIndex].balanceNative.isub( + bidNativeQuoteAmount, ); // Increase reserved in Serum3Info for quote @@ -389,15 +450,12 @@ export class HealthCache { ): I80F48 { const adjustedCache: HealthCache = _.cloneDeep(this); 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.isub( - askNativeBaseAmount.mul(base.oraclePrice), - ); + adjustedCache.tokenInfos[baseIndex].balanceNative.isub(askNativeBaseAmount); // Increase reserved in Serum3Info for base adjustedCache.adjustSerum3Reserved( @@ -426,66 +484,67 @@ export class HealthCache { return this.findPerpInfoIndex(perpMarket.perpMarketIndex); } - recomputePerpInfo( - perpMarket: PerpMarket, + adjustPerpInfo( perpInfoIndex: number, - clonedExistingPerpPosition: PerpPosition, + price: I80F48, side: PerpOrderSide, newOrderBaseLots: BN, ): void { if (side == PerpOrderSide.bid) { - clonedExistingPerpPosition.bidsBaseLots.iadd(newOrderBaseLots); + this.perpInfos[perpInfoIndex].baseLots.iadd(newOrderBaseLots); + this.perpInfos[perpInfoIndex].quote.isub( + I80F48.fromI64(newOrderBaseLots) + .mul(I80F48.fromI64(this.perpInfos[perpInfoIndex].baseLotSize)) + .mul(price), + ); } else { - clonedExistingPerpPosition.asksBaseLots.iadd(newOrderBaseLots); + this.perpInfos[perpInfoIndex].baseLots.isub(newOrderBaseLots); + this.perpInfos[perpInfoIndex].quote.iadd( + I80F48.fromI64(newOrderBaseLots) + .mul(I80F48.fromI64(this.perpInfos[perpInfoIndex].baseLotSize)) + .mul(price), + ); } - this.perpInfos[perpInfoIndex] = PerpInfo.fromPerpPosition( - perpMarket, - clonedExistingPerpPosition, - ); } simHealthRatioWithPerpOrderChanges( perpMarket: PerpMarket, existingPerpPosition: PerpPosition, - baseLots: BN, side: PerpOrderSide, + baseLots: BN, + price: I80F48, healthType: HealthType = HealthType.init, ): I80F48 { const clonedHealthCache: HealthCache = _.cloneDeep(this); - const clonedExistingPosition: PerpPosition = - _.cloneDeep(existingPerpPosition); const perpInfoIndex = clonedHealthCache.getOrCreatePerpInfoIndex(perpMarket); - clonedHealthCache.recomputePerpInfo( - perpMarket, - perpInfoIndex, - clonedExistingPosition, - side, - baseLots, - ); + clonedHealthCache.adjustPerpInfo(perpInfoIndex, price, side, baseLots); return clonedHealthCache.healthRatio(healthType); } - public static logHealthCache(debug: string, healthCache: HealthCache): void { + public logHealthCache(debug: string): void { if (debug) console.log(debug); - for (const token of healthCache.tokenInfos) { + for (const token of this.tokenInfos) { console.log(` ${token.toString()}`); } - for (const serum3Info of healthCache.serum3Infos) { - console.log(` ${serum3Info.toString(healthCache.tokenInfos)}`); + const res = this.computeSerum3Reservations(HealthType.maint); + for (const [index, serum3Info] of this.serum3Infos.entries()) { + console.log( + ` ${serum3Info.toString( + this.tokenInfos, + res.tokenMaxReserved, + res.serum3Reserved[index], + )}`, + ); } console.log( - ` assets ${healthCache.assets( + ` assets ${this.assets(HealthType.init)}, liabs ${this.liabs( HealthType.init, - )}, liabs ${healthCache.liabs(HealthType.init)}, `, + )}, `, ); + console.log(` health(HealthType.init) ${this.health(HealthType.init)}`); console.log( - ` health(HealthType.init) ${healthCache.health(HealthType.init)}`, - ); - console.log( - ` healthRatio(HealthType.init) ${healthCache.healthRatio( - HealthType.init, - )}`, + ` healthRatio(HealthType.init) ${this.healthRatio(HealthType.init)}`, ); } @@ -495,9 +554,10 @@ export class HealthCache { right: I80F48, rightRatio: I80F48, targetRatio: I80F48, + minStep: I80F48, healthRatioAfterActionFn: (I80F48) => I80F48, ): I80F48 { - const maxIterations = 40; + const maxIterations = 20; const targetError = I80F48.fromNumber(0.1); if ( @@ -514,6 +574,9 @@ export class HealthCache { let newAmount; // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const key of Array(maxIterations).fill(0).keys()) { + if (right.sub(left).abs().lt(minStep)) { + return left; + } newAmount = left.add(right).mul(I80F48.fromNumber(0.5)); const newAmountRatio = healthRatioAfterActionFn(newAmount); const error = newAmountRatio.sub(targetRatio); @@ -522,11 +585,20 @@ export class HealthCache { } if (newAmountRatio.gt(targetRatio) != rightRatio.gt(targetRatio)) { left = newAmount; + leftRatio = newAmountRatio; } else { right = newAmount; rightRatio = newAmountRatio; } + // console.log( + // ` -- ${left.toNumber().toFixed(3)} (${leftRatio + // .toNumber() + // .toFixed(3)}) ${right.toNumber().toFixed(3)} (${rightRatio + // .toNumber() + // .toFixed(3)})`, + // ); } + console.error( `Unable to get targetRatio within ${maxIterations} iterations`, ); @@ -536,8 +608,8 @@ export class HealthCache { getMaxSourceForTokenSwap( sourceBank: BankForHealth, targetBank: BankForHealth, + price: I80F48, minRatio: I80F48, - priceFactor: I80F48, ): I80F48 { if ( sourceBank.initLiabWeight @@ -563,22 +635,28 @@ export class HealthCache { return ZERO_I80F48(); } - // If the price is sufficiently good, then health will just increase from swapping: - // once we've swapped enough, swapping x reduces health by x * source_liab_weight and - // increases it by x * target_asset_weight * price_factor. - const finalHealthSlope = sourceBank.initLiabWeight - .neg() - .add(targetBank.initAssetWeight.mul(priceFactor)); - if (finalHealthSlope.gte(ZERO_I80F48())) { - return MAX_I80F48(); - } - const healthCacheClone: HealthCache = _.cloneDeep(this); const sourceIndex = healthCacheClone.getOrCreateTokenInfoIndex(sourceBank); const targetIndex = healthCacheClone.getOrCreateTokenInfoIndex(targetBank); const source = healthCacheClone.tokenInfos[sourceIndex]; const target = healthCacheClone.tokenInfos[targetIndex]; + // If the price is sufficiently good, then health will just increase from swapping: + // once we've swapped enough, swapping x reduces health by x * source_liab_weight and + // increases it by x * target_asset_weight * price_factor. + const finalHealthSlope = source.initLiabWeight + .neg() + .mul(source.prices.liab(HealthType.init)) + .add( + target.initAssetWeight + .mul(target.prices.asset(HealthType.init)) + .mul(price), + ); + + if (finalHealthSlope.gte(ZERO_I80F48())) { + return MAX_I80F48(); + } + // There are two key slope changes: Assume source.balance > 0 and target.balance < 0. Then // initially health ratio goes up. When one of balances flips sign, the health ratio slope // may be positive or negative for a bit, until both balances have flipped and the slope is @@ -587,12 +665,12 @@ export class HealthCache { function cacheAfterSwap(amount: I80F48): HealthCache { const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone); - // HealthCache.logHealthCache('beforeSwap', adjustedCache); - adjustedCache.tokenInfos[sourceIndex].balance.isub(amount); - adjustedCache.tokenInfos[targetIndex].balance.iadd( - amount.mul(priceFactor), + // adjustedCache.logHealthCache('beforeSwap', adjustedCache); + adjustedCache.tokenInfos[sourceIndex].balanceNative.isub(amount); + adjustedCache.tokenInfos[targetIndex].balanceNative.iadd( + amount.mul(price), ); - // HealthCache.logHealthCache('afterSwap', adjustedCache); + // adjustedCache.logHealthCache('afterSwap', adjustedCache); return adjustedCache; } @@ -600,19 +678,24 @@ export class HealthCache { return cacheAfterSwap(amount).healthRatio(HealthType.init); } + function healthAfterSwap(amount: I80F48): I80F48 { + return cacheAfterSwap(amount).health(HealthType.init); + } + // There are two key slope changes: Assume source.balance > 0 and target.balance < 0. // When these values flip sign, the health slope decreases, but could still be positive. // After point1 it's definitely negative (due to finalHealthSlope check above). // The maximum health ratio will be at 0 or at one of these points (ignoring serum3 effects). - const sourceForZeroTargetBalance = target.balance.neg().div(priceFactor); - const point0Amount = source.balance + const sourceForZeroTargetBalance = target.balanceNative.neg().div(price); + const point0Amount = source.balanceNative .min(sourceForZeroTargetBalance) .max(ZERO_I80F48()); - const point1Amount = source.balance + const point1Amount = source.balanceNative .max(sourceForZeroTargetBalance) .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); @@ -648,12 +731,15 @@ export class HealthCache { point1Health.div(finalHealthSlope), ); const zeroHealthRatio = healthRatioAfterSwap(zeroHealthAmount); + const zeroHealth = healthAfterSwap(zeroHealthAmount); + amount = HealthCache.binaryApproximationSearch( point1Amount, point1Ratio, zeroHealthAmount, zeroHealthRatio, minRatio, + ZERO_I80F48(), healthRatioAfterSwap, ); } else if (point0Ratio.gte(minRatio)) { @@ -664,6 +750,7 @@ export class HealthCache { point1Amount, point1Ratio, minRatio, + ZERO_I80F48(), healthRatioAfterSwap, ); } else { @@ -674,11 +761,12 @@ export class HealthCache { point0Amount, point0Ratio, minRatio, + ZERO_I80F48(), healthRatioAfterSwap, ); } - return amount.div(source.oraclePrice); + return amount; } getMaxSerum3OrderForHealthRatio( @@ -714,10 +802,12 @@ export class HealthCache { // and when its a bid, then quote->bid let zeroAmount; if (side == Serum3Side.ask) { - const quoteBorrows = quote.balance.lt(ZERO_I80F48()) - ? quote.balance.abs() + const quoteBorrows = quote.balanceNative.lt(ZERO_I80F48()) + ? quote.balanceNative.abs().mul(quote.prices.liab(HealthType.init)) : ZERO_I80F48(); - const max = base.balance.max(quoteBorrows); + const max = base.balanceNative + .mul(base.prices.asset(HealthType.init)) + .max(quoteBorrows); zeroAmount = max.add( initialHealth .add(max.mul(quote.initLiabWeight.sub(base.initAssetWeight))) @@ -728,10 +818,12 @@ export class HealthCache { ), ); } else { - const baseBorrows = base.balance.lt(ZERO_I80F48()) - ? base.balance.abs() + const baseBorrows = base.balanceNative.lt(ZERO_I80F48()) + ? base.balanceNative.abs().mul(base.prices.liab(HealthType.init)) : ZERO_I80F48(); - const max = quote.balance.max(baseBorrows); + const max = quote.balanceNative + .mul(quote.prices.asset(HealthType.init)) + .max(baseBorrows); zeroAmount = max.add( initialHealth .add(max.mul(base.initLiabWeight.sub(quote.initAssetWeight))) @@ -750,20 +842,30 @@ export class HealthCache { function cacheAfterPlacingOrder(amount: I80F48): HealthCache { const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone); - + // adjustedCache.logHealthCache(` before placing order ${amount}`); + // TODO: there should also be some issue with oracle vs stable price here; + // probably better to pass in not the quote amount but the base or quote native amount side === Serum3Side.ask - ? adjustedCache.tokenInfos[baseIndex].balance.isub(amount) - : adjustedCache.tokenInfos[quoteIndex].balance.isub(amount); - + ? adjustedCache.tokenInfos[baseIndex].balanceNative.isub( + amount.div(base.prices.oracle), + ) + : adjustedCache.tokenInfos[quoteIndex].balanceNative.isub( + amount.div(quote.prices.oracle), + ); adjustedCache.adjustSerum3Reserved( baseBank, quoteBank, serum3Market, - side === Serum3Side.ask ? amount.div(base.oraclePrice) : ZERO_I80F48(), + side === Serum3Side.ask + ? amount.div(base.prices.oracle) + : ZERO_I80F48(), ZERO_I80F48(), - side === Serum3Side.bid ? amount.div(quote.oraclePrice) : ZERO_I80F48(), + side === Serum3Side.bid + ? amount.div(quote.prices.oracle) + : ZERO_I80F48(), ZERO_I80F48(), ); + // adjustedCache.logHealthCache(' after placing order'); return adjustedCache; } @@ -778,6 +880,7 @@ export class HealthCache { zeroAmount, zeroAmountRatio, minRatio, + ONE_I80F48(), healthRatioAfterPlacingOrder, ); @@ -786,7 +889,7 @@ export class HealthCache { getMaxPerpForHealthRatio( perpMarket: PerpMarket, - existingPerpPosition: PerpPosition, + price, side: PerpOrderSide, minRatio: I80F48, ): I80F48 { @@ -801,46 +904,41 @@ export class HealthCache { const perpInfoIndex = healthCacheClone.getOrCreatePerpInfoIndex(perpMarket); const perpInfo = healthCacheClone.perpInfos[perpInfoIndex]; - const oraclePrice = perpInfo.oraclePrice; + const prices = perpInfo.prices; const baseLotSize = I80F48.fromI64(perpMarket.baseLotSize); // If the price is sufficiently good then health will just increase from trading const finalHealthSlope = direction == 1 - ? perpInfo.initAssetWeight.mul(oraclePrice).sub(oraclePrice) - : oraclePrice.sub(perpInfo.initLiabWeight.mul(oraclePrice)); + ? perpInfo.initAssetWeight.mul(prices.asset(HealthType.init)).sub(price) + : price.sub(perpInfo.initLiabWeight.mul(prices.liab(HealthType.init))); if (finalHealthSlope.gte(ZERO_I80F48())) { return MAX_I80F48(); } - function cacheAfterPlaceOrder(baseLots: BN): HealthCache { + function cacheAfterTrade(baseLots: BN): HealthCache { const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone); - const adjustedExistingPerpPosition: PerpPosition = - _.cloneDeep(existingPerpPosition); - adjustedCache.recomputePerpInfo( - perpMarket, - perpInfoIndex, - adjustedExistingPerpPosition, - side, - baseLots, - ); + // adjustedCache.logHealthCache(' -- before trade'); + adjustedCache.adjustPerpInfo(perpInfoIndex, price, side, baseLots); + // adjustedCache.logHealthCache(' -- after trade'); return adjustedCache; } function healthAfterTrade(baseLots: I80F48): I80F48 { - return cacheAfterPlaceOrder(new BN(baseLots.toNumber())).health( + return cacheAfterTrade(new BN(baseLots.toNumber())).health( HealthType.init, ); } function healthRatioAfterTrade(baseLots: I80F48): I80F48 { - return cacheAfterPlaceOrder(new BN(baseLots.toNumber())).healthRatio( + return cacheAfterTrade(new BN(baseLots.toNumber())).healthRatio( HealthType.init, ); } + function healthRatioAfterTradeTrunc(baseLots: I80F48): I80F48 { + return healthRatioAfterTrade(baseLots.floor()); + } - const initialBaseLots = perpInfo.base - .div(perpInfo.oraclePrice) - .div(baseLotSize); + const initialBaseLots = I80F48.fromU64(perpInfo.baseLots); // There are two cases: // 1. We are increasing abs(baseLots) @@ -868,21 +966,36 @@ export class HealthCache { } } else if (case1StartRatio.gte(minRatio)) { // Must reach minRatio to the right of case1Start - const case1StartHealth = healthAfterTrade(case1Start); - if (case1StartHealth.lte(ZERO_I80F48())) { + + // Need to figure out how many lots to trade to reach zero health (zero_health_amount). + // We do this by looking at the starting health and the health slope per + // traded base lot (final_health_slope). + const startCache = cacheAfterTrade(new BN(case1Start.toNumber())); + const startHealth = startCache.health(HealthType.init); + if (startHealth.lte(ZERO_I80F48())) { return ZERO_I80F48(); } - const zeroHealthAmount = case1Start.sub( - case1StartHealth.div(finalHealthSlope).div(baseLotSize), - ); - const zeroHealthRatio = healthRatioAfterTrade(zeroHealthAmount); + + // The perp market's contribution to the health above may be capped. But we need to trade + // enough to fully reduce any positive-pnl buffer. Thus get the uncapped health: + const perpInfo = startCache.perpInfos[perpInfoIndex]; + const startHealthUncapped = startHealth + .sub(perpInfo.healthContribution(HealthType.init)) + .add(perpInfo.uncappedHealthContribution(HealthType.init)); + + const zeroHealthAmount = case1Start + .sub(startHealthUncapped.div(finalHealthSlope).div(baseLotSize)) + .add(ONE_I80F48()); + const zeroHealthRatio = healthRatioAfterTradeTrunc(zeroHealthAmount); + baseLots = HealthCache.binaryApproximationSearch( case1Start, case1StartRatio, zeroHealthAmount, zeroHealthRatio, minRatio, - healthRatioAfterTrade, + ONE_I80F48(), + healthRatioAfterTradeTrunc, ); } else { // Between 0 and case1Start @@ -892,7 +1005,8 @@ export class HealthCache { case1Start, case1StartRatio, minRatio, - healthRatioAfterTrade, + ONE_I80F48(), + healthRatioAfterTradeTrunc, ); } @@ -900,6 +1014,24 @@ export class HealthCache { } } +export class Prices { + constructor(public oracle: I80F48, public stable: I80F48) {} + + public liab(healthType: HealthType): I80F48 { + if (healthType == HealthType.maint) { + return this.oracle; + } + return this.oracle.max(this.stable); + } + + public asset(healthType: HealthType): I80F48 { + if (healthType == HealthType.maint) { + return this.oracle; + } + return this.oracle.min(this.stable); + } +} + export class TokenInfo { constructor( public tokenIndex: TokenIndex, @@ -907,12 +1039,8 @@ export class TokenInfo { public initAssetWeight: I80F48, public maintLiabWeight: I80F48, public initLiabWeight: I80F48, - // native/native - public oraclePrice: I80F48, - // in health-reference-token native units - public balance: I80F48, - // in health-reference-token native units - public serum3MaxReserved: I80F48, + public prices: Prices, + public balanceNative: I80F48, ) {} static fromDto(dto: TokenInfoDto): TokenInfo { @@ -922,26 +1050,26 @@ export class TokenInfo { I80F48.from(dto.initAssetWeight), I80F48.from(dto.maintLiabWeight), I80F48.from(dto.initLiabWeight), - I80F48.from(dto.oraclePrice), - I80F48.from(dto.balance), - I80F48.from(dto.serum3MaxReserved), + new Prices( + I80F48.from(dto.prices.oracle), + I80F48.from(dto.prices.stable), + ), + I80F48.from(dto.balanceNative), ); } - static fromBank( - bank: BankForHealth, - nativeBalance?: I80F48, - serum3MaxReserved?: I80F48, - ): TokenInfo { + static fromBank(bank: BankForHealth, nativeBalance?: I80F48): TokenInfo { return new TokenInfo( bank.tokenIndex, bank.maintAssetWeight, bank.initAssetWeight, bank.maintLiabWeight, bank.initLiabWeight, - bank.price, - nativeBalance ? nativeBalance.mul(bank.price) : ZERO_I80F48(), - serum3MaxReserved ? serum3MaxReserved : ZERO_I80F48(), + new Prices( + bank.price, + I80F48.fromNumber(bank.stablePriceModel.stablePrice), + ), + nativeBalance ? nativeBalance : ZERO_I80F48(), ); } @@ -958,25 +1086,35 @@ export class TokenInfo { } healthContribution(healthType: HealthType): I80F48 { - return ( - this.balance.isNeg() - ? this.liabWeight(healthType) - : this.assetWeight(healthType) - ).mul(this.balance); + let weight, price; + if (this.balanceNative.isNeg()) { + weight = this.liabWeight(healthType); + price = this.prices.liab(healthType); + } else { + weight = this.assetWeight(healthType); + price = this.prices.asset(healthType); + } + return this.balanceNative.mul(weight).mul(price); } toString(): string { - return ` tokenIndex: ${this.tokenIndex}, balance: ${ - this.balance - }, serum3MaxReserved: ${ - this.serum3MaxReserved + return ` tokenIndex: ${this.tokenIndex}, balanceNative: ${ + this.balanceNative }, initHealth ${this.healthContribution(HealthType.init)}`; } } +export class Serum3Reserved { + constructor( + public allReservedAsBase: I80F48, + public allReservedAsQuote: I80F48, + ) {} +} + export class Serum3Info { constructor( - public reserved: I80F48, + public reservedBase: I80F48, + public reservedQuote: I80F48, public baseIndex: number, public quoteIndex: number, public marketIndex: MarketIndex, @@ -984,7 +1122,8 @@ export class Serum3Info { static fromDto(dto: Serum3InfoDto): Serum3Info { return new Serum3Info( - I80F48.from(dto.reserved), + I80F48.from(dto.reservedBase), + I80F48.from(dto.reservedQuote), dto.baseIndex, dto.quoteIndex, dto.marketIndex as MarketIndex, @@ -997,6 +1136,7 @@ export class Serum3Info { quoteEntryIndex: number, ): Serum3Info { return new Serum3Info( + ZERO_I80F48(), ZERO_I80F48(), baseEntryIndex, quoteEntryIndex, @@ -1012,90 +1152,117 @@ export class Serum3Info { marketIndex: MarketIndex, oo: OpenOrders, ): Serum3Info { - // add the amounts that are freely settleable + // add the amounts that are freely settleable immediately to token balances const baseFree = I80F48.fromI64(oo.baseTokenFree); // NOTE: referrerRebatesAccrued is not declared on oo class, but the layout // is aware of it const quoteFree = I80F48.fromI64( oo.quoteTokenFree.add((oo as any).referrerRebatesAccrued), ); - baseInfo.balance.iadd(baseFree.mul(baseInfo.oraclePrice)); - quoteInfo.balance.iadd(quoteFree.mul(quoteInfo.oraclePrice)); + baseInfo.balanceNative.iadd(baseFree); + quoteInfo.balanceNative.iadd(quoteFree); - // add the reserved amount to both sides, to have the worst-case covered + // track the reserved amounts const reservedBase = I80F48.fromI64( oo.baseTokenTotal.sub(oo.baseTokenFree), ); const reservedQuote = I80F48.fromI64( oo.quoteTokenTotal.sub(oo.quoteTokenFree), ); - const reservedBalance = reservedBase - .mul(baseInfo.oraclePrice) - .add(reservedQuote.mul(quoteInfo.oraclePrice)); - baseInfo.serum3MaxReserved.iadd(reservedBalance); - quoteInfo.serum3MaxReserved.iadd(reservedBalance); - return new Serum3Info(reservedBalance, baseIndex, quoteIndex, marketIndex); + return new Serum3Info( + reservedBase, + reservedQuote, + baseIndex, + quoteIndex, + marketIndex, + ); } - healthContribution(healthType: HealthType, tokenInfos: TokenInfo[]): I80F48 { - const baseInfo = tokenInfos[this.baseIndex]; - const quoteInfo = tokenInfos[this.quoteIndex]; - const reserved = this.reserved; - // console.log(` - reserved ${reserved}`); - // console.log(` - this.baseIndex ${this.baseIndex}`); - // console.log(` - this.quoteIndex ${this.quoteIndex}`); - - if (reserved.isZero()) { + healthContribution( + healthType: HealthType, + tokenInfos: TokenInfo[], + tokenMaxReserved: I80F48[], + marketReserved: Serum3Reserved, + ): I80F48 { + if ( + marketReserved.allReservedAsBase.isZero() || + marketReserved.allReservedAsQuote.isZero() + ) { return ZERO_I80F48(); } + const baseInfo = tokenInfos[this.baseIndex]; + const quoteInfo = tokenInfos[this.quoteIndex]; + const baseMaxReserved = tokenMaxReserved[this.baseIndex]; + const quoteMaxReserved = tokenMaxReserved[this.quoteIndex]; + // How much the health would increase if the reserved balance were applied to the passed // token info? - const computeHealthEffect = function (tokenInfo: TokenInfo): I80F48 { + const computeHealthEffect = function ( + tokenInfo: TokenInfo, + tokenMaxReserved: I80F48, + marketReserved: I80F48, + ): I80F48 { // This balance includes all possible reserved funds from markets that relate to the - // token, including this market itself: `reserved` is already included in `max_balance`. - const maxBalance = tokenInfo.balance.add(tokenInfo.serum3MaxReserved); + // token, including this market itself: `tokenMaxReserved` is already included in `maxBalance`. + const maxBalance = tokenInfo.balanceNative.add(tokenMaxReserved); // Assuming `reserved` was added to `max_balance` last (because that gives the smallest // health effects): how much did health change because of it? let assetPart, liabPart; - if (maxBalance.gte(reserved)) { - assetPart = reserved; + if (maxBalance.gte(marketReserved)) { + assetPart = marketReserved; liabPart = ZERO_I80F48(); } else if (maxBalance.isNeg()) { assetPart = ZERO_I80F48(); - liabPart = reserved; + liabPart = marketReserved; } else { assetPart = maxBalance; - liabPart = reserved.sub(maxBalance); + liabPart = marketReserved.sub(maxBalance); } const assetWeight = tokenInfo.assetWeight(healthType); const liabWeight = tokenInfo.liabWeight(healthType); + const assetPrice = tokenInfo.prices.asset(healthType); + const liabPrice = tokenInfo.prices.liab(healthType); - // console.log(` - tokenInfo.index ${tokenInfo.tokenIndex}`); - // console.log(` - tokenInfo.balance ${tokenInfo.balance}`); - // console.log( - // ` - tokenInfo.serum3MaxReserved ${tokenInfo.serum3MaxReserved}`, - // ); - // console.log(` - assetPart ${assetPart}`); - // console.log(` - liabPart ${liabPart}`); - return assetWeight.mul(assetPart).add(liabWeight.mul(liabPart)); + return assetWeight + .mul(assetPart) + .mul(assetPrice) + .add(liabWeight.mul(liabPart).mul(liabPrice)); }; - const reservedAsBase = computeHealthEffect(baseInfo); - const reservedAsQuote = computeHealthEffect(quoteInfo); - // console.log(` - reservedAsBase ${reservedAsBase}`); - // console.log(` - reservedAsQuote ${reservedAsQuote}`); - return reservedAsBase.min(reservedAsQuote); + const healthBase = computeHealthEffect( + baseInfo, + baseMaxReserved, + marketReserved.allReservedAsBase, + ); + const healthQuote = computeHealthEffect( + quoteInfo, + quoteMaxReserved, + marketReserved.allReservedAsQuote, + ); + + return healthBase.min(healthQuote); } - toString(tokenInfos: TokenInfo[]): string { + toString( + tokenInfos: TokenInfo[], + tokenMaxReserved: I80F48[], + marketReserved: Serum3Reserved, + ): string { return ` marketIndex: ${this.marketIndex}, baseIndex: ${ this.baseIndex - }, quoteIndex: ${this.quoteIndex}, reserved: ${ - this.reserved - }, initHealth ${this.healthContribution(HealthType.init, tokenInfos)}`; + }, quoteIndex: ${this.quoteIndex}, reservedBase: ${ + this.reservedBase + }, reservedQuote: ${ + this.reservedQuote + }, initHealth ${this.healthContribution( + HealthType.init, + tokenInfos, + tokenMaxReserved, + marketReserved, + )}`; } } @@ -1106,9 +1273,12 @@ export class PerpInfo { public initAssetWeight: I80F48, public maintLiabWeight: I80F48, public initLiabWeight: I80F48, - public base: I80F48, + public baseLotSize: BN, + public baseLots: BN, + public bidsBaseLots: BN, + public asksBaseLots: BN, public quote: I80F48, - public oraclePrice: I80F48, + public prices: Prices, public hasOpenOrders: boolean, public trustedMarket: boolean, ) {} @@ -1120,9 +1290,15 @@ export class PerpInfo { I80F48.from(dto.initAssetWeight), I80F48.from(dto.maintLiabWeight), I80F48.from(dto.initLiabWeight), - I80F48.from(dto.base), + dto.baseLotSize, + dto.baseLots, + dto.bidsBaseLots, + dto.asksBaseLots, I80F48.from(dto.quote), - I80F48.from(dto.oraclePrice), + new Prices( + I80F48.from(dto.prices.oracle), + I80F48.from(dto.prices.stable), + ), dto.hasOpenOrders, dto.trustedMarket, ); @@ -1132,11 +1308,9 @@ export class PerpInfo { perpMarket: PerpMarket, perpPosition: PerpPosition, ): PerpInfo { - const baseLotSize = I80F48.fromI64(perpMarket.baseLotSize); - const baseLots = I80F48.fromI64( - perpPosition.basePositionLots.add(perpPosition.takerBaseLots), + const baseLots = perpPosition.basePositionLots.add( + perpPosition.takerBaseLots, ); - const unsettledFunding = perpPosition.getUnsettledFunding(perpMarket); const takerQuote = I80F48.fromI64( @@ -1146,109 +1320,87 @@ export class PerpInfo { .sub(unsettledFunding) .add(takerQuote); - // Two scenarios: - // 1. The price goes low and all bids execute, converting to base. - // That means the perp position is increased by `bids` and the quote position - // is decreased by `bids * baseLotSize * price`. - // The health for this case is: - // (weighted(baseLots + bids) - bids) * baseLotSize * price + quote - // 2. The price goes high and all asks execute, converting to quote. - // The health for this case is: - // (weighted(baseLots - asks) + asks) * baseLotSize * price + quote - // - // Comparing these makes it clear we need to pick the worse subfactor - // weighted(baseLots + bids) - bids =: scenario1 - // or - // weighted(baseLots - asks) + asks =: scenario2 - // - // Additionally, we want this scenario choice to be the same no matter whether we're - // computing init or maint health. This can be guaranteed by requiring the weights - // to satisfy the property (P): - // - // (1 - initAssetWeight) / (initLiabWeight - 1) - // == (1 - maintAssetWeight) / (maintLiabWeight - 1) - // - // Derivation: - // Set asksNetLots := baseLots - asks, bidsNetLots := baseLots + bids. - // Now - // scenario1 = weighted(bidsNetLots) - bidsNetLots + baseLots and - // scenario2 = weighted(asksNetLots) - asksNetLots + baseLots - // So with expanding weigthed(a) = weightFactorForA * a, the question - // scenario1 < scenario2 - // becomes: - // (weightFactorForBidsNetLots - 1) * bidsNetLots - // < (weightFactorForAsksNetLots - 1) * asksNetLots - // Since asksNetLots < 0 and bidsNetLots > 0 is the only interesting case, (P) follows. - // - // We satisfy (P) by requiring - // assetWeight = 1 - x and liabWeight = 1 + x - // - // And with that assumption the scenario choice condition further simplifies to: - // scenario1 < scenario2 - // iff abs(bidsNetLots) > abs(asksNetLots) - - const bidsNetLots = baseLots.add(I80F48.fromI64(perpPosition.bidsBaseLots)); - const asksNetLots = baseLots.sub(I80F48.fromI64(perpPosition.asksBaseLots)); - - const lotsToQuote = baseLotSize.mul(perpMarket.price); - - let base, quote; - if (bidsNetLots.abs().gt(asksNetLots.abs())) { - const bidsBaseLots = I80F48.fromI64(perpPosition.bidsBaseLots); - base = bidsNetLots.mul(lotsToQuote); - quote = quoteCurrent.sub(bidsBaseLots.mul(lotsToQuote)); - } else { - const asksBaseLots = I80F48.fromI64(perpPosition.asksBaseLots); - base = asksNetLots.mul(lotsToQuote); - quote = quoteCurrent.add(asksBaseLots.mul(lotsToQuote)); - } - - // console.log(`bidsNetLots ${bidsNetLots}`); - // console.log(`asksNetLots ${asksNetLots}`); - // console.log(`quoteCurrent ${quoteCurrent}`); - // console.log(`base ${base}`); - // console.log(`quote ${quote}`); - return new PerpInfo( perpMarket.perpMarketIndex, perpMarket.maintAssetWeight, perpMarket.initAssetWeight, perpMarket.maintLiabWeight, perpMarket.initLiabWeight, - base, - quote, - perpMarket.price, + perpMarket.baseLotSize, + baseLots, + perpPosition.bidsBaseLots, + perpPosition.asksBaseLots, + quoteCurrent, + new Prices( + perpMarket.price, + I80F48.fromNumber(perpMarket.stablePriceModel.stablePrice), + ), perpPosition.hasOpenOrders(), perpMarket.trustedMarket, ); } healthContribution(healthType: HealthType): I80F48 { - let weight; - if (healthType == HealthType.init && this.base.isNeg()) { - weight = this.initLiabWeight; - } else if (healthType == HealthType.init && !this.base.isNeg()) { - weight = this.initAssetWeight; - } - if (healthType == HealthType.maint && this.base.isNeg()) { - weight = this.maintLiabWeight; - } - if (healthType == HealthType.maint && !this.base.isNeg()) { - weight = this.maintAssetWeight; + return this.trustedMarket + ? this.uncappedHealthContribution(healthType) + : this.uncappedHealthContribution(healthType).min(ZERO_I80F48()); + } + + uncappedHealthContribution(healthType: HealthType): I80F48 { + function orderExecutionCase( + pi: PerpInfo, + ordersBaseLots: BN, + orderPrice: I80F48, + ): I80F48 { + const netBaseNative = I80F48.fromU64( + pi.baseLots.add(ordersBaseLots).mul(pi.baseLotSize), + ); + + let weight, basePrice; + if (healthType == HealthType.init) { + if (netBaseNative.isNeg()) { + weight = pi.initLiabWeight; + basePrice = pi.prices.liab(healthType); + } else { + weight = pi.initAssetWeight; + basePrice = pi.prices.asset(healthType); + } + } else { + if (netBaseNative.isNeg()) { + weight = pi.maintLiabWeight; + basePrice = pi.prices.liab(healthType); + } else { + weight = pi.maintAssetWeight; + basePrice = pi.prices.asset(healthType); + } + } + + // Total value of the order-execution adjusted base position + const baseHealth = netBaseNative.mul(weight).mul(basePrice); + + const ordersBaseNative = I80F48.fromU64( + ordersBaseLots.mul(pi.baseLotSize), + ); + // The quote change from executing the bids/asks + const orderQuote = ordersBaseNative.neg().mul(orderPrice); + + return baseHealth.add(orderQuote); } - // console.log(` - this.quote ${this.quote}`); - // console.log(` - weight ${weight}`); - // console.log(` - this.base ${this.base}`); - // console.log(` - weight.mul(this.base) ${weight.mul(this.base)}`); + // What is worse: Executing all bids at oracle_price.liab, or executing all asks at oracle_price.asset? + const bidsCase = orderExecutionCase( + this, + this.bidsBaseLots, + this.prices.liab(healthType), + ); + const asksCase = orderExecutionCase( + this, + this.asksBaseLots.neg(), + this.prices.asset(healthType), + ); + const worstCase = bidsCase.min(asksCase); - const uncappedHealthContribution = this.quote.add(weight.mul(this.base)); - // console.log(` - uncappedHealthContribution ${uncappedHealthContribution}`); - if (this.trustedMarket) { - return uncappedHealthContribution; - } else { - return uncappedHealthContribution.min(ZERO_I80F48()); - } + return this.quote.add(worstCase); } static emptyFromPerpMarket(perpMarket: PerpMarket): PerpInfo { @@ -1258,9 +1410,15 @@ export class PerpInfo { perpMarket.initAssetWeight, perpMarket.maintLiabWeight, perpMarket.initLiabWeight, + perpMarket.baseLotSize, + new BN(0), + new BN(0), + new BN(0), ZERO_I80F48(), - ZERO_I80F48(), - perpMarket.price, + new Prices( + perpMarket.price, + I80F48.fromNumber(perpMarket.stablePriceModel.stablePrice), + ), false, perpMarket.trustedMarket, ); @@ -1268,10 +1426,12 @@ export class PerpInfo { toString(): string { return ` perpMarketIndex: ${this.perpMarketIndex}, base: ${ - this.base + this.baseLots }, quote: ${this.quote}, oraclePrice: ${ - this.oraclePrice - }, initHealth ${this.healthContribution(HealthType.init)}`; + this.prices.oracle + }, uncapped health contribution ${this.uncappedHealthContribution( + HealthType.init, + )}`; } } @@ -1286,11 +1446,8 @@ export class TokenInfoDto { initAssetWeight: I80F48Dto; maintLiabWeight: I80F48Dto; initLiabWeight: I80F48Dto; - oraclePrice: I80F48Dto; // native/native - // in health-reference-token native units - balance: I80F48Dto; - // in health-reference-token native units - serum3MaxReserved: I80F48Dto; + prices: { oracle: I80F48Dto; stable: I80F48Dto }; + balanceNative: I80F48Dto; constructor( tokenIndex: number, @@ -1298,29 +1455,34 @@ export class TokenInfoDto { initAssetWeight: I80F48Dto, maintLiabWeight: I80F48Dto, initLiabWeight: I80F48Dto, - oraclePrice: I80F48Dto, - balance: I80F48Dto, - serum3MaxReserved: I80F48Dto, + prices: { oracle: I80F48Dto; stable: I80F48Dto }, + balanceNative: 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; + this.prices = prices; + this.balanceNative = balanceNative; } } export class Serum3InfoDto { - reserved: I80F48Dto; + reservedBase: I80F48Dto; + reservedQuote: I80F48Dto; baseIndex: number; quoteIndex: number; marketIndex: number; - constructor(reserved: I80F48Dto, baseIndex: number, quoteIndex: number) { - this.reserved = reserved; + constructor( + reservedBase: I80F48Dto, + reservedQuote: I80F48Dto, + baseIndex: number, + quoteIndex: number, + ) { + this.reservedBase = reservedBase; + this.reservedQuote = reservedQuote; this.baseIndex = baseIndex; this.quoteIndex = quoteIndex; } @@ -1332,9 +1494,12 @@ export class PerpInfoDto { initAssetWeight: I80F48Dto; maintLiabWeight: I80F48Dto; initLiabWeight: I80F48Dto; - base: I80F48Dto; + public baseLotSize: BN; + public baseLots: BN; + public bidsBaseLots: BN; + public asksBaseLots: BN; quote: I80F48Dto; - oraclePrice: I80F48Dto; + prices: { oracle: I80F48Dto; stable: I80F48Dto }; hasOpenOrders: boolean; trustedMarket: boolean; } diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index c44041aa9..950c31b6d 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -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); diff --git a/ts/client/src/accounts/perp.ts b/ts/client/src/accounts/perp.ts index 37f28fc91..a83a70e79 100644 --- a/ts/client/src/accounts/perp.ts +++ b/ts/client/src/accounts/perp.ts @@ -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); diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 60a634fed..829e84199 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -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, ) diff --git a/ts/client/src/debug-scripts/mb-debug-user.ts b/ts/client/src/debug-scripts/mb-debug-user.ts index 159affef7..3ebd2927b 100644 --- a/ts/client/src/debug-scripts/mb-debug-user.ts +++ b/ts/client/src/debug-scripts/mb-debug-user.ts @@ -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( diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 96e8bb892..1f591cff3 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -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" }, { diff --git a/ts/client/src/scripts/devnet-admin.ts b/ts/client/src/scripts/devnet-admin.ts index e7028f18c..e7f6ac5a1 100644 --- a/ts/client/src/scripts/devnet-admin.ts +++ b/ts/client/src/scripts/devnet-admin.ts @@ -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); diff --git a/ts/client/src/scripts/devnet-user.ts b/ts/client/src/scripts/devnet-user.ts index c8401d55e..8228983b0 100644 --- a/ts/client/src/scripts/devnet-user.ts +++ b/ts/client/src/scripts/devnet-user.ts @@ -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}`, diff --git a/ts/client/src/scripts/mb-example1-admin.ts b/ts/client/src/scripts/mb-example1-admin.ts index 029972e81..82bc665b8 100644 --- a/ts/client/src/scripts/mb-example1-admin.ts +++ b/ts/client/src/scripts/mb-example1-admin.ts @@ -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'], diff --git a/ts/client/src/scripts/mb-liqtest-make-candidates.ts b/ts/client/src/scripts/mb-liqtest-make-candidates.ts index 4e3da5e35..c4f38dbdc 100644 --- a/ts/client/src/scripts/mb-liqtest-make-candidates.ts +++ b/ts/client/src/scripts/mb-liqtest-make-candidates.ts @@ -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,