parent
9bfa574ee6
commit
fa09c557a1
|
@ -27,6 +27,7 @@ pub async fn runner(
|
||||||
.context
|
.context
|
||||||
.tokens
|
.tokens
|
||||||
.keys()
|
.keys()
|
||||||
|
// TODO: grouping tokens whose oracle might have less confidencen e.g. ORCA with the rest, fails whole ix
|
||||||
// TokenUpdateIndexAndRate is known to take max 71k cu
|
// TokenUpdateIndexAndRate is known to take max 71k cu
|
||||||
// from cargo test-bpf local tests
|
// from cargo test-bpf local tests
|
||||||
// chunk size of 8 seems to be max before encountering "VersionedTransaction too large" issues
|
// chunk size of 8 seems to be max before encountering "VersionedTransaction too large" issues
|
||||||
|
|
|
@ -279,8 +279,6 @@ pub mod mango_v4 {
|
||||||
/// Serum
|
/// Serum
|
||||||
///
|
///
|
||||||
|
|
||||||
// TODO deposit/withdraw msrm
|
|
||||||
|
|
||||||
pub fn serum3_register_market(
|
pub fn serum3_register_market(
|
||||||
ctx: Context<Serum3RegisterMarket>,
|
ctx: Context<Serum3RegisterMarket>,
|
||||||
market_index: Serum3MarketIndex,
|
market_index: Serum3MarketIndex,
|
||||||
|
|
|
@ -579,7 +579,9 @@ impl HealthCache {
|
||||||
|
|
||||||
for perp_info in self.perp_infos.iter() {
|
for perp_info in self.perp_infos.iter() {
|
||||||
if perp_info.trusted_market {
|
if perp_info.trusted_market {
|
||||||
let positive_contrib = perp_info.health_contribution(health_type).max(I80F48::ZERO);
|
let positive_contrib = perp_info
|
||||||
|
.uncapped_health_contribution(health_type)
|
||||||
|
.max(I80F48::ZERO);
|
||||||
cm!(health += positive_contrib);
|
cm!(health += positive_contrib);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,18 @@ export type OracleConfig = {
|
||||||
reserved: number[];
|
reserved: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type StablePriceModel = {
|
||||||
|
stablePrice: number;
|
||||||
|
lastUpdateTimestamp: BN;
|
||||||
|
delayPrices: number[];
|
||||||
|
delayAccumulatorPrice: number;
|
||||||
|
delayAccumulatorTime: number;
|
||||||
|
delayIntervalSeconds: number;
|
||||||
|
delayGrowthLimit: number;
|
||||||
|
stableGrowthLimit: number;
|
||||||
|
lastDelayIntervalIndex: number;
|
||||||
|
};
|
||||||
|
|
||||||
export interface BankForHealth {
|
export interface BankForHealth {
|
||||||
tokenIndex: TokenIndex;
|
tokenIndex: TokenIndex;
|
||||||
maintAssetWeight: I80F48;
|
maintAssetWeight: I80F48;
|
||||||
|
@ -21,6 +33,7 @@ export interface BankForHealth {
|
||||||
maintLiabWeight: I80F48;
|
maintLiabWeight: I80F48;
|
||||||
initLiabWeight: I80F48;
|
initLiabWeight: I80F48;
|
||||||
price: I80F48;
|
price: I80F48;
|
||||||
|
stablePriceModel: StablePriceModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Bank implements BankForHealth {
|
export class Bank implements BankForHealth {
|
||||||
|
@ -53,6 +66,7 @@ export class Bank implements BankForHealth {
|
||||||
static from(
|
static from(
|
||||||
publicKey: PublicKey,
|
publicKey: PublicKey,
|
||||||
obj: {
|
obj: {
|
||||||
|
// TODO: rearrange fields to have same order as in bank.rs
|
||||||
group: PublicKey;
|
group: PublicKey;
|
||||||
name: number[];
|
name: number[];
|
||||||
mint: PublicKey;
|
mint: PublicKey;
|
||||||
|
@ -88,6 +102,14 @@ export class Bank implements BankForHealth {
|
||||||
tokenIndex: number;
|
tokenIndex: number;
|
||||||
mintDecimals: number;
|
mintDecimals: number;
|
||||||
bankNum: number;
|
bankNum: number;
|
||||||
|
stablePriceModel: StablePriceModel;
|
||||||
|
minVaultToDepositsRatio: number;
|
||||||
|
netBorrowsWindowSizeTs: BN;
|
||||||
|
lastNetBorrowsWindowStartTs: BN;
|
||||||
|
netBorrowsLimitQuote: BN;
|
||||||
|
netBorrowsInWindow: BN;
|
||||||
|
borrowLimitQuote: number;
|
||||||
|
collateralLimitQuote: number;
|
||||||
},
|
},
|
||||||
): Bank {
|
): Bank {
|
||||||
return new Bank(
|
return new Bank(
|
||||||
|
@ -127,6 +149,14 @@ export class Bank implements BankForHealth {
|
||||||
obj.tokenIndex as TokenIndex,
|
obj.tokenIndex as TokenIndex,
|
||||||
obj.mintDecimals,
|
obj.mintDecimals,
|
||||||
obj.bankNum,
|
obj.bankNum,
|
||||||
|
obj.stablePriceModel,
|
||||||
|
obj.minVaultToDepositsRatio,
|
||||||
|
obj.netBorrowsWindowSizeTs,
|
||||||
|
obj.lastNetBorrowsWindowStartTs,
|
||||||
|
obj.netBorrowsLimitQuote,
|
||||||
|
obj.netBorrowsInWindow,
|
||||||
|
obj.borrowLimitQuote,
|
||||||
|
obj.collateralLimitQuote,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,6 +197,14 @@ export class Bank implements BankForHealth {
|
||||||
public tokenIndex: TokenIndex,
|
public tokenIndex: TokenIndex,
|
||||||
public mintDecimals: number,
|
public mintDecimals: number,
|
||||||
public bankNum: number,
|
public bankNum: number,
|
||||||
|
public stablePriceModel: StablePriceModel,
|
||||||
|
minVaultToDepositsRatio: number,
|
||||||
|
netBorrowsWindowSizeTs: BN,
|
||||||
|
lastNetBorrowsWindowStartTs: BN,
|
||||||
|
netBorrowsLimitQuote: BN,
|
||||||
|
netBorrowsInWindow: BN,
|
||||||
|
borrowLimitQuote: number,
|
||||||
|
collateralLimitQuote: number,
|
||||||
) {
|
) {
|
||||||
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
||||||
this.depositIndex = I80F48.from(depositIndex);
|
this.depositIndex = I80F48.from(depositIndex);
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { BN } from '@project-serum/anchor';
|
import { BN } from '@project-serum/anchor';
|
||||||
import { OpenOrders } from '@project-serum/serum';
|
import { OpenOrders } from '@project-serum/serum';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
import _ from 'lodash';
|
||||||
import { I80F48, ZERO_I80F48 } from '../numbers/I80F48';
|
import { I80F48, ZERO_I80F48 } from '../numbers/I80F48';
|
||||||
import { toUiDecimalsForQuote } from '../utils';
|
import { BankForHealth, StablePriceModel, TokenIndex } from './bank';
|
||||||
import { BankForHealth, TokenIndex } from './bank';
|
|
||||||
import { HealthCache, PerpInfo, Serum3Info, TokenInfo } from './healthCache';
|
import { HealthCache, PerpInfo, Serum3Info, TokenInfo } from './healthCache';
|
||||||
import { HealthType, PerpPosition } from './mangoAccount';
|
import { HealthType, PerpPosition } from './mangoAccount';
|
||||||
import { PerpMarket } from './perp';
|
import { PerpMarket, PerpOrderSide } from './perp';
|
||||||
import { MarketIndex } from './serum3';
|
import { MarketIndex } from './serum3';
|
||||||
|
|
||||||
function mockBankAndOracle(
|
function mockBankAndOracle(
|
||||||
|
@ -22,6 +22,7 @@ function mockBankAndOracle(
|
||||||
maintLiabWeight: I80F48.fromNumber(1 + maintWeight),
|
maintLiabWeight: I80F48.fromNumber(1 + maintWeight),
|
||||||
initLiabWeight: I80F48.fromNumber(1 + initWeight),
|
initLiabWeight: I80F48.fromNumber(1 + initWeight),
|
||||||
price: I80F48.fromNumber(price),
|
price: I80F48.fromNumber(price),
|
||||||
|
stablePriceModel: { stablePrice: price } as StablePriceModel,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +30,8 @@ function mockPerpMarket(
|
||||||
perpMarketIndex: number,
|
perpMarketIndex: number,
|
||||||
maintWeight: number,
|
maintWeight: number,
|
||||||
initWeight: number,
|
initWeight: number,
|
||||||
price: I80F48,
|
baseLotSize: number,
|
||||||
|
price: number,
|
||||||
): PerpMarket {
|
): PerpMarket {
|
||||||
return {
|
return {
|
||||||
perpMarketIndex,
|
perpMarketIndex,
|
||||||
|
@ -37,9 +39,10 @@ function mockPerpMarket(
|
||||||
initAssetWeight: I80F48.fromNumber(1 - initWeight),
|
initAssetWeight: I80F48.fromNumber(1 - initWeight),
|
||||||
maintLiabWeight: I80F48.fromNumber(1 + maintWeight),
|
maintLiabWeight: I80F48.fromNumber(1 + maintWeight),
|
||||||
initLiabWeight: I80F48.fromNumber(1 + initWeight),
|
initLiabWeight: I80F48.fromNumber(1 + initWeight),
|
||||||
price,
|
price: I80F48.fromNumber(price),
|
||||||
|
stablePriceModel: { stablePrice: price } as StablePriceModel,
|
||||||
quoteLotSize: new BN(100),
|
quoteLotSize: new BN(100),
|
||||||
baseLotSize: new BN(10),
|
baseLotSize: new BN(baseLotSize),
|
||||||
longFunding: ZERO_I80F48(),
|
longFunding: ZERO_I80F48(),
|
||||||
shortFunding: ZERO_I80F48(),
|
shortFunding: ZERO_I80F48(),
|
||||||
} as unknown as PerpMarket;
|
} as unknown as PerpMarket;
|
||||||
|
@ -78,7 +81,7 @@ describe('Health Cache', () => {
|
||||||
} as any as OpenOrders,
|
} as any as OpenOrders,
|
||||||
);
|
);
|
||||||
|
|
||||||
const pM = mockPerpMarket(9, 0.1, 0.2, targetBank.price);
|
const pM = mockPerpMarket(9, 0.1, 0.2, 10, targetBank.price.toNumber());
|
||||||
const pp = new PerpPosition(
|
const pp = new PerpPosition(
|
||||||
pM.perpMarketIndex,
|
pM.perpMarketIndex,
|
||||||
new BN(3),
|
new BN(3),
|
||||||
|
@ -112,7 +115,7 @@ describe('Health Cache', () => {
|
||||||
|
|
||||||
const health = hc.health(HealthType.init).toNumber();
|
const health = hc.health(HealthType.init).toNumber();
|
||||||
console.log(
|
console.log(
|
||||||
`health ${health
|
` - health ${health
|
||||||
.toFixed(3)
|
.toFixed(3)
|
||||||
.padStart(
|
.padStart(
|
||||||
10,
|
10,
|
||||||
|
@ -122,7 +125,7 @@ describe('Health Cache', () => {
|
||||||
expect(health - (health1 + health2 + health3)).lessThan(0.0000001);
|
expect(health - (health1 + health2 + health3)).lessThan(0.0000001);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('test_health1', () => {
|
it('test_health1', (done) => {
|
||||||
function testFixture(fixture: {
|
function testFixture(fixture: {
|
||||||
name: string;
|
name: string;
|
||||||
token1: number;
|
token1: number;
|
||||||
|
@ -186,17 +189,17 @@ describe('Health Cache', () => {
|
||||||
} as any as OpenOrders,
|
} as any as OpenOrders,
|
||||||
);
|
);
|
||||||
|
|
||||||
const pM = mockPerpMarket(9, 0.1, 0.2, bank2.price);
|
const pM = mockPerpMarket(9, 0.1, 0.2, 10, bank2.price.toNumber());
|
||||||
const pp = new PerpPosition(
|
const pp = new PerpPosition(
|
||||||
pM.perpMarketIndex,
|
pM.perpMarketIndex,
|
||||||
new BN(fixture.perp1[0]),
|
new BN(fixture.perp1[0]),
|
||||||
I80F48.fromNumber(fixture.perp1[1]),
|
I80F48.fromNumber(fixture.perp1[1]),
|
||||||
|
new BN(0),
|
||||||
|
new BN(0),
|
||||||
|
I80F48.fromNumber(0),
|
||||||
|
I80F48.fromNumber(0),
|
||||||
new BN(fixture.perp1[2]),
|
new BN(fixture.perp1[2]),
|
||||||
new BN(fixture.perp1[3]),
|
new BN(fixture.perp1[3]),
|
||||||
I80F48.fromNumber(0),
|
|
||||||
I80F48.fromNumber(0),
|
|
||||||
new BN(0),
|
|
||||||
new BN(0),
|
|
||||||
new BN(0),
|
new BN(0),
|
||||||
new BN(0),
|
new BN(0),
|
||||||
0,
|
0,
|
||||||
|
@ -210,7 +213,7 @@ describe('Health Cache', () => {
|
||||||
const hc = new HealthCache([ti1, ti2, ti3], [si1, si2], [pi1]);
|
const hc = new HealthCache([ti1, ti2, ti3], [si1, si2], [pi1]);
|
||||||
const health = hc.health(HealthType.init).toNumber();
|
const health = hc.health(HealthType.init).toNumber();
|
||||||
console.log(
|
console.log(
|
||||||
`health ${health.toFixed(3).padStart(10)}, case "${fixture.name}"`,
|
` - case "${fixture.name}" health ${health.toFixed(3).padStart(10)}`,
|
||||||
);
|
);
|
||||||
expect(health - fixture.expectedHealth).lessThan(0.0000001);
|
expect(health - fixture.expectedHealth).lessThan(0.0000001);
|
||||||
}
|
}
|
||||||
|
@ -374,76 +377,361 @@ describe('Health Cache', () => {
|
||||||
// oo_1_3 (-> token1)
|
// oo_1_3 (-> token1)
|
||||||
20.0 * 0.8,
|
20.0 * 0.8,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('max swap tokens for min ratio', () => {
|
it('test_max_swap', (done) => {
|
||||||
// USDC like
|
const b0 = mockBankAndOracle(0 as TokenIndex, 0.1, 0.1, 2);
|
||||||
const sourceBank: BankForHealth = {
|
const b1 = mockBankAndOracle(1 as TokenIndex, 0.2, 0.2, 3);
|
||||||
tokenIndex: 0 as TokenIndex,
|
const b2 = mockBankAndOracle(2 as TokenIndex, 0.3, 0.3, 4);
|
||||||
maintAssetWeight: I80F48.fromNumber(1),
|
const banks = [b0, b1, b2];
|
||||||
initAssetWeight: I80F48.fromNumber(1),
|
|
||||||
maintLiabWeight: I80F48.fromNumber(1),
|
|
||||||
initLiabWeight: I80F48.fromNumber(1),
|
|
||||||
price: I80F48.fromNumber(1),
|
|
||||||
};
|
|
||||||
// BTC like
|
|
||||||
const targetBank: BankForHealth = {
|
|
||||||
tokenIndex: 1 as TokenIndex,
|
|
||||||
maintAssetWeight: I80F48.fromNumber(0.9),
|
|
||||||
initAssetWeight: I80F48.fromNumber(0.8),
|
|
||||||
maintLiabWeight: I80F48.fromNumber(1.1),
|
|
||||||
initLiabWeight: I80F48.fromNumber(1.2),
|
|
||||||
price: I80F48.fromNumber(20000),
|
|
||||||
};
|
|
||||||
|
|
||||||
const hc = new HealthCache(
|
const hc = new HealthCache(
|
||||||
[
|
[
|
||||||
new TokenInfo(
|
TokenInfo.fromBank(b0, I80F48.fromNumber(0)),
|
||||||
0 as TokenIndex,
|
TokenInfo.fromBank(b1, I80F48.fromNumber(0)),
|
||||||
sourceBank.maintAssetWeight,
|
TokenInfo.fromBank(b2, I80F48.fromNumber(0)),
|
||||||
sourceBank.initAssetWeight,
|
|
||||||
sourceBank.maintLiabWeight,
|
|
||||||
sourceBank.initLiabWeight,
|
|
||||||
sourceBank.price!,
|
|
||||||
I80F48.fromNumber(-18 * Math.pow(10, 6)),
|
|
||||||
ZERO_I80F48(),
|
|
||||||
),
|
|
||||||
|
|
||||||
new TokenInfo(
|
|
||||||
1 as TokenIndex,
|
|
||||||
targetBank.maintAssetWeight,
|
|
||||||
targetBank.initAssetWeight,
|
|
||||||
targetBank.maintLiabWeight,
|
|
||||||
targetBank.initLiabWeight,
|
|
||||||
targetBank.price!,
|
|
||||||
I80F48.fromNumber(51 * Math.pow(10, 6)),
|
|
||||||
ZERO_I80F48(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
toUiDecimalsForQuote(
|
hc
|
||||||
hc.getMaxSourceForTokenSwap(
|
.getMaxSourceForTokenSwap(
|
||||||
targetBank,
|
b0,
|
||||||
sourceBank,
|
b1,
|
||||||
I80F48.fromNumber(1),
|
I80F48.fromNumber(2 / 3),
|
||||||
I80F48.fromNumber(0.95),
|
I80F48.fromNumber(50),
|
||||||
),
|
)
|
||||||
).toFixed(3),
|
.toNumber(),
|
||||||
).equals('0.008');
|
).lessThan(0.0000001);
|
||||||
|
|
||||||
|
function findMaxSwapActual(
|
||||||
|
hc: HealthCache,
|
||||||
|
source: TokenIndex,
|
||||||
|
target: TokenIndex,
|
||||||
|
ratio: number,
|
||||||
|
priceFactor: number,
|
||||||
|
): I80F48[] {
|
||||||
|
const clonedHc: HealthCache = _.cloneDeep(hc);
|
||||||
|
|
||||||
|
const sourcePrice = clonedHc.tokenInfos[source].prices;
|
||||||
|
const targetPrice = clonedHc.tokenInfos[target].prices;
|
||||||
|
const swapPrice = I80F48.fromNumber(priceFactor)
|
||||||
|
.mul(sourcePrice.oracle)
|
||||||
|
.div(targetPrice.oracle);
|
||||||
|
const sourceAmount = clonedHc.getMaxSourceForTokenSwap(
|
||||||
|
banks[source],
|
||||||
|
banks[target],
|
||||||
|
swapPrice,
|
||||||
|
I80F48.fromNumber(ratio),
|
||||||
|
);
|
||||||
|
|
||||||
|
// adjust token balance
|
||||||
|
clonedHc.tokenInfos[source].balanceNative.isub(sourceAmount);
|
||||||
|
clonedHc.tokenInfos[target].balanceNative.iadd(
|
||||||
|
sourceAmount.mul(swapPrice),
|
||||||
|
);
|
||||||
|
|
||||||
|
return [sourceAmount, clonedHc.healthRatio(HealthType.init)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkMaxSwapResult(
|
||||||
|
hc: HealthCache,
|
||||||
|
source: TokenIndex,
|
||||||
|
target: TokenIndex,
|
||||||
|
ratio: number,
|
||||||
|
priceFactor: number,
|
||||||
|
): void {
|
||||||
|
const [sourceAmount, actualRatio] = findMaxSwapActual(
|
||||||
|
hc,
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
ratio,
|
||||||
|
priceFactor,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` -- checking ${source} to ${target} for priceFactor: ${priceFactor}, target ratio ${ratio}: actual ratio: ${actualRatio}, amount: ${sourceAmount}`,
|
||||||
|
);
|
||||||
|
expect(Math.abs(actualRatio.toNumber() - ratio)).lessThan(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
console.log(' - test 0');
|
||||||
|
// adjust by usdc
|
||||||
|
const clonedHc = _.cloneDeep(hc);
|
||||||
|
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||||
|
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const priceFactor of [0.1, 0.9, 1.1]) {
|
||||||
|
for (const target of _.range(1, 100, 1)) {
|
||||||
|
// checkMaxSwapResult(
|
||||||
|
// clonedHc,
|
||||||
|
// 0 as TokenIndex,
|
||||||
|
// 1 as TokenIndex,
|
||||||
|
// target,
|
||||||
|
// priceFactor,
|
||||||
|
// );
|
||||||
|
checkMaxSwapResult(
|
||||||
|
clonedHc,
|
||||||
|
1 as TokenIndex,
|
||||||
|
0 as TokenIndex,
|
||||||
|
target,
|
||||||
|
priceFactor,
|
||||||
|
);
|
||||||
|
// checkMaxSwapResult(
|
||||||
|
// clonedHc,
|
||||||
|
// 0 as TokenIndex,
|
||||||
|
// 2 as TokenIndex,
|
||||||
|
// target,
|
||||||
|
// priceFactor,
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this unlikely price it's healthy to swap infinitely
|
||||||
|
expect(function () {
|
||||||
|
findMaxSwapActual(
|
||||||
|
clonedHc,
|
||||||
|
0 as TokenIndex,
|
||||||
|
1 as TokenIndex,
|
||||||
|
50.0,
|
||||||
|
1.5,
|
||||||
|
);
|
||||||
|
}).to.throw('Number out of range');
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
console.log(' - test 1');
|
||||||
|
const clonedHc = _.cloneDeep(hc);
|
||||||
|
// adjust by usdc
|
||||||
|
clonedHc.tokenInfos[0].balanceNative.iadd(
|
||||||
|
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
|
||||||
|
);
|
||||||
|
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||||
|
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const priceFactor of [0.1, 0.9, 1.1]) {
|
||||||
|
for (const target of _.range(1, 100, 1)) {
|
||||||
|
checkMaxSwapResult(
|
||||||
|
clonedHc,
|
||||||
|
0 as TokenIndex,
|
||||||
|
1 as TokenIndex,
|
||||||
|
target,
|
||||||
|
priceFactor,
|
||||||
|
);
|
||||||
|
checkMaxSwapResult(
|
||||||
|
clonedHc,
|
||||||
|
1 as TokenIndex,
|
||||||
|
0 as TokenIndex,
|
||||||
|
target,
|
||||||
|
priceFactor,
|
||||||
|
);
|
||||||
|
checkMaxSwapResult(
|
||||||
|
clonedHc,
|
||||||
|
0 as TokenIndex,
|
||||||
|
2 as TokenIndex,
|
||||||
|
target,
|
||||||
|
priceFactor,
|
||||||
|
);
|
||||||
|
checkMaxSwapResult(
|
||||||
|
clonedHc,
|
||||||
|
2 as TokenIndex,
|
||||||
|
0 as TokenIndex,
|
||||||
|
target,
|
||||||
|
priceFactor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
console.log(' - test 2');
|
||||||
|
const clonedHc = _.cloneDeep(hc);
|
||||||
|
// adjust by usdc
|
||||||
|
clonedHc.tokenInfos[0].balanceNative.iadd(
|
||||||
|
I80F48.fromNumber(-50).div(clonedHc.tokenInfos[0].prices.oracle),
|
||||||
|
);
|
||||||
|
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||||
|
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||||
|
);
|
||||||
|
// possible even though the init ratio is <100
|
||||||
|
checkMaxSwapResult(clonedHc, 1 as TokenIndex, 0 as TokenIndex, 100, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
console.log(' - test 3');
|
||||||
|
const clonedHc = _.cloneDeep(hc);
|
||||||
|
// adjust by usdc
|
||||||
|
clonedHc.tokenInfos[0].balanceNative.iadd(
|
||||||
|
I80F48.fromNumber(-30).div(clonedHc.tokenInfos[0].prices.oracle),
|
||||||
|
);
|
||||||
|
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||||
|
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||||
|
);
|
||||||
|
clonedHc.tokenInfos[2].balanceNative.iadd(
|
||||||
|
I80F48.fromNumber(-30).div(clonedHc.tokenInfos[2].prices.oracle),
|
||||||
|
);
|
||||||
|
|
||||||
|
// swapping with a high ratio advises paying back all liabs
|
||||||
|
// and then swapping even more because increasing assets in 0 has better asset weight
|
||||||
|
const initRatio = clonedHc.healthRatio(HealthType.init);
|
||||||
|
const [amount, actualRatio] = findMaxSwapActual(
|
||||||
|
clonedHc,
|
||||||
|
1 as TokenIndex,
|
||||||
|
0 as TokenIndex,
|
||||||
|
100,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
expect(actualRatio.div(I80F48.fromNumber(2)).toNumber()).greaterThan(
|
||||||
|
initRatio.toNumber(),
|
||||||
|
);
|
||||||
|
expect(amount.toNumber() - 100 / 3).lessThan(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
console.log(' - test 4');
|
||||||
|
const clonedHc = _.cloneDeep(hc);
|
||||||
|
// adjust by usdc
|
||||||
|
clonedHc.tokenInfos[0].balanceNative.iadd(
|
||||||
|
I80F48.fromNumber(100).div(clonedHc.tokenInfos[0].prices.oracle),
|
||||||
|
);
|
||||||
|
clonedHc.tokenInfos[1].balanceNative.iadd(
|
||||||
|
I80F48.fromNumber(-2).div(clonedHc.tokenInfos[1].prices.oracle),
|
||||||
|
);
|
||||||
|
clonedHc.tokenInfos[2].balanceNative.iadd(
|
||||||
|
I80F48.fromNumber(-65).div(clonedHc.tokenInfos[2].prices.oracle),
|
||||||
|
);
|
||||||
|
|
||||||
|
const initRatio = clonedHc.healthRatio(HealthType.init);
|
||||||
|
expect(initRatio.toNumber()).greaterThan(3);
|
||||||
|
expect(initRatio.toNumber()).lessThan(4);
|
||||||
|
|
||||||
|
checkMaxSwapResult(clonedHc, 0 as TokenIndex, 1 as TokenIndex, 1, 1);
|
||||||
|
checkMaxSwapResult(clonedHc, 0 as TokenIndex, 1 as TokenIndex, 3, 1);
|
||||||
|
checkMaxSwapResult(clonedHc, 0 as TokenIndex, 1 as TokenIndex, 4, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test_max_perp', (done) => {
|
||||||
|
const baseLotSize = 100;
|
||||||
|
const b0 = mockBankAndOracle(0 as TokenIndex, 0.0, 0.0, 1);
|
||||||
|
const p0 = mockPerpMarket(0, 0.3, 0.3, baseLotSize, 2);
|
||||||
|
const hc = new HealthCache(
|
||||||
|
[TokenInfo.fromBank(b0, I80F48.fromNumber(0))],
|
||||||
|
[],
|
||||||
|
[PerpInfo.emptyFromPerpMarket(p0)],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(hc.health(HealthType.init).toNumber()).equals(0);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
toUiDecimalsForQuote(
|
hc
|
||||||
hc.getMaxSourceForTokenSwap(
|
.getMaxPerpForHealthRatio(
|
||||||
sourceBank,
|
p0,
|
||||||
targetBank,
|
I80F48.fromNumber(2),
|
||||||
I80F48.fromNumber(1),
|
PerpOrderSide.bid,
|
||||||
I80F48.fromNumber(0.95),
|
I80F48.fromNumber(50),
|
||||||
),
|
)
|
||||||
).toFixed(3),
|
.toNumber(),
|
||||||
).equals('90.176');
|
).equals(0);
|
||||||
|
|
||||||
|
function findMaxTrade(
|
||||||
|
hc: HealthCache,
|
||||||
|
side: PerpOrderSide,
|
||||||
|
ratio: number,
|
||||||
|
priceFactor: number,
|
||||||
|
): number[] {
|
||||||
|
const prices = hc.perpInfos[0].prices;
|
||||||
|
const tradePrice = I80F48.fromNumber(priceFactor).mul(prices.oracle);
|
||||||
|
const baseLots0 = hc
|
||||||
|
.getMaxPerpForHealthRatio(
|
||||||
|
p0,
|
||||||
|
tradePrice,
|
||||||
|
side,
|
||||||
|
I80F48.fromNumber(ratio),
|
||||||
|
)
|
||||||
|
.toNumber();
|
||||||
|
|
||||||
|
const direction = side == PerpOrderSide.bid ? 1 : -1;
|
||||||
|
|
||||||
|
// compute the health ratio we'd get when executing the trade
|
||||||
|
const baseLots1 = direction * baseLots0;
|
||||||
|
let baseNative = I80F48.fromNumber(baseLots1).mul(
|
||||||
|
I80F48.fromNumber(baseLotSize),
|
||||||
|
);
|
||||||
|
let hcClone: HealthCache = _.cloneDeep(hc);
|
||||||
|
hcClone.perpInfos[0].baseLots.iadd(new BN(baseLots1));
|
||||||
|
hcClone.perpInfos[0].quote.isub(baseNative.mul(tradePrice));
|
||||||
|
const actualRatio = hcClone.healthRatio(HealthType.init);
|
||||||
|
|
||||||
|
// the ratio for trading just one base lot extra
|
||||||
|
const baseLots2 = direction * (baseLots0 + 1);
|
||||||
|
baseNative = I80F48.fromNumber(baseLots2 * baseLotSize);
|
||||||
|
hcClone = _.cloneDeep(hc);
|
||||||
|
hcClone.perpInfos[0].baseLots.iadd(new BN(baseLots2));
|
||||||
|
hcClone.perpInfos[0].quote.isub(baseNative.mul(tradePrice));
|
||||||
|
const plusRatio = hcClone.healthRatio(HealthType.init);
|
||||||
|
|
||||||
|
return [baseLots0, actualRatio.toNumber(), plusRatio.toNumber()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkMaxTrade(
|
||||||
|
hc: HealthCache,
|
||||||
|
side: PerpOrderSide,
|
||||||
|
ratio: number,
|
||||||
|
priceFactor: number,
|
||||||
|
): void {
|
||||||
|
const [baseLots, actualRatio, plusRatio] = findMaxTrade(
|
||||||
|
hc,
|
||||||
|
side,
|
||||||
|
ratio,
|
||||||
|
priceFactor,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`checking for price_factor: ${priceFactor}, target ratio ${ratio}: actual ratio: ${actualRatio}, plus ratio: ${plusRatio}, base_lots: ${baseLots}`,
|
||||||
|
);
|
||||||
|
expect(ratio).lessThan(actualRatio);
|
||||||
|
expect(plusRatio - 0.1).lessThanOrEqual(ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
// adjust token
|
||||||
|
hc.tokenInfos[0].balanceNative.iadd(I80F48.fromNumber(3000));
|
||||||
|
for (const existing of [-5, 0, 3]) {
|
||||||
|
const hcClone: HealthCache = _.cloneDeep(hc);
|
||||||
|
hcClone.perpInfos[0].baseLots.iadd(new BN(existing));
|
||||||
|
hcClone.perpInfos[0].quote.isub(
|
||||||
|
I80F48.fromNumber(existing * baseLotSize * 2),
|
||||||
|
);
|
||||||
|
for (const side of [PerpOrderSide.bid, PerpOrderSide.ask]) {
|
||||||
|
console.log(
|
||||||
|
`existing ${existing} ${side === PerpOrderSide.bid ? 'bid' : 'ask'}`,
|
||||||
|
);
|
||||||
|
for (const priceFactor of [0.8, 1.0, 1.1]) {
|
||||||
|
for (const ratio of _.range(1, 101, 1)) {
|
||||||
|
checkMaxTrade(hcClone, side, ratio, priceFactor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check some extremely bad prices
|
||||||
|
checkMaxTrade(hc, PerpOrderSide.bid, 50, 2);
|
||||||
|
checkMaxTrade(hc, PerpOrderSide.ask, 50, 0.1);
|
||||||
|
|
||||||
|
// and extremely good prices
|
||||||
|
expect(function () {
|
||||||
|
findMaxTrade(hc, PerpOrderSide.bid, 50, 0.1);
|
||||||
|
}).to.throw();
|
||||||
|
expect(function () {
|
||||||
|
findMaxTrade(hc, PerpOrderSide.ask, 50, 1.5);
|
||||||
|
}).to.throw();
|
||||||
|
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -482,29 +482,28 @@ export class MangoAccount {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The max amount of given source ui token you can swap to a target token.
|
* The max amount of given source ui token you can swap to a target token.
|
||||||
* PriceFactor is ratio between A - how many source tokens can be traded for target tokens
|
* Price is simply the source tokens price divided by target tokens price,
|
||||||
* and B - source native oracle price / target native oracle price.
|
* it is supposed to give an indication of how many source tokens can be traded for target tokens,
|
||||||
* e.g. a slippage of 5% and some fees which are 1%, then priceFactor = 0.94
|
* it can optionally contain information on slippage and fees.
|
||||||
* the factor is used to compute how much target can be obtained by swapping source
|
|
||||||
* in reality, and not only relying on oracle prices, and taking in account e.g. slippage which
|
|
||||||
* can occur at large size
|
|
||||||
* @returns max amount of given source ui token you can swap to a target token, in ui token
|
* @returns max amount of given source ui token you can swap to a target token, in ui token
|
||||||
*/
|
*/
|
||||||
getMaxSourceUiForTokenSwap(
|
getMaxSourceUiForTokenSwap(
|
||||||
group: Group,
|
group: Group,
|
||||||
sourceMintPk: PublicKey,
|
sourceMintPk: PublicKey,
|
||||||
targetMintPk: PublicKey,
|
targetMintPk: PublicKey,
|
||||||
priceFactor: number,
|
price: number,
|
||||||
): number {
|
): number {
|
||||||
if (sourceMintPk.equals(targetMintPk)) {
|
if (sourceMintPk.equals(targetMintPk)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
const s = group.getFirstBankByMint(sourceMintPk);
|
||||||
|
const t = group.getFirstBankByMint(targetMintPk);
|
||||||
const hc = HealthCache.fromMangoAccount(group, this);
|
const hc = HealthCache.fromMangoAccount(group, this);
|
||||||
const maxSource = hc.getMaxSourceForTokenSwap(
|
const maxSource = hc.getMaxSourceForTokenSwap(
|
||||||
group.getFirstBankByMint(sourceMintPk),
|
s,
|
||||||
group.getFirstBankByMint(targetMintPk),
|
t,
|
||||||
|
I80F48.fromNumber(price * Math.pow(10, t.mintDecimals - s.mintDecimals)),
|
||||||
I80F48.fromNumber(2), // target 2% health
|
I80F48.fromNumber(2), // target 2% health
|
||||||
I80F48.fromNumber(priceFactor),
|
|
||||||
);
|
);
|
||||||
maxSource.idiv(
|
maxSource.idiv(
|
||||||
ONE_I80F48().add(
|
ONE_I80F48().add(
|
||||||
|
@ -609,6 +608,8 @@ export class MangoAccount {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* TODO REWORK, know to break in binary search, also make work for limit orders
|
||||||
|
*
|
||||||
* @param group
|
* @param group
|
||||||
* @param externalMarketPk
|
* @param externalMarketPk
|
||||||
* @returns maximum ui quote which can be traded at oracle price for base token given current health
|
* @returns maximum ui quote which can be traded at oracle price for base token given current health
|
||||||
|
@ -646,6 +647,7 @@ export class MangoAccount {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* TODO REWORK, know to break in binary search, also make work for limit orders
|
||||||
* @param group
|
* @param group
|
||||||
* @param externalMarketPk
|
* @param externalMarketPk
|
||||||
* @returns maximum ui base which can be traded at oracle price for quote token given current health
|
* @returns maximum ui base which can be traded at oracle price for quote token given current health
|
||||||
|
@ -673,7 +675,7 @@ export class MangoAccount {
|
||||||
// If its a ask then the reserved fund and potential loan is in base
|
// If its a ask then the reserved fund and potential loan is in base
|
||||||
// also keep some buffer for fees, use taker fees for worst case simulation.
|
// also keep some buffer for fees, use taker fees for worst case simulation.
|
||||||
nativeAmount = nativeAmount
|
nativeAmount = nativeAmount
|
||||||
.div(baseBank.price)
|
// .div(baseBank.price)
|
||||||
.div(ONE_I80F48().add(baseBank.loanOriginationFeeRate))
|
.div(ONE_I80F48().add(baseBank.loanOriginationFeeRate))
|
||||||
.div(ONE_I80F48().add(I80F48.fromNumber(group.getSerum3FeeRates(false))));
|
.div(ONE_I80F48().add(I80F48.fromNumber(group.getSerum3FeeRates(false))));
|
||||||
return toUiDecimals(
|
return toUiDecimals(
|
||||||
|
@ -795,7 +797,10 @@ export class MangoAccount {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* TODO: also think about limit orders
|
||||||
*
|
*
|
||||||
|
* The max ui quote you can place a market/ioc bid on the market,
|
||||||
|
* price is the ui price at which you think the order would materialiase.
|
||||||
* @param group
|
* @param group
|
||||||
* @param perpMarketName
|
* @param perpMarketName
|
||||||
* @returns maximum ui quote which can be traded at oracle price for quote token given current health
|
* @returns maximum ui quote which can be traded at oracle price for quote token given current health
|
||||||
|
@ -803,15 +808,13 @@ export class MangoAccount {
|
||||||
public getMaxQuoteForPerpBidUi(
|
public getMaxQuoteForPerpBidUi(
|
||||||
group: Group,
|
group: Group,
|
||||||
perpMarketIndex: PerpMarketIndex,
|
perpMarketIndex: PerpMarketIndex,
|
||||||
|
price: number,
|
||||||
): number {
|
): number {
|
||||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||||
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
|
|
||||||
const hc = HealthCache.fromMangoAccount(group, this);
|
const hc = HealthCache.fromMangoAccount(group, this);
|
||||||
const baseLots = hc.getMaxPerpForHealthRatio(
|
const baseLots = hc.getMaxPerpForHealthRatio(
|
||||||
perpMarket,
|
perpMarket,
|
||||||
pp
|
I80F48.fromNumber(price),
|
||||||
? pp
|
|
||||||
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
|
|
||||||
PerpOrderSide.bid,
|
PerpOrderSide.bid,
|
||||||
I80F48.fromNumber(2),
|
I80F48.fromNumber(2),
|
||||||
);
|
);
|
||||||
|
@ -821,7 +824,10 @@ export class MangoAccount {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* TODO: also think about limit orders
|
||||||
*
|
*
|
||||||
|
* The max ui base you can place a market/ioc ask on the market,
|
||||||
|
* price is the ui price at which you think the order would materialiase.
|
||||||
* @param group
|
* @param group
|
||||||
* @param perpMarketName
|
* @param perpMarketName
|
||||||
* @param uiPrice ui price at which ask would be placed at
|
* @param uiPrice ui price at which ask would be placed at
|
||||||
|
@ -830,15 +836,13 @@ export class MangoAccount {
|
||||||
public getMaxBaseForPerpAskUi(
|
public getMaxBaseForPerpAskUi(
|
||||||
group: Group,
|
group: Group,
|
||||||
perpMarketIndex: PerpMarketIndex,
|
perpMarketIndex: PerpMarketIndex,
|
||||||
|
price: number,
|
||||||
): number {
|
): number {
|
||||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||||
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
|
|
||||||
const hc = HealthCache.fromMangoAccount(group, this);
|
const hc = HealthCache.fromMangoAccount(group, this);
|
||||||
const baseLots = hc.getMaxPerpForHealthRatio(
|
const baseLots = hc.getMaxPerpForHealthRatio(
|
||||||
perpMarket,
|
perpMarket,
|
||||||
pp
|
I80F48.fromNumber(price),
|
||||||
? pp
|
|
||||||
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
|
|
||||||
PerpOrderSide.ask,
|
PerpOrderSide.ask,
|
||||||
I80F48.fromNumber(2),
|
I80F48.fromNumber(2),
|
||||||
);
|
);
|
||||||
|
@ -849,6 +853,7 @@ export class MangoAccount {
|
||||||
group: Group,
|
group: Group,
|
||||||
perpMarketIndex: PerpMarketIndex,
|
perpMarketIndex: PerpMarketIndex,
|
||||||
size: number,
|
size: number,
|
||||||
|
price: number,
|
||||||
): number {
|
): number {
|
||||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||||
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
|
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
|
||||||
|
@ -859,8 +864,9 @@ export class MangoAccount {
|
||||||
pp
|
pp
|
||||||
? pp
|
? pp
|
||||||
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
|
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
|
||||||
perpMarket.uiBaseToLots(size),
|
|
||||||
PerpOrderSide.bid,
|
PerpOrderSide.bid,
|
||||||
|
perpMarket.uiBaseToLots(size),
|
||||||
|
I80F48.fromNumber(price),
|
||||||
HealthType.init,
|
HealthType.init,
|
||||||
)
|
)
|
||||||
.toNumber();
|
.toNumber();
|
||||||
|
@ -870,6 +876,7 @@ export class MangoAccount {
|
||||||
group: Group,
|
group: Group,
|
||||||
perpMarketIndex: PerpMarketIndex,
|
perpMarketIndex: PerpMarketIndex,
|
||||||
size: number,
|
size: number,
|
||||||
|
price: number,
|
||||||
): number {
|
): number {
|
||||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||||
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
|
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
|
||||||
|
@ -880,8 +887,9 @@ export class MangoAccount {
|
||||||
pp
|
pp
|
||||||
? pp
|
? pp
|
||||||
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
|
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
|
||||||
perpMarket.uiBaseToLots(size),
|
|
||||||
PerpOrderSide.ask,
|
PerpOrderSide.ask,
|
||||||
|
perpMarket.uiBaseToLots(size),
|
||||||
|
I80F48.fromNumber(price),
|
||||||
HealthType.init,
|
HealthType.init,
|
||||||
)
|
)
|
||||||
.toNumber();
|
.toNumber();
|
||||||
|
@ -1228,6 +1236,7 @@ export class PerpPosition {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO FUTURE: double check with program side code that this is in sycn with latest changes in program
|
||||||
public getEntryPrice(perpMarket: PerpMarket): BN {
|
public getEntryPrice(perpMarket: PerpMarket): BN {
|
||||||
if (this.basePositionLots.eq(new BN(0))) {
|
if (this.basePositionLots.eq(new BN(0))) {
|
||||||
return new BN(0);
|
return new BN(0);
|
||||||
|
@ -1237,6 +1246,7 @@ export class PerpPosition {
|
||||||
.abs();
|
.abs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO FUTURE: double check with program side code that this is in sycn with latest changes in program
|
||||||
public getBreakEvenPrice(perpMarket: PerpMarket): BN {
|
public getBreakEvenPrice(perpMarket: PerpMarket): BN {
|
||||||
if (this.basePositionLots.eq(new BN(0))) {
|
if (this.basePositionLots.eq(new BN(0))) {
|
||||||
return new BN(0);
|
return new BN(0);
|
||||||
|
|
|
@ -5,7 +5,12 @@ import Big from 'big.js';
|
||||||
import { MangoClient } from '../client';
|
import { MangoClient } from '../client';
|
||||||
import { I80F48, I80F48Dto, ZERO_I80F48 } from '../numbers/I80F48';
|
import { I80F48, I80F48Dto, ZERO_I80F48 } from '../numbers/I80F48';
|
||||||
import { As, toNative, U64_MAX_BN } from '../utils';
|
import { As, toNative, U64_MAX_BN } from '../utils';
|
||||||
import { OracleConfig, QUOTE_DECIMALS, TokenIndex } from './bank';
|
import {
|
||||||
|
OracleConfig,
|
||||||
|
QUOTE_DECIMALS,
|
||||||
|
StablePriceModel,
|
||||||
|
TokenIndex,
|
||||||
|
} from './bank';
|
||||||
import { Group } from './group';
|
import { Group } from './group';
|
||||||
import { MangoAccount } from './mangoAccount';
|
import { MangoAccount } from './mangoAccount';
|
||||||
|
|
||||||
|
@ -73,6 +78,7 @@ export class PerpMarket {
|
||||||
settleFeeFlat: number;
|
settleFeeFlat: number;
|
||||||
settleFeeAmountThreshold: number;
|
settleFeeAmountThreshold: number;
|
||||||
settleFeeFractionLowHealth: number;
|
settleFeeFractionLowHealth: number;
|
||||||
|
stablePriceModel: StablePriceModel;
|
||||||
},
|
},
|
||||||
): PerpMarket {
|
): PerpMarket {
|
||||||
return new PerpMarket(
|
return new PerpMarket(
|
||||||
|
@ -112,6 +118,7 @@ export class PerpMarket {
|
||||||
obj.settleFeeFlat,
|
obj.settleFeeFlat,
|
||||||
obj.settleFeeAmountThreshold,
|
obj.settleFeeAmountThreshold,
|
||||||
obj.settleFeeFractionLowHealth,
|
obj.settleFeeFractionLowHealth,
|
||||||
|
obj.stablePriceModel,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,6 +159,7 @@ export class PerpMarket {
|
||||||
public settleFeeFlat: number,
|
public settleFeeFlat: number,
|
||||||
public settleFeeAmountThreshold: number,
|
public settleFeeAmountThreshold: number,
|
||||||
public settleFeeFractionLowHealth: number,
|
public settleFeeFractionLowHealth: number,
|
||||||
|
public stablePriceModel: StablePriceModel,
|
||||||
) {
|
) {
|
||||||
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
||||||
this.maintAssetWeight = I80F48.from(maintAssetWeight);
|
this.maintAssetWeight = I80F48.from(maintAssetWeight);
|
||||||
|
|
|
@ -285,7 +285,7 @@ export class MangoClient {
|
||||||
public async tokenEdit(
|
public async tokenEdit(
|
||||||
group: Group,
|
group: Group,
|
||||||
mintPk: PublicKey,
|
mintPk: PublicKey,
|
||||||
oracle: PublicKey | null,
|
oracle: PublicKey, // TODO: do we need an extra param for resetting stable_price_model?
|
||||||
oracleConfig: OracleConfigParams | null,
|
oracleConfig: OracleConfigParams | null,
|
||||||
groupInsuranceFund: boolean | null,
|
groupInsuranceFund: boolean | null,
|
||||||
interestRateParams: InterestRateParams | null,
|
interestRateParams: InterestRateParams | null,
|
||||||
|
@ -332,6 +332,7 @@ export class MangoClient {
|
||||||
)
|
)
|
||||||
.accounts({
|
.accounts({
|
||||||
group: group.publicKey,
|
group: group.publicKey,
|
||||||
|
oracle,
|
||||||
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||||
mintInfo: mintInfo.publicKey,
|
mintInfo: mintInfo.publicKey,
|
||||||
})
|
})
|
||||||
|
@ -342,7 +343,7 @@ export class MangoClient {
|
||||||
isSigner: false,
|
isSigner: false,
|
||||||
} as AccountMeta,
|
} as AccountMeta,
|
||||||
])
|
])
|
||||||
.rpc({ skipPreflight: true });
|
.rpc();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async tokenDeregister(
|
public async tokenDeregister(
|
||||||
|
@ -1469,7 +1470,7 @@ export class MangoClient {
|
||||||
public async perpEditMarket(
|
public async perpEditMarket(
|
||||||
group: Group,
|
group: Group,
|
||||||
perpMarketIndex: PerpMarketIndex,
|
perpMarketIndex: PerpMarketIndex,
|
||||||
oracle: PublicKey | null,
|
oracle: PublicKey, // TODO: do we need an extra param for resetting stable_price_model
|
||||||
oracleConfig: OracleConfigParams | null,
|
oracleConfig: OracleConfigParams | null,
|
||||||
baseDecimals: number | null,
|
baseDecimals: number | null,
|
||||||
maintAssetWeight: number | null,
|
maintAssetWeight: number | null,
|
||||||
|
@ -1527,6 +1528,7 @@ export class MangoClient {
|
||||||
)
|
)
|
||||||
.accounts({
|
.accounts({
|
||||||
group: group.publicKey,
|
group: group.publicKey,
|
||||||
|
oracle,
|
||||||
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||||
perpMarket: perpMarket.publicKey,
|
perpMarket: perpMarket.publicKey,
|
||||||
})
|
})
|
||||||
|
@ -1775,7 +1777,7 @@ export class MangoClient {
|
||||||
new BN(clientOrderId ?? Date.now()),
|
new BN(clientOrderId ?? Date.now()),
|
||||||
orderType ? orderType : PerpOrderType.limit,
|
orderType ? orderType : PerpOrderType.limit,
|
||||||
reduceOnly ? reduceOnly : false,
|
reduceOnly ? reduceOnly : false,
|
||||||
new BN(expiryTimestamp ? expiryTimestamp : 0),
|
new BN(expiryTimestamp ?? 0),
|
||||||
limit ? limit : 10,
|
limit ? limit : 10,
|
||||||
-1,
|
-1,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||||
import { Cluster, Connection, Keypair } from '@solana/web3.js';
|
import { Cluster, Connection, Keypair } from '@solana/web3.js';
|
||||||
|
import { expect } from 'chai';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { Group } from '../accounts/group';
|
import { Group } from '../accounts/group';
|
||||||
import { HealthCache } from '../accounts/healthCache';
|
import { HealthCache } from '../accounts/healthCache';
|
||||||
|
@ -95,50 +96,83 @@ async function debugUser(
|
||||||
await getMaxWithdrawWithBorrowForTokenUiWrapper(srcToken);
|
await getMaxWithdrawWithBorrowForTokenUiWrapper(srcToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
function simHealthRatioWithTokenPositionChangesWrapper(debug, change): void {
|
|
||||||
console.log(
|
|
||||||
`mangoAccount.simHealthRatioWithTokenPositionChanges ${debug}` +
|
|
||||||
mangoAccount.simHealthRatioWithTokenPositionUiChanges(group, [change]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (const srcToken of Array.from(group.banksMapByName.keys())) {
|
|
||||||
simHealthRatioWithTokenPositionChangesWrapper(`${srcToken} 1 `, {
|
|
||||||
mintPk: group.banksMapByName.get(srcToken)![0].mint,
|
|
||||||
uiTokenAmount: 1,
|
|
||||||
});
|
|
||||||
simHealthRatioWithTokenPositionChangesWrapper(`${srcToken} -1 `, {
|
|
||||||
mintPk: group.banksMapByName.get(srcToken)![0].mint,
|
|
||||||
uiTokenAmount: -1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMaxSourceForTokenSwapWrapper(src, tgt): void {
|
function getMaxSourceForTokenSwapWrapper(src, tgt): void {
|
||||||
console.log(
|
const maxSourceUi = mangoAccount.getMaxSourceUiForTokenSwap(
|
||||||
`getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` +
|
|
||||||
mangoAccount.getMaxSourceUiForTokenSwap(
|
|
||||||
group,
|
group,
|
||||||
group.banksMapByName.get(src)![0].mint,
|
group.banksMapByName.get(src)![0].mint,
|
||||||
group.banksMapByName.get(tgt)![0].mint,
|
group.banksMapByName.get(tgt)![0].mint,
|
||||||
1,
|
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)} ` +
|
||||||
|
maxSourceUi.toFixed(3).padStart(10) +
|
||||||
|
`, health ratio after (${sim.toFixed(3).padStart(10)})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (const srcToken of Array.from(group.banksMapByName.keys())) {
|
for (const srcToken of Array.from(group.banksMapByName.keys()).sort()) {
|
||||||
for (const tgtToken of Array.from(group.banksMapByName.keys())) {
|
for (const tgtToken of Array.from(group.banksMapByName.keys()).sort()) {
|
||||||
// if (srcToken === 'SOL')
|
|
||||||
// if (tgtToken === 'MSOL')
|
|
||||||
getMaxSourceForTokenSwapWrapper(srcToken, tgtToken);
|
getMaxSourceForTokenSwapWrapper(srcToken, tgtToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMaxForPerpWrapper(perpMarket: PerpMarket): void {
|
function getMaxForPerpWrapper(perpMarket: PerpMarket): void {
|
||||||
console.log(
|
const maxQuoteUi = mangoAccount.getMaxQuoteForPerpBidUi(
|
||||||
`getMaxQuoteForPerpBidUi ${perpMarket.perpMarketIndex} ` +
|
group,
|
||||||
mangoAccount.getMaxQuoteForPerpBidUi(group, perpMarket.perpMarketIndex),
|
perpMarket.perpMarketIndex,
|
||||||
|
perpMarket.uiPrice,
|
||||||
);
|
);
|
||||||
|
const simMaxQuote = mangoAccount.simHealthRatioWithPerpBidUiChanges(
|
||||||
|
group,
|
||||||
|
perpMarket.perpMarketIndex,
|
||||||
|
maxQuoteUi / perpMarket.uiPrice,
|
||||||
|
perpMarket.uiPrice,
|
||||||
|
);
|
||||||
|
expect(simMaxQuote).gt(2);
|
||||||
|
expect(simMaxQuote).lt(3);
|
||||||
|
const maxBaseUi = mangoAccount.getMaxBaseForPerpAskUi(
|
||||||
|
group,
|
||||||
|
perpMarket.perpMarketIndex,
|
||||||
|
perpMarket.uiPrice,
|
||||||
|
);
|
||||||
|
const simMaxBase = mangoAccount.simHealthRatioWithPerpAskUiChanges(
|
||||||
|
group,
|
||||||
|
perpMarket.perpMarketIndex,
|
||||||
|
maxBaseUi,
|
||||||
|
perpMarket.uiPrice,
|
||||||
|
);
|
||||||
|
expect(simMaxBase).gt(2);
|
||||||
|
expect(simMaxBase).lt(3);
|
||||||
console.log(
|
console.log(
|
||||||
`getMaxBaseForPerpAskUi ${perpMarket.perpMarketIndex} ` +
|
`getMaxPerp ${perpMarket.name.padStart(
|
||||||
mangoAccount.getMaxBaseForPerpAskUi(group, perpMarket.perpMarketIndex),
|
10,
|
||||||
|
)} getMaxQuoteForPerpBidUi ${maxQuoteUi
|
||||||
|
.toFixed(3)
|
||||||
|
.padStart(10)} health ratio after (${simMaxQuote
|
||||||
|
.toFixed(3)
|
||||||
|
.padStart(10)}), getMaxBaseForPerpAskUi ${maxBaseUi
|
||||||
|
.toFixed(3)
|
||||||
|
.padStart(10)} health ratio after (${simMaxBase
|
||||||
|
.toFixed(3)
|
||||||
|
.padStart(10)})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (const perpMarket of Array.from(
|
for (const perpMarket of Array.from(
|
||||||
|
@ -148,7 +182,6 @@ async function debugUser(
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMaxForSerum3Wrapper(serum3Market: Serum3Market): void {
|
function getMaxForSerum3Wrapper(serum3Market: Serum3Market): void {
|
||||||
// if (serum3Market.name !== 'SOL/USDC') return;
|
|
||||||
console.log(
|
console.log(
|
||||||
`getMaxQuoteForSerum3BidUi ${serum3Market.name} ` +
|
`getMaxQuoteForSerum3BidUi ${serum3Market.name} ` +
|
||||||
mangoAccount.getMaxQuoteForSerum3BidUi(
|
mangoAccount.getMaxQuoteForSerum3BidUi(
|
||||||
|
|
|
@ -3667,12 +3667,37 @@ export type MangoV4 = {
|
||||||
],
|
],
|
||||||
"type": "i64"
|
"type": "i64"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "borrowLimitQuote",
|
||||||
|
"docs": [
|
||||||
|
"Soft borrow limit in native quote",
|
||||||
|
"",
|
||||||
|
"Once the borrows on the bank exceed this quote value, init_liab_weight is scaled up.",
|
||||||
|
"Set to f64::MAX to disable.",
|
||||||
|
"",
|
||||||
|
"See scaled_init_liab_weight()."
|
||||||
|
],
|
||||||
|
"type": "f64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "collateralLimitQuote",
|
||||||
|
"docs": [
|
||||||
|
"Limit for collateral of deposits",
|
||||||
|
"",
|
||||||
|
"Once the deposits in the bank exceed this quote value, init_asset_weight is scaled",
|
||||||
|
"down to keep the total collateral value constant.",
|
||||||
|
"Set to f64::MAX to disable.",
|
||||||
|
"",
|
||||||
|
"See scaled_init_asset_weight()."
|
||||||
|
],
|
||||||
|
"type": "f64"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "reserved",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
2136
|
2120
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4319,10 +4344,17 @@ export type MangoV4 = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "settlePnlLimitFactor",
|
"name": "settlePnlLimitFactor",
|
||||||
|
"docs": [
|
||||||
|
"Fraction of perp base value that can be settled each window.",
|
||||||
|
"Set to a negative value to disable the limit."
|
||||||
|
],
|
||||||
"type": "f32"
|
"type": "f32"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "settlePnlLimitWindowSizeTs",
|
"name": "settlePnlLimitWindowSizeTs",
|
||||||
|
"docs": [
|
||||||
|
"Window size in seconds for the perp settlement limit"
|
||||||
|
],
|
||||||
"type": "u64"
|
"type": "u64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -10894,12 +10926,37 @@ export const IDL: MangoV4 = {
|
||||||
],
|
],
|
||||||
"type": "i64"
|
"type": "i64"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "borrowLimitQuote",
|
||||||
|
"docs": [
|
||||||
|
"Soft borrow limit in native quote",
|
||||||
|
"",
|
||||||
|
"Once the borrows on the bank exceed this quote value, init_liab_weight is scaled up.",
|
||||||
|
"Set to f64::MAX to disable.",
|
||||||
|
"",
|
||||||
|
"See scaled_init_liab_weight()."
|
||||||
|
],
|
||||||
|
"type": "f64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "collateralLimitQuote",
|
||||||
|
"docs": [
|
||||||
|
"Limit for collateral of deposits",
|
||||||
|
"",
|
||||||
|
"Once the deposits in the bank exceed this quote value, init_asset_weight is scaled",
|
||||||
|
"down to keep the total collateral value constant.",
|
||||||
|
"Set to f64::MAX to disable.",
|
||||||
|
"",
|
||||||
|
"See scaled_init_asset_weight()."
|
||||||
|
],
|
||||||
|
"type": "f64"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "reserved",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
2136
|
2120
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11546,10 +11603,17 @@ export const IDL: MangoV4 = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "settlePnlLimitFactor",
|
"name": "settlePnlLimitFactor",
|
||||||
|
"docs": [
|
||||||
|
"Fraction of perp base value that can be settled each window.",
|
||||||
|
"Set to a negative value to disable the limit."
|
||||||
|
],
|
||||||
"type": "f32"
|
"type": "f32"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "settlePnlLimitWindowSizeTs",
|
"name": "settlePnlLimitWindowSizeTs",
|
||||||
|
"docs": [
|
||||||
|
"Window size in seconds for the perp settlement limit"
|
||||||
|
],
|
||||||
"type": "u64"
|
"type": "u64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { buildVersionedTx } from '../utils';
|
||||||
// * solana airdrop 1 -k ~/.config/solana/admin.json
|
// * solana airdrop 1 -k ~/.config/solana/admin.json
|
||||||
//
|
//
|
||||||
|
|
||||||
|
// TODO: switch out with devnet openbook markets
|
||||||
const DEVNET_SERUM3_MARKETS = new Map([
|
const DEVNET_SERUM3_MARKETS = new Map([
|
||||||
['BTC/USDC', 'DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB'],
|
['BTC/USDC', 'DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB'],
|
||||||
['SOL/USDC', '5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR'],
|
['SOL/USDC', '5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR'],
|
||||||
|
@ -44,6 +45,7 @@ const DEVNET_ORACLES = new Map([
|
||||||
['SRM', '992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs'],
|
['SRM', '992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// TODO: should these constants be baked right into client.ts or even program?
|
||||||
const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2;
|
const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2;
|
||||||
const NET_BORROWS_WINDOW_SIZE_TS = 24 * 60 * 60;
|
const NET_BORROWS_WINDOW_SIZE_TS = 24 * 60 * 60;
|
||||||
const NET_BORROWS_LIMIT_NATIVE = 1 * Math.pow(10, 7) * Math.pow(10, 6);
|
const NET_BORROWS_LIMIT_NATIVE = 1 * Math.pow(10, 7) * Math.pow(10, 6);
|
||||||
|
|
|
@ -5,11 +5,6 @@ import fs from 'fs';
|
||||||
import { Group } from '../accounts/group';
|
import { Group } from '../accounts/group';
|
||||||
import { HealthType } from '../accounts/mangoAccount';
|
import { HealthType } from '../accounts/mangoAccount';
|
||||||
import { PerpOrderSide, PerpOrderType } from '../accounts/perp';
|
import { PerpOrderSide, PerpOrderType } from '../accounts/perp';
|
||||||
import {
|
|
||||||
Serum3OrderType,
|
|
||||||
Serum3SelfTradeBehavior,
|
|
||||||
Serum3Side,
|
|
||||||
} from '../accounts/serum3';
|
|
||||||
import { MangoClient } from '../client';
|
import { MangoClient } from '../client';
|
||||||
import { MANGO_V4_ID } from '../constants';
|
import { MANGO_V4_ID } from '../constants';
|
||||||
import { toUiDecimalsForQuote } from '../utils';
|
import { toUiDecimalsForQuote } from '../utils';
|
||||||
|
@ -193,131 +188,132 @@ async function main() {
|
||||||
await mangoAccount.reload(client);
|
await mangoAccount.reload(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (true) {
|
// Note: Disable for now until we have openbook devnet markets
|
||||||
// serum3
|
// if (true) {
|
||||||
const asks = await group.loadSerum3AsksForMarket(
|
// // serum3
|
||||||
client,
|
// const asks = await group.loadSerum3AsksForMarket(
|
||||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
// client,
|
||||||
);
|
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||||
const lowestAsk = Array.from(asks!)[0];
|
// );
|
||||||
const bids = await group.loadSerum3BidsForMarket(
|
// const lowestAsk = Array.from(asks!)[0];
|
||||||
client,
|
// const bids = await group.loadSerum3BidsForMarket(
|
||||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
// client,
|
||||||
);
|
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||||
const highestBid = Array.from(bids!)![0];
|
// );
|
||||||
|
// const highestBid = Array.from(bids!)![0];
|
||||||
|
|
||||||
console.log(`...cancelling all existing serum3 orders`);
|
// console.log(`...cancelling all existing serum3 orders`);
|
||||||
if (
|
// if (
|
||||||
Array.from(mangoAccount.serum3OosMapByMarketIndex.values()).length > 0
|
// Array.from(mangoAccount.serum3OosMapByMarketIndex.values()).length > 0
|
||||||
) {
|
// ) {
|
||||||
await client.serum3CancelAllOrders(
|
// await client.serum3CancelAllOrders(
|
||||||
group,
|
// group,
|
||||||
mangoAccount,
|
// mangoAccount,
|
||||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||||
10,
|
// 10,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
let price = 20;
|
// let price = 20;
|
||||||
let qty = 0.0001;
|
// let qty = 0.0001;
|
||||||
console.log(
|
// console.log(
|
||||||
`...placing serum3 bid which would not be settled since its relatively low then midprice at ${price} for ${qty}`,
|
// `...placing serum3 bid which would not be settled since its relatively low then midprice at ${price} for ${qty}`,
|
||||||
);
|
// );
|
||||||
await client.serum3PlaceOrder(
|
// await client.serum3PlaceOrder(
|
||||||
group,
|
// group,
|
||||||
mangoAccount,
|
// mangoAccount,
|
||||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||||
Serum3Side.bid,
|
// Serum3Side.bid,
|
||||||
price,
|
// price,
|
||||||
qty,
|
// qty,
|
||||||
Serum3SelfTradeBehavior.decrementTake,
|
// Serum3SelfTradeBehavior.decrementTake,
|
||||||
Serum3OrderType.limit,
|
// Serum3OrderType.limit,
|
||||||
Date.now(),
|
// Date.now(),
|
||||||
10,
|
// 10,
|
||||||
);
|
// );
|
||||||
await mangoAccount.reload(client);
|
// await mangoAccount.reload(client);
|
||||||
let orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
|
// let orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
|
||||||
client,
|
// client,
|
||||||
group,
|
// group,
|
||||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||||
);
|
// );
|
||||||
expect(orders[0].price).equals(20);
|
// expect(orders[0].price).equals(20);
|
||||||
expect(orders[0].size).equals(qty);
|
// expect(orders[0].size).equals(qty);
|
||||||
|
|
||||||
price = lowestAsk.price + lowestAsk.price / 2;
|
// price = lowestAsk.price + lowestAsk.price / 2;
|
||||||
qty = 0.0001;
|
// qty = 0.0001;
|
||||||
console.log(
|
// console.log(
|
||||||
`...placing serum3 bid way above midprice at ${price} for ${qty}`,
|
// `...placing serum3 bid way above midprice at ${price} for ${qty}`,
|
||||||
);
|
// );
|
||||||
await client.serum3PlaceOrder(
|
// await client.serum3PlaceOrder(
|
||||||
group,
|
// group,
|
||||||
mangoAccount,
|
// mangoAccount,
|
||||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||||
Serum3Side.bid,
|
// Serum3Side.bid,
|
||||||
price,
|
// price,
|
||||||
qty,
|
// qty,
|
||||||
Serum3SelfTradeBehavior.decrementTake,
|
// Serum3SelfTradeBehavior.decrementTake,
|
||||||
Serum3OrderType.limit,
|
// Serum3OrderType.limit,
|
||||||
Date.now(),
|
// Date.now(),
|
||||||
10,
|
// 10,
|
||||||
);
|
// );
|
||||||
await mangoAccount.reload(client);
|
// await mangoAccount.reload(client);
|
||||||
|
|
||||||
price = highestBid.price - highestBid.price / 2;
|
// price = highestBid.price - highestBid.price / 2;
|
||||||
qty = 0.0001;
|
// qty = 0.0001;
|
||||||
console.log(
|
// console.log(
|
||||||
`...placing serum3 ask way below midprice at ${price} for ${qty}`,
|
// `...placing serum3 ask way below midprice at ${price} for ${qty}`,
|
||||||
);
|
// );
|
||||||
await client.serum3PlaceOrder(
|
// await client.serum3PlaceOrder(
|
||||||
group,
|
// group,
|
||||||
mangoAccount,
|
// mangoAccount,
|
||||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||||
Serum3Side.ask,
|
// Serum3Side.ask,
|
||||||
price,
|
// price,
|
||||||
qty,
|
// qty,
|
||||||
Serum3SelfTradeBehavior.decrementTake,
|
// Serum3SelfTradeBehavior.decrementTake,
|
||||||
Serum3OrderType.limit,
|
// Serum3OrderType.limit,
|
||||||
Date.now(),
|
// Date.now(),
|
||||||
10,
|
// 10,
|
||||||
);
|
// );
|
||||||
|
|
||||||
console.log(`...current own orders on OB`);
|
// console.log(`...current own orders on OB`);
|
||||||
orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
|
// orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
|
||||||
client,
|
// client,
|
||||||
group,
|
// group,
|
||||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||||
);
|
// );
|
||||||
for (const order of orders) {
|
// for (const order of orders) {
|
||||||
console.log(
|
// console.log(
|
||||||
` - order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`,
|
// ` - order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`,
|
||||||
);
|
// );
|
||||||
console.log(` - cancelling order with ${order.orderId}`);
|
// console.log(` - cancelling order with ${order.orderId}`);
|
||||||
await client.serum3CancelOrder(
|
// await client.serum3CancelOrder(
|
||||||
group,
|
// group,
|
||||||
mangoAccount,
|
// mangoAccount,
|
||||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||||
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
|
// order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
|
||||||
order.orderId,
|
// order.orderId,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
console.log(`...current own orders on OB`);
|
// console.log(`...current own orders on OB`);
|
||||||
orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
|
// orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
|
||||||
client,
|
// client,
|
||||||
group,
|
// group,
|
||||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||||
);
|
// );
|
||||||
for (const order of orders) {
|
// for (const order of orders) {
|
||||||
console.log(order);
|
// console.log(order);
|
||||||
}
|
// }
|
||||||
|
|
||||||
console.log(`...settling funds`);
|
// console.log(`...settling funds`);
|
||||||
await client.serum3SettleFunds(
|
// await client.serum3SettleFunds(
|
||||||
group,
|
// group,
|
||||||
mangoAccount,
|
// mangoAccount,
|
||||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
// DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (true) {
|
if (true) {
|
||||||
await mangoAccount.reload(client);
|
await mangoAccount.reload(client);
|
||||||
|
@ -505,6 +501,7 @@ async function main() {
|
||||||
const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi(
|
const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi(
|
||||||
group,
|
group,
|
||||||
perpMarket.perpMarketIndex,
|
perpMarket.perpMarketIndex,
|
||||||
|
perpMarket.uiPrice,
|
||||||
);
|
);
|
||||||
const baseQty = quoteQty / price;
|
const baseQty = quoteQty / price;
|
||||||
console.log(
|
console.log(
|
||||||
|
@ -512,6 +509,7 @@ async function main() {
|
||||||
group,
|
group,
|
||||||
perpMarket.perpMarketIndex,
|
perpMarket.perpMarketIndex,
|
||||||
baseQty,
|
baseQty,
|
||||||
|
perpMarket.uiPrice,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
|
@ -554,6 +552,7 @@ async function main() {
|
||||||
mangoAccount.getMaxQuoteForPerpBidUi(
|
mangoAccount.getMaxQuoteForPerpBidUi(
|
||||||
group,
|
group,
|
||||||
perpMarket.perpMarketIndex,
|
perpMarket.perpMarketIndex,
|
||||||
|
perpMarket.uiPrice,
|
||||||
) * 1.02;
|
) * 1.02;
|
||||||
|
|
||||||
const baseQty = quoteQty / price;
|
const baseQty = quoteQty / price;
|
||||||
|
@ -589,12 +588,14 @@ async function main() {
|
||||||
const baseQty = mangoAccount.getMaxBaseForPerpAskUi(
|
const baseQty = mangoAccount.getMaxBaseForPerpAskUi(
|
||||||
group,
|
group,
|
||||||
perpMarket.perpMarketIndex,
|
perpMarket.perpMarketIndex,
|
||||||
|
perpMarket.uiPrice,
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
` simHealthRatioWithPerpAskUiChanges - ${mangoAccount.simHealthRatioWithPerpAskUiChanges(
|
` simHealthRatioWithPerpAskUiChanges - ${mangoAccount.simHealthRatioWithPerpAskUiChanges(
|
||||||
group,
|
group,
|
||||||
perpMarket.perpMarketIndex,
|
perpMarket.perpMarketIndex,
|
||||||
baseQty,
|
baseQty,
|
||||||
|
perpMarket.uiPrice,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
const quoteQty = baseQty * price;
|
const quoteQty = baseQty * price;
|
||||||
|
@ -627,8 +628,11 @@ async function main() {
|
||||||
group.banksMapByName.get('BTC')![0].uiPrice! +
|
group.banksMapByName.get('BTC')![0].uiPrice! +
|
||||||
Math.floor(Math.random() * 100);
|
Math.floor(Math.random() * 100);
|
||||||
const baseQty =
|
const baseQty =
|
||||||
mangoAccount.getMaxBaseForPerpAskUi(group, perpMarket.perpMarketIndex) *
|
mangoAccount.getMaxBaseForPerpAskUi(
|
||||||
1.02;
|
group,
|
||||||
|
perpMarket.perpMarketIndex,
|
||||||
|
perpMarket.uiPrice,
|
||||||
|
) * 1.02;
|
||||||
const quoteQty = baseQty * price;
|
const quoteQty = baseQty * price;
|
||||||
console.log(
|
console.log(
|
||||||
`...placing max qty perp ask * 1.02 clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
|
`...placing max qty perp ask * 1.02 clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
|
||||||
|
|
|
@ -43,6 +43,7 @@ const MAINNET_ORACLES = new Map([
|
||||||
|
|
||||||
// External markets are matched with those in https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json
|
// External markets are matched with those in https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json
|
||||||
// and verified to have best liquidity for pair on https://openserum.io/
|
// and verified to have best liquidity for pair on https://openserum.io/
|
||||||
|
// TODO: replace with markets from https://github.com/openbook-dex/resources/blob/main/markets.json
|
||||||
const MAINNET_SERUM3_MARKETS = new Map([
|
const MAINNET_SERUM3_MARKETS = new Map([
|
||||||
['BTC/USDC', 'A8YFbxQYFVqKZaoYJLLUVcQiWP7G2MeEgW5wsAQgMvFw'],
|
['BTC/USDC', 'A8YFbxQYFVqKZaoYJLLUVcQiWP7G2MeEgW5wsAQgMvFw'],
|
||||||
['SOL/USDC', '9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT'],
|
['SOL/USDC', '9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT'],
|
||||||
|
|
|
@ -156,7 +156,7 @@ async function main() {
|
||||||
await client.tokenEdit(
|
await client.tokenEdit(
|
||||||
group,
|
group,
|
||||||
buyMint,
|
buyMint,
|
||||||
null,
|
group.getFirstBankByMint(buyMint).oracle,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
@ -196,7 +196,7 @@ async function main() {
|
||||||
await client.tokenEdit(
|
await client.tokenEdit(
|
||||||
group,
|
group,
|
||||||
buyMint,
|
buyMint,
|
||||||
null,
|
group.getFirstBankByMint(buyMint).oracle,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
|
Loading…
Reference in New Issue