mango-v4/ts/client/src/accounts/healthCache.spec.ts

993 lines
28 KiB
TypeScript

import { BN } from '@coral-xyz/anchor';
import { OpenOrders } from '@project-serum/serum';
import { expect } from 'chai';
import cloneDeep from 'lodash/cloneDeep';
import range from 'lodash/range';
import { I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { BankForHealth, StablePriceModel, TokenIndex } from './bank';
import { HealthCache, PerpInfo, Serum3Info, TokenInfo } from './healthCache';
import { HealthType, PerpPosition } from './mangoAccount';
import { PerpMarket, PerpOrderSide } from './perp';
import { MarketIndex } from './serum3';
function mockBankAndOracle(
tokenIndex: TokenIndex,
maintWeight: number,
initWeight: number,
price: number,
stablePrice: number,
): BankForHealth {
return {
tokenIndex,
maintAssetWeight: I80F48.fromNumber(1 - maintWeight),
initAssetWeight: I80F48.fromNumber(1 - initWeight),
maintLiabWeight: I80F48.fromNumber(1 + maintWeight),
initLiabWeight: I80F48.fromNumber(1 + initWeight),
price: I80F48.fromNumber(price),
stablePriceModel: { stablePrice: stablePrice } as StablePriceModel,
scaledInitAssetWeight: () => I80F48.fromNumber(1 - initWeight),
scaledInitLiabWeight: () => I80F48.fromNumber(1 + initWeight),
};
}
function mockPerpMarket(
perpMarketIndex: number,
maintBaseWeight: number,
initBaseWeight: number,
baseLotSize: number,
price: number,
): PerpMarket {
return {
perpMarketIndex,
maintBaseAssetWeight: I80F48.fromNumber(1 - maintBaseWeight),
initBaseAssetWeight: I80F48.fromNumber(1 - initBaseWeight),
maintBaseLiabWeight: I80F48.fromNumber(1 + maintBaseWeight),
initBaseLiabWeight: I80F48.fromNumber(1 + initBaseWeight),
maintOverallAssetWeight: I80F48.fromNumber(1 - 0.02),
initOverallAssetWeight: I80F48.fromNumber(1 - 0.05),
price: I80F48.fromNumber(price),
stablePriceModel: { stablePrice: price } as StablePriceModel,
quoteLotSize: new BN(100),
baseLotSize: new BN(baseLotSize),
longFunding: ZERO_I80F48(),
shortFunding: ZERO_I80F48(),
} as unknown as PerpMarket;
}
describe('Health Cache', () => {
it('test_health0', () => {
const sourceBank: BankForHealth = mockBankAndOracle(
1 as TokenIndex,
0.1,
0.2,
1,
1,
);
const targetBank: BankForHealth = mockBankAndOracle(
4 as TokenIndex,
0.3,
0.5,
5,
5,
);
const ti1 = TokenInfo.fromBank(sourceBank, I80F48.fromNumber(100));
const ti2 = TokenInfo.fromBank(targetBank, I80F48.fromNumber(-10));
const si1 = Serum3Info.fromOoModifyingTokenInfos(
1,
ti2,
0,
ti1,
2 as MarketIndex,
{
quoteTokenTotal: new BN(21),
baseTokenTotal: new BN(18),
quoteTokenFree: new BN(1),
baseTokenFree: new BN(3),
referrerRebatesAccrued: new BN(2),
} as any as OpenOrders,
);
const pM = mockPerpMarket(9, 0.1, 0.2, 10, targetBank.price.toNumber());
const pp = new PerpPosition(
pM.perpMarketIndex,
0,
new BN(0),
new BN(3),
I80F48.fromNumber(-310),
new BN(0),
ZERO_I80F48(),
ZERO_I80F48(),
new BN(7),
new BN(11),
new BN(1),
new BN(2),
0,
0,
new BN(0),
new BN(0),
new BN(0),
0,
ZERO_I80F48(),
ZERO_I80F48(),
new BN(0),
ZERO_I80F48(),
);
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
const hc = new HealthCache([ti1, ti2], [si1], [pi1]);
// for bank1/oracle1, including open orders (scenario: bids execute)
const health1 = (100.0 + 1.0 + 2.0 + (20.0 + 15.0 * 5.0)) * 0.8;
// for bank2/oracle2
const health2 = (-10.0 + 3.0) * 5.0 * 1.5;
// for perp (scenario: bids execute)
const health3 =
(3.0 + 7.0 + 1.0) * 10.0 * 5.0 * 0.8 +
(-310.0 + 2.0 * 100.0 - 7.0 * 10.0 * 5.0);
const health = hc.health(HealthType.init).toNumber();
console.log(
` - health ${health
.toFixed(3)
.padStart(
10,
)}, case "test that includes all the side values (like referrer_rebates_accrued)"`,
);
expect(health - (health1 + health2 + health3)).lessThan(0.0000001);
});
it('test_health1', (done) => {
function testFixture(fixture: {
name: string;
token1: number;
token2: number;
token3: number;
oo12: [number, number];
oo13: [number, number];
perp1: [number, number, number, number];
expectedHealth: number;
}): void {
const bank1: BankForHealth = mockBankAndOracle(
1 as TokenIndex,
0.1,
0.2,
1,
1,
);
const bank2: BankForHealth = mockBankAndOracle(
4 as TokenIndex,
0.3,
0.5,
5,
5,
);
const bank3: BankForHealth = mockBankAndOracle(
5 as TokenIndex,
0.3,
0.5,
10,
10,
);
const ti1 = TokenInfo.fromBank(bank1, I80F48.fromNumber(fixture.token1));
const ti2 = TokenInfo.fromBank(bank2, I80F48.fromNumber(fixture.token2));
const ti3 = TokenInfo.fromBank(bank3, I80F48.fromNumber(fixture.token3));
const si1 = Serum3Info.fromOoModifyingTokenInfos(
1,
ti2,
0,
ti1,
2 as MarketIndex,
{
quoteTokenTotal: new BN(fixture.oo12[0]),
baseTokenTotal: new BN(fixture.oo12[1]),
quoteTokenFree: new BN(0),
baseTokenFree: new BN(0),
referrerRebatesAccrued: new BN(0),
} as any as OpenOrders,
);
const si2 = Serum3Info.fromOoModifyingTokenInfos(
2,
ti3,
0,
ti1,
2 as MarketIndex,
{
quoteTokenTotal: new BN(fixture.oo13[0]),
baseTokenTotal: new BN(fixture.oo13[1]),
quoteTokenFree: new BN(0),
baseTokenFree: new BN(0),
referrerRebatesAccrued: new BN(0),
} as any as OpenOrders,
);
const pM = mockPerpMarket(9, 0.1, 0.2, 10, bank2.price.toNumber());
const pp = new PerpPosition(
pM.perpMarketIndex,
0,
new BN(0),
new BN(fixture.perp1[0]),
I80F48.fromNumber(fixture.perp1[1]),
new BN(0),
ZERO_I80F48(),
ZERO_I80F48(),
new BN(fixture.perp1[2]),
new BN(fixture.perp1[3]),
new BN(0),
new BN(0),
0,
0,
new BN(0),
new BN(0),
new BN(0),
0,
ZERO_I80F48(),
ZERO_I80F48(),
new BN(0),
ZERO_I80F48(),
);
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
const hc = new HealthCache([ti1, ti2, ti3], [si1, si2], [pi1]);
const health = hc.health(HealthType.init).toNumber();
console.log(
` - case "${fixture.name}" health ${health
.toFixed(3)
.padStart(10)}, expected ${fixture.expectedHealth}`,
);
expect(health - fixture.expectedHealth).lessThan(0.0000001);
}
const basePrice = 5;
const baseLotsToQuote = 10.0 * basePrice;
testFixture({
name: '0',
token1: 100,
token2: -10,
token3: 0,
oo12: [20, 15],
oo13: [0, 0],
perp1: [3, -131, 7, 11],
expectedHealth:
// for token1, including open orders (scenario: bids execute)
(100.0 + (20.0 + 15.0 * basePrice)) * 0.8 -
// for token2
10.0 * basePrice * 1.5 +
// for perp (scenario: bids execute)
(3.0 + 7.0) * baseLotsToQuote * 0.8 +
(-131.0 - 7.0 * baseLotsToQuote),
});
testFixture({
name: '1',
token1: -100,
token2: 10,
token3: 0,
oo12: [20, 15],
oo13: [0, 0],
perp1: [-10, -131, 7, 11],
expectedHealth:
// for token1
-100.0 * 1.2 +
// for token2, including open orders (scenario: asks execute)
(10.0 * basePrice + (20.0 + 15.0 * basePrice)) * 0.5 +
// for perp (scenario: asks execute)
(-10.0 - 11.0) * baseLotsToQuote * 1.2 +
(-131.0 + 11.0 * baseLotsToQuote),
});
testFixture({
name: '2: weighted positive perp pnl',
token1: 0,
token2: 0,
token3: 0,
oo12: [0, 0],
oo13: [0, 0],
perp1: [-1, 100, 0, 0],
expectedHealth: 0.95 * (100.0 - 1.2 * 1.0 * baseLotsToQuote),
});
testFixture({
name: '3: negative perp pnl is not weighted',
token1: 0,
token2: 0,
token3: 0,
oo12: [0, 0],
oo13: [0, 0],
perp1: [1, -100, 0, 0],
expectedHealth: -100.0 + 0.8 * 1.0 * baseLotsToQuote,
});
testFixture({
name: '4: perp health',
token1: 0,
token2: 0,
token3: 0,
oo12: [0, 0],
oo13: [0, 0],
perp1: [10, 100, 0, 0],
expectedHealth: 0.95 * (100.0 + 0.8 * 10.0 * baseLotsToQuote),
});
testFixture({
name: '5: perp health',
token1: 0,
token2: 0,
token3: 0,
oo12: [0, 0],
oo13: [0, 0],
perp1: [30, -100, 0, 0],
expectedHealth: 0.95 * (-100.0 + 0.8 * 30.0 * baseLotsToQuote),
});
testFixture({
name: '6, reserved oo funds',
token1: -100,
token2: -10,
token3: -10,
oo12: [1, 1],
oo13: [1, 1],
perp1: [0, 0, 0, 0],
expectedHealth:
// tokens
-100.0 * 1.2 -
10.0 * 5.0 * 1.5 -
10.0 * 10.0 * 1.5 +
// oo_1_2 (-> token1)
(1.0 + 5.0) * 1.2 +
// oo_1_3 (-> token1)
(1.0 + 10.0) * 1.2,
});
testFixture({
name: '7, reserved oo funds cross the zero balance level',
token1: -14,
token2: -10,
token3: -10,
oo12: [1, 1],
oo13: [1, 1],
perp1: [0, 0, 0, 0],
expectedHealth:
-14.0 * 1.2 -
10.0 * 5.0 * 1.5 -
10.0 * 10.0 * 1.5 +
// oo_1_2 (-> token1)
3.0 * 1.2 +
3.0 * 0.8 +
// oo_1_3 (-> token1)
8.0 * 1.2 +
3.0 * 0.8,
});
testFixture({
name: '8, reserved oo funds in a non-quote currency',
token1: -100,
token2: -100,
token3: -1,
oo12: [0, 0],
oo13: [10, 1],
perp1: [0, 0, 0, 0],
expectedHealth:
// tokens
-100.0 * 1.2 -
100.0 * 5.0 * 1.5 -
10.0 * 1.5 +
// oo_1_3 (-> token3)
10.0 * 1.5 +
10.0 * 0.5,
});
testFixture({
name: '9, like 8 but oo_1_2 flips the oo_1_3 target',
token1: -100,
token2: -100,
token3: -1,
oo12: [100, 0],
oo13: [10, 1],
perp1: [0, 0, 0, 0],
expectedHealth:
// tokens
-100.0 * 1.2 -
100.0 * 5.0 * 1.5 -
10.0 * 1.5 +
// oo_1_2 (-> token1)
80.0 * 1.2 +
20.0 * 0.8 +
// oo_1_3 (-> token1)
20.0 * 0.8,
});
done();
});
it('test_max_swap', (done) => {
const b0 = mockBankAndOracle(0 as TokenIndex, 0.1, 0.1, 2, 2);
const b1 = mockBankAndOracle(1 as TokenIndex, 0.2, 0.2, 3, 3);
const b2 = mockBankAndOracle(2 as TokenIndex, 0.3, 0.3, 4, 4);
const banks = [b0, b1, b2];
const hc = new HealthCache(
[
TokenInfo.fromBank(b0, I80F48.fromNumber(0)),
TokenInfo.fromBank(b1, I80F48.fromNumber(0)),
TokenInfo.fromBank(b2, I80F48.fromNumber(0)),
],
[],
[],
);
expect(
hc
.getMaxSwapSourceForHealthRatio(
b0,
b1,
I80F48.fromNumber(2 / 3),
I80F48.fromNumber(50),
)
.toNumber(),
).lessThan(0.0000001);
function findMaxSwapActual(
hc: HealthCache,
source: TokenIndex,
target: TokenIndex,
minValue: number,
priceFactor: number,
maxSwapFn: (HealthCache) => I80F48,
): number[] {
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.getMaxSwapSourceForHealthFn(
banks[source],
banks[target],
swapPrice,
I80F48.fromNumber(minValue),
maxSwapFn,
);
function valueForAmount(amount: I80F48): I80F48 {
// adjust token balance
const clonedHcClone: HealthCache = cloneDeep(clonedHc);
clonedHc.tokenInfos[source].balanceNative.isub(amount);
clonedHc.tokenInfos[target].balanceNative.iadd(amount.mul(swapPrice));
return maxSwapFn(clonedHcClone);
}
return [
sourceAmount.toNumber(),
valueForAmount(sourceAmount).toNumber(),
valueForAmount(sourceAmount.sub(ONE_I80F48())).toNumber(),
valueForAmount(sourceAmount.add(ONE_I80F48())).toNumber(),
];
}
function checkMaxSwapResult(
hc: HealthCache,
source: TokenIndex,
target: TokenIndex,
minValue: number,
priceFactor: number,
maxSwapFn: (HealthCache) => I80F48,
): void {
const [sourceAmount, actualValue, minusValue, plusValue] =
findMaxSwapActual(hc, source, target, minValue, priceFactor, maxSwapFn);
console.log(
` -- checking ${source} to ${target} for priceFactor: ${priceFactor}, target: ${minValue} actual: ${minusValue}/${actualValue}/${plusValue}, amount: ${sourceAmount}`,
);
if (actualValue < minValue) {
// check that swapping more would decrease the ratio!
expect(plusValue < actualValue);
} else {
expect(actualValue >= minValue);
// either we're within tolerance of the target, or swapping 1 more would
// bring us below the target
expect(actualValue < minValue + 1 || plusValue < minValue);
}
}
function maxSwapFnRatio(hc: HealthCache): I80F48 {
return hc.healthRatio(HealthType.init);
}
function maxSwapFn(hc: HealthCache): I80F48 {
return hc.health(HealthType.init);
}
for (const fn of [maxSwapFn, maxSwapFnRatio]) {
{
console.log(' - test 0');
// adjust by usdc
const clonedHc: HealthCache = 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,
fn,
);
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
2 as TokenIndex,
target,
priceFactor,
fn,
);
}
}
// At this unlikely price it's healthy to swap infinitely
expect(function () {
findMaxSwapActual(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
50.0,
1.5,
fn,
);
}).to.throw('Number out of range');
}
{
console.log(' - test 1');
const clonedHc: HealthCache = 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,
fn,
);
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
2 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
2 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
fn,
);
}
}
}
{
console.log(' - test 2');
const clonedHc: HealthCache = 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,
maxSwapFn,
);
}
{
console.log(' - test 3');
const clonedHc: HealthCache = 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).toNumber();
const [amount, actualRatio] = findMaxSwapActual(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
100,
1,
maxSwapFn,
);
expect(actualRatio / 2.0 > initRatio);
expect(amount - 100 / 3).lessThan(1);
}
{
console.log(' - test 4');
const clonedHc: HealthCache = 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,
maxSwapFn,
);
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
3,
1,
maxSwapFn,
);
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
4,
1,
maxSwapFn,
);
}
// TODO test 5
{
console.log(' - test 6');
const clonedHc: HealthCache = cloneDeep(hc);
clonedHc.serum3Infos = [
new Serum3Info(
I80F48.fromNumber(30 / 3),
I80F48.fromNumber(30 / 2),
1,
0,
0 as MarketIndex,
),
];
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(-40).div(clonedHc.tokenInfos[1].prices.oracle),
);
clonedHc.tokenInfos[2].balanceNative.iadd(
I80F48.fromNumber(120).div(clonedHc.tokenInfos[2].prices.oracle),
);
for (const priceFactor of [0.9, 1.1]) {
for (const target of range(1, 100, 1)) {
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
2 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
2 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
2 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
2 as TokenIndex,
1 as TokenIndex,
target,
priceFactor,
fn,
);
}
}
}
{
// check starting with negative health but swapping can make it positive
console.log(' - test 7');
const clonedHc: HealthCache = 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(20).div(clonedHc.tokenInfos[1].prices.oracle),
);
expect(clonedHc.health(HealthType.init).toNumber() < 0);
for (const priceFactor of [0.9, 1.1]) {
for (const target of range(1, 100, 1)) {
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
fn,
);
}
}
}
{
// check starting with negative health but swapping can't make it positive
console.log(' - test 8');
const clonedHc: HealthCache = 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(10).div(clonedHc.tokenInfos[1].prices.oracle),
);
expect(clonedHc.health(HealthType.init).toNumber() < 0);
for (const priceFactor of [0.9, 1.1]) {
for (const target of range(1, 100, 1)) {
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
fn,
);
}
}
}
{
// swap some assets into a zero-asset-weight token
console.log(' - test 9');
const clonedHc: HealthCache = cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(10).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].initAssetWeight = ZERO_I80F48();
expect(
findMaxSwapActual(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
1,
1,
maxSwapFn,
)[0] > 0,
);
for (const priceFactor of [0.9, 1.1]) {
for (const target of range(1, 100, 1)) {
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
target,
priceFactor,
fn,
);
}
}
}
}
done();
});
it('test_max_perp', (done) => {
const baseLotSize = 100;
const b0 = mockBankAndOracle(0 as TokenIndex, 0.0, 0.0, 1, 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(
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();
});
});