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