ts: use price factor in maxSourceForSwap + max perp bid and ask + tests (#237)
* ts: use price factor in maxSourceForSwap ts: max perp bid and ask ts: mocha test for max swap Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * ts: comemnt Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
parent
39bdf20813
commit
bb6790e678
|
@ -460,6 +460,8 @@ pub mod mango_v4 {
|
|||
instructions::perp_close_market(ctx)
|
||||
}
|
||||
|
||||
// TODO perp_change_perp_market_params
|
||||
|
||||
pub fn perp_deactivate_position(ctx: Context<PerpDeactivatePosition>) -> Result<()> {
|
||||
instructions::perp_deactivate_position(ctx)
|
||||
}
|
||||
|
|
|
@ -891,6 +891,8 @@ impl HealthCache {
|
|||
/// swap BTC -> SOL and they're at ui prices of $20000 and $40, that means price
|
||||
/// should be 500000 native_SOL for a native_BTC. Because 1 BTC gives you 500 SOL
|
||||
/// so 1e6 native_BTC gives you 500e9 native_SOL.
|
||||
///
|
||||
/// NOTE: keep getMaxSourceForTokenSwap in ts/client in sync with changes here
|
||||
#[cfg(feature = "client")]
|
||||
pub fn max_swap_source_for_health_ratio(
|
||||
&self,
|
||||
|
@ -1018,6 +1020,7 @@ impl HealthCache {
|
|||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
/// NOTE: keep getMaxSourceForTokenSwap in ts/client in sync with changes here
|
||||
pub fn max_perp_for_health_ratio(
|
||||
&self,
|
||||
perp_market_index: PerpMarketIndex,
|
||||
|
|
|
@ -10,7 +10,18 @@ export type OracleConfig = {
|
|||
confFilter: I80F48Dto;
|
||||
};
|
||||
|
||||
export class Bank {
|
||||
export class BankForHealth {
|
||||
constructor(
|
||||
public tokenIndex: number,
|
||||
public maintAssetWeight: I80F48,
|
||||
public initAssetWeight: I80F48,
|
||||
public maintLiabWeight: I80F48,
|
||||
public initLiabWeight: I80F48,
|
||||
public price: I80F48 | undefined,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class Bank extends BankForHealth {
|
||||
public name: string;
|
||||
public depositIndex: I80F48;
|
||||
public borrowIndex: I80F48;
|
||||
|
|
|
@ -256,7 +256,6 @@ export class Group {
|
|||
bank.oracle,
|
||||
ai,
|
||||
this.getMintDecimals(bank.mint),
|
||||
this.getMintDecimals(this.insuranceMint),
|
||||
);
|
||||
bank.price = price;
|
||||
bank.uiPrice = uiPrice;
|
||||
|
@ -283,7 +282,6 @@ export class Group {
|
|||
perpMarket.oracle,
|
||||
ai,
|
||||
perpMarket.baseDecimals,
|
||||
this.getMintDecimals(this.insuranceMint),
|
||||
);
|
||||
perpMarket.price = price;
|
||||
perpMarket.uiPrice = uiPrice;
|
||||
|
@ -295,7 +293,6 @@ export class Group {
|
|||
oracle: PublicKey,
|
||||
ai: AccountInfo<Buffer>,
|
||||
baseDecimals: number,
|
||||
quoteDecimals: number,
|
||||
) {
|
||||
let price, uiPrice;
|
||||
if (
|
||||
|
@ -305,13 +302,13 @@ export class Group {
|
|||
) {
|
||||
const stubOracle = coder.decode('stubOracle', ai.data);
|
||||
price = new I80F48(stubOracle.price.val);
|
||||
uiPrice = this?.toUiPrice(price, baseDecimals, quoteDecimals);
|
||||
uiPrice = this?.toUiPrice(price, baseDecimals);
|
||||
} else if (isPythOracle(ai)) {
|
||||
uiPrice = parsePriceData(ai.data).previousPrice;
|
||||
price = this?.toNativePrice(uiPrice, baseDecimals, quoteDecimals);
|
||||
price = this?.toNativePrice(uiPrice, baseDecimals);
|
||||
} else if (isSwitchboardOracle(ai)) {
|
||||
uiPrice = await parseSwitchboardOracle(ai);
|
||||
price = this?.toNativePrice(uiPrice, baseDecimals, quoteDecimals);
|
||||
price = this?.toNativePrice(uiPrice, baseDecimals);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unknown oracle provider for oracle ${oracle}, with owner ${ai.owner}`,
|
||||
|
@ -347,6 +344,10 @@ export class Group {
|
|||
return banks[0].mintDecimals;
|
||||
}
|
||||
|
||||
public getInsuranceMintDecimals(): number {
|
||||
return this.getMintDecimals(this.insuranceMint);
|
||||
}
|
||||
|
||||
public getFirstBankByMint(mintPk: PublicKey): Bank {
|
||||
const banks = this.banksMapByMint.get(mintPk.toString());
|
||||
if (!banks) throw new Error(`Unable to find bank for ${mintPk.toString()}`);
|
||||
|
@ -478,23 +479,26 @@ export class Group {
|
|||
}
|
||||
}
|
||||
|
||||
public toUiPrice(
|
||||
price: I80F48,
|
||||
baseDecimals: number,
|
||||
quoteDecimals: number,
|
||||
): number {
|
||||
public toUiPrice(price: I80F48, baseDecimals: number): number {
|
||||
return price
|
||||
.mul(I80F48.fromNumber(Math.pow(10, baseDecimals - quoteDecimals)))
|
||||
.mul(
|
||||
I80F48.fromNumber(
|
||||
Math.pow(10, baseDecimals - this.getInsuranceMintDecimals()),
|
||||
),
|
||||
)
|
||||
.toNumber();
|
||||
}
|
||||
|
||||
public toNativePrice(
|
||||
uiPrice: number,
|
||||
baseDecimals: number,
|
||||
quoteDecimals: number,
|
||||
): I80F48 {
|
||||
public toNativePrice(uiPrice: number, baseDecimals: number): I80F48 {
|
||||
return I80F48.fromNumber(uiPrice).mul(
|
||||
I80F48.fromNumber(Math.pow(10, quoteDecimals - baseDecimals)),
|
||||
I80F48.fromNumber(
|
||||
Math.pow(
|
||||
10,
|
||||
// note: our oracles are quoted in USD and our insurance mint is USD
|
||||
// please update when these assumptions change
|
||||
this.getInsuranceMintDecimals() - baseDecimals,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
import { expect } from 'chai';
|
||||
import { toUiDecimalsForQuote } from '../utils';
|
||||
import { BankForHealth } from './bank';
|
||||
import { HealthCache, TokenInfo } from './healthCache';
|
||||
import { I80F48, ZERO_I80F48 } from './I80F48';
|
||||
|
||||
describe('Health Cache', () => {
|
||||
it('max swap tokens for min ratio', () => {
|
||||
// USDC like
|
||||
const sourceBank = new BankForHealth(
|
||||
0,
|
||||
I80F48.fromNumber(1),
|
||||
I80F48.fromNumber(1),
|
||||
I80F48.fromNumber(1),
|
||||
I80F48.fromNumber(1),
|
||||
I80F48.fromNumber(1),
|
||||
);
|
||||
// BTC like
|
||||
const targetBank = new BankForHealth(
|
||||
1,
|
||||
I80F48.fromNumber(0.9),
|
||||
I80F48.fromNumber(0.8),
|
||||
I80F48.fromNumber(1.1),
|
||||
I80F48.fromNumber(1.2),
|
||||
I80F48.fromNumber(20000),
|
||||
);
|
||||
|
||||
const hc = new HealthCache(
|
||||
[
|
||||
new TokenInfo(
|
||||
0,
|
||||
sourceBank.maintAssetWeight,
|
||||
sourceBank.initAssetWeight,
|
||||
sourceBank.maintLiabWeight,
|
||||
sourceBank.initLiabWeight,
|
||||
sourceBank.price!,
|
||||
I80F48.fromNumber(-18 * Math.pow(10, 6)),
|
||||
ZERO_I80F48(),
|
||||
),
|
||||
|
||||
new TokenInfo(
|
||||
1,
|
||||
targetBank.maintAssetWeight,
|
||||
targetBank.initAssetWeight,
|
||||
targetBank.maintLiabWeight,
|
||||
targetBank.initLiabWeight,
|
||||
targetBank.price!,
|
||||
I80F48.fromNumber(51 * Math.pow(10, 6)),
|
||||
ZERO_I80F48(),
|
||||
),
|
||||
],
|
||||
[],
|
||||
[],
|
||||
);
|
||||
|
||||
expect(
|
||||
toUiDecimalsForQuote(
|
||||
hc.getMaxSourceForTokenSwap(
|
||||
targetBank,
|
||||
sourceBank,
|
||||
I80F48.fromNumber(1),
|
||||
I80F48.fromNumber(0.95),
|
||||
),
|
||||
).toFixed(3),
|
||||
).equals('0.008');
|
||||
|
||||
expect(
|
||||
toUiDecimalsForQuote(
|
||||
hc.getMaxSourceForTokenSwap(
|
||||
sourceBank,
|
||||
targetBank,
|
||||
I80F48.fromNumber(1),
|
||||
I80F48.fromNumber(0.95),
|
||||
),
|
||||
).toFixed(3),
|
||||
).equals('90.477');
|
||||
});
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import { PublicKey } from '@solana/web3.js';
|
||||
import _ from 'lodash';
|
||||
import { Bank } from './bank';
|
||||
import { Bank, BankForHealth } from './bank';
|
||||
import { Group } from './group';
|
||||
import {
|
||||
HUNDRED_I80F48,
|
||||
|
@ -11,6 +11,7 @@ import {
|
|||
ZERO_I80F48,
|
||||
} from './I80F48';
|
||||
import { HealthType } from './mangoAccount';
|
||||
import { PerpMarket, PerpOrderSide } from './perp';
|
||||
import { Serum3Market, Serum3Side } from './serum3';
|
||||
|
||||
// ░░░░
|
||||
|
@ -45,10 +46,26 @@ export class HealthCache {
|
|||
) {}
|
||||
|
||||
static fromDto(dto) {
|
||||
// console.log(
|
||||
JSON.stringify(
|
||||
dto,
|
||||
function replacer(k, v) {
|
||||
// console.log(k);
|
||||
console.log(v);
|
||||
// if (v instanceof BN) {
|
||||
// console.log(v);
|
||||
// return new I80F48(v).toNumber();
|
||||
// }
|
||||
// return v;
|
||||
},
|
||||
2,
|
||||
),
|
||||
// );
|
||||
process.exit(0);
|
||||
return new HealthCache(
|
||||
dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)),
|
||||
dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)),
|
||||
dto.perpInfos.map((dto) => new PerpInfo(dto)),
|
||||
dto.perpInfos.map((dto) => PerpInfo.fromDto(dto)),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -169,7 +186,7 @@ export class HealthCache {
|
|||
);
|
||||
}
|
||||
|
||||
getOrCreateTokenInfoIndex(bank: Bank): number {
|
||||
getOrCreateTokenInfoIndex(bank: BankForHealth): number {
|
||||
const index = this.findTokenInfoIndex(bank.tokenIndex);
|
||||
if (index == -1) {
|
||||
this.tokenInfos.push(TokenInfo.emptyFromBank(bank));
|
||||
|
@ -242,6 +259,20 @@ export class HealthCache {
|
|||
serum3Info.reserved = serum3Info.reserved.add(reservedAmount);
|
||||
}
|
||||
|
||||
findPerpInfoIndex(perpMarketIndex: number): number {
|
||||
return this.perpInfos.findIndex(
|
||||
(perpInfo) => perpInfo.perpMarketIndex === perpMarketIndex,
|
||||
);
|
||||
}
|
||||
|
||||
getOrCreatePerpInfoIndex(perpMarket: PerpMarket): number {
|
||||
const index = this.findPerpInfoIndex(perpMarket.perpMarketIndex);
|
||||
if (index == -1) {
|
||||
this.perpInfos.push(PerpInfo.emptyFromPerpMarket(perpMarket));
|
||||
}
|
||||
return this.findPerpInfoIndex(perpMarket.perpMarketIndex);
|
||||
}
|
||||
|
||||
public static logHealthCache(debug: string, healthCache: HealthCache) {
|
||||
if (debug) console.log(debug);
|
||||
for (const token of healthCache.tokenInfos) {
|
||||
|
@ -407,19 +438,17 @@ export class HealthCache {
|
|||
}
|
||||
|
||||
getMaxSourceForTokenSwap(
|
||||
group: Group,
|
||||
sourceMintPk: PublicKey,
|
||||
targetMintPk: PublicKey,
|
||||
sourceBank: BankForHealth,
|
||||
targetBank: BankForHealth,
|
||||
minRatio: I80F48,
|
||||
priceFactor: I80F48,
|
||||
): I80F48 {
|
||||
const sourceBank: Bank = group.getFirstBankByMint(sourceMintPk);
|
||||
const targetBank: Bank = group.getFirstBankByMint(targetMintPk);
|
||||
|
||||
if (sourceMintPk.equals(targetMintPk)) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
|
||||
if (!sourceBank.price || sourceBank.price.lte(ZERO_I80F48())) {
|
||||
if (
|
||||
!sourceBank.price ||
|
||||
sourceBank.price.lte(ZERO_I80F48()) ||
|
||||
!targetBank.price ||
|
||||
targetBank.price.lte(ZERO_I80F48())
|
||||
) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
|
||||
|
@ -441,10 +470,21 @@ export class HealthCache {
|
|||
// - be careful about finding the minRatio point: the function isn't convex
|
||||
|
||||
const initialRatio = this.healthRatio(HealthType.init);
|
||||
const initialHealth = this.health(HealthType.init);
|
||||
if (initialRatio.lte(ZERO_I80F48())) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
|
||||
// If the price is sufficiently good, then health will just increase from swapping:
|
||||
// once we've swapped enough, swapping x reduces health by x * source_liab_weight and
|
||||
// increases it by x * target_asset_weight * price_factor.
|
||||
const finalHealthSlope = sourceBank.initLiabWeight
|
||||
.neg()
|
||||
.add(targetBank.initAssetWeight.mul(priceFactor));
|
||||
if (finalHealthSlope.gte(ZERO_I80F48())) {
|
||||
return MAX_I80F48();
|
||||
}
|
||||
|
||||
const healthCacheClone: HealthCache = _.cloneDeep(this);
|
||||
const sourceIndex = healthCacheClone.getOrCreateTokenInfoIndex(sourceBank);
|
||||
const targetIndex = healthCacheClone.getOrCreateTokenInfoIndex(targetBank);
|
||||
|
@ -461,7 +501,9 @@ export class HealthCache {
|
|||
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
|
||||
// HealthCache.logHealthCache('beforeSwap', adjustedCache);
|
||||
adjustedCache.tokenInfos[sourceIndex].balance.isub(amount);
|
||||
adjustedCache.tokenInfos[targetIndex].balance.iadd(amount);
|
||||
adjustedCache.tokenInfos[targetIndex].balance.iadd(
|
||||
amount.mul(priceFactor),
|
||||
);
|
||||
// HealthCache.logHealthCache('afterSwap', adjustedCache);
|
||||
return adjustedCache;
|
||||
}
|
||||
|
@ -470,11 +512,16 @@ export class HealthCache {
|
|||
return cacheAfterSwap(amount).healthRatio(HealthType.init);
|
||||
}
|
||||
|
||||
// There are two key slope changes: Assume source.balance > 0 and target.balance < 0.
|
||||
// When these values flip sign, the health slope decreases, but could still be positive.
|
||||
// After point1 it's definitely negative (due to finalHealthSlope check above).
|
||||
// The maximum health ratio will be at 0 or at one of these points (ignoring serum3 effects).
|
||||
const sourceForZeroTargetBalance = target.balance.neg().div(priceFactor);
|
||||
const point0Amount = source.balance
|
||||
.min(target.balance.neg())
|
||||
.min(sourceForZeroTargetBalance)
|
||||
.max(ZERO_I80F48());
|
||||
const point1Amount = source.balance
|
||||
.max(target.balance.neg())
|
||||
.max(sourceForZeroTargetBalance)
|
||||
.max(ZERO_I80F48());
|
||||
const cache0 = cacheAfterSwap(point0Amount);
|
||||
const point0Ratio = cache0.healthRatio(HealthType.init);
|
||||
|
@ -505,12 +552,12 @@ export class HealthCache {
|
|||
// If point1Ratio is still bigger than minRatio, the target amount must be >point1Amount
|
||||
// search to the right of point1Amount: but how far?
|
||||
// At point1, source.balance < 0 and target.balance > 0, so use a simple estimation for
|
||||
// zero health: health - source_liab_weight * a + target_asset_weight * a = 0.
|
||||
// zero health: health - source_liab_weight * a + target_asset_weight * a * priceFactor = 0.
|
||||
if (point1Health.lte(ZERO_I80F48())) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
const zeroHealthAmount = point1Amount.add(
|
||||
point1Health.div(source.initLiabWeight.sub(target.initAssetWeight)),
|
||||
const zeroHealthAmount = point1Amount.sub(
|
||||
point1Health.div(finalHealthSlope),
|
||||
);
|
||||
const zeroHealthRatio = healthRatioAfterSwap(zeroHealthAmount);
|
||||
amount = HealthCache.binaryApproximationSearch(
|
||||
|
@ -532,21 +579,21 @@ export class HealthCache {
|
|||
healthRatioAfterSwap,
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
`internal error: assert that init ratio ${initialRatio.toNumber()} <= point0 ratio ${point0Ratio.toNumber()}`,
|
||||
// Must be between 0 and point0_amount
|
||||
amount = HealthCache.binaryApproximationSearch(
|
||||
ZERO_I80F48(),
|
||||
initialRatio,
|
||||
point0Amount,
|
||||
point0Ratio,
|
||||
minRatio,
|
||||
healthRatioAfterSwap,
|
||||
);
|
||||
}
|
||||
|
||||
return amount
|
||||
.div(source.oraclePrice)
|
||||
.div(
|
||||
ONE_I80F48().add(
|
||||
group.getFirstBankByMint(sourceMintPk).loanOriginationFeeRate,
|
||||
),
|
||||
);
|
||||
return amount.div(source.oraclePrice);
|
||||
}
|
||||
|
||||
getMaxForSerum3Order(
|
||||
getMaxSerum3OrderForHealthRatio(
|
||||
group: Group,
|
||||
serum3Market: Serum3Market,
|
||||
side: Serum3Side,
|
||||
|
@ -669,6 +716,115 @@ export class HealthCache {
|
|||
.div(ONE_I80F48().add(quoteBank.loanOriginationFeeRate))
|
||||
.div(ONE_I80F48().add(I80F48.fromNumber(group.getFeeRate(false))));
|
||||
}
|
||||
|
||||
getMaxPerpForHealthRatio(
|
||||
perpMarket: PerpMarket,
|
||||
side: PerpOrderSide,
|
||||
minRatio: I80F48,
|
||||
price: I80F48,
|
||||
): I80F48 {
|
||||
const healthCacheClone: HealthCache = _.cloneDeep(this);
|
||||
|
||||
const initialRatio = this.healthRatio(HealthType.init);
|
||||
if (initialRatio.lt(ZERO_I80F48())) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
|
||||
const direction = side == PerpOrderSide.bid ? 1 : -1;
|
||||
|
||||
const perpInfoIndex = this.getOrCreatePerpInfoIndex(perpMarket);
|
||||
const perpInfo = this.perpInfos[perpInfoIndex];
|
||||
const oraclePrice = perpInfo.oraclePrice;
|
||||
const baseLotSize = I80F48.fromString(perpMarket.baseLotSize.toString());
|
||||
|
||||
// If the price is sufficiently good then health will just increase from trading
|
||||
const finalHealthSlope =
|
||||
direction == 1
|
||||
? perpInfo.initAssetWeight.mul(oraclePrice).sub(price)
|
||||
: price.sub(perpInfo.initLiabWeight.mul(oraclePrice));
|
||||
if (finalHealthSlope.gte(ZERO_I80F48())) {
|
||||
return MAX_I80F48();
|
||||
}
|
||||
|
||||
function cacheAfterTrade(baseLots: I80F48): HealthCache {
|
||||
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
|
||||
const d = I80F48.fromNumber(direction);
|
||||
adjustedCache.perpInfos[perpInfoIndex].base.iadd(
|
||||
d.mul(baseLots.mul(baseLotSize.mul(oraclePrice))),
|
||||
);
|
||||
adjustedCache.perpInfos[perpInfoIndex].quote.isub(
|
||||
d.mul(baseLots.mul(baseLotSize.mul(price))),
|
||||
);
|
||||
return adjustedCache;
|
||||
}
|
||||
|
||||
function healthAfterTrade(baseLots: I80F48): I80F48 {
|
||||
return cacheAfterTrade(baseLots).health(HealthType.init);
|
||||
}
|
||||
function healthRatioAfterTrade(baseLots: I80F48): I80F48 {
|
||||
return cacheAfterTrade(baseLots).healthRatio(HealthType.init);
|
||||
}
|
||||
|
||||
const initialBaseLots = perpInfo.base
|
||||
.div(perpInfo.oraclePrice)
|
||||
.div(baseLotSize);
|
||||
|
||||
// There are two cases:
|
||||
// 1. We are increasing abs(baseLots)
|
||||
// 2. We are bringing the base position to 0, and then going to case 1.
|
||||
const hasCase2 =
|
||||
(initialBaseLots.gt(ZERO_I80F48()) && direction == -1) ||
|
||||
(initialBaseLots.lt(ZERO_I80F48()) && direction == 1);
|
||||
|
||||
let case1Start: I80F48, case1StartRatio: I80F48;
|
||||
if (hasCase2) {
|
||||
case1Start = initialBaseLots.abs();
|
||||
case1StartRatio = healthRatioAfterTrade(case1Start);
|
||||
} else {
|
||||
case1Start = ZERO_I80F48();
|
||||
case1StartRatio = initialRatio;
|
||||
}
|
||||
|
||||
// If we start out below minRatio and can't go above, pick the best case
|
||||
let baseLots: I80F48;
|
||||
if (initialRatio.lte(minRatio) && case1StartRatio.lt(minRatio)) {
|
||||
if (case1StartRatio.gte(initialRatio)) {
|
||||
baseLots = case1Start;
|
||||
} else {
|
||||
baseLots = ZERO_I80F48();
|
||||
}
|
||||
} else if (case1StartRatio.gte(minRatio)) {
|
||||
// Must reach minRatio to the right of case1Start
|
||||
const case1StartHealth = healthAfterTrade(case1Start);
|
||||
if (case1StartHealth.lte(ZERO_I80F48())) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
const zeroHealthAmount = case1Start.sub(
|
||||
case1StartHealth.div(finalHealthSlope).div(baseLotSize),
|
||||
);
|
||||
const zeroHealthRatio = healthRatioAfterTrade(zeroHealthAmount);
|
||||
baseLots = HealthCache.binaryApproximationSearch(
|
||||
case1Start,
|
||||
case1StartRatio,
|
||||
zeroHealthAmount,
|
||||
zeroHealthRatio,
|
||||
minRatio,
|
||||
healthRatioAfterTrade,
|
||||
);
|
||||
} else {
|
||||
// Between 0 and case1Start
|
||||
baseLots = HealthCache.binaryApproximationSearch(
|
||||
ZERO_I80F48(),
|
||||
initialRatio,
|
||||
case1Start,
|
||||
case1StartRatio,
|
||||
minRatio,
|
||||
healthRatioAfterTrade,
|
||||
);
|
||||
}
|
||||
|
||||
return baseLots.floor();
|
||||
}
|
||||
}
|
||||
|
||||
export class TokenInfo {
|
||||
|
@ -699,10 +855,10 @@ export class TokenInfo {
|
|||
);
|
||||
}
|
||||
|
||||
static emptyFromBank(bank: Bank): TokenInfo {
|
||||
static emptyFromBank(bank: BankForHealth): TokenInfo {
|
||||
if (!bank.price)
|
||||
throw new Error(
|
||||
`Failed to create TokenInfo. Bank price unavailable. ${bank.mint.toString()}`,
|
||||
`Failed to create TokenInfo. Bank price unavailable for bank with tokenIndex ${bank.tokenIndex}`,
|
||||
);
|
||||
return new TokenInfo(
|
||||
bank.tokenIndex,
|
||||
|
@ -824,22 +980,33 @@ export class Serum3Info {
|
|||
}
|
||||
|
||||
export class PerpInfo {
|
||||
constructor(dto: PerpInfoDto) {
|
||||
this.maintAssetWeight = I80F48.from(dto.maintAssetWeight);
|
||||
this.initAssetWeight = I80F48.from(dto.initAssetWeight);
|
||||
this.maintLiabWeight = I80F48.from(dto.maintLiabWeight);
|
||||
this.initLiabWeight = I80F48.from(dto.initLiabWeight);
|
||||
this.base = I80F48.from(dto.base);
|
||||
this.quote = I80F48.from(dto.quote);
|
||||
}
|
||||
maintAssetWeight: I80F48;
|
||||
initAssetWeight: I80F48;
|
||||
maintLiabWeight: I80F48;
|
||||
initLiabWeight: I80F48;
|
||||
constructor(
|
||||
public perpMarketIndex: number,
|
||||
public maintAssetWeight: I80F48,
|
||||
public initAssetWeight: I80F48,
|
||||
public maintLiabWeight: I80F48,
|
||||
public initLiabWeight: I80F48,
|
||||
// in health-reference-token native units, needs scaling by asset/liab
|
||||
base: I80F48;
|
||||
public base: I80F48,
|
||||
// in health-reference-token native units, no asset/liab factor needed
|
||||
quote: I80F48;
|
||||
public quote: I80F48,
|
||||
public oraclePrice: I80F48,
|
||||
public hasOpenOrders: boolean,
|
||||
) {}
|
||||
|
||||
static fromDto(dto: PerpInfoDto) {
|
||||
return new PerpInfo(
|
||||
dto.perpMarketIndex,
|
||||
I80F48.from(dto.maintAssetWeight),
|
||||
I80F48.from(dto.initAssetWeight),
|
||||
I80F48.from(dto.maintLiabWeight),
|
||||
I80F48.from(dto.initLiabWeight),
|
||||
I80F48.from(dto.base),
|
||||
I80F48.from(dto.quote),
|
||||
I80F48.from(dto.oraclePrice),
|
||||
dto.hasOpenOrders,
|
||||
);
|
||||
}
|
||||
|
||||
healthContribution(healthType: HealthType): I80F48 {
|
||||
let weight;
|
||||
|
@ -859,6 +1026,24 @@ export class PerpInfo {
|
|||
// `self.quote + weight * self.base` here
|
||||
return this.quote.add(weight.mul(this.base)).min(ZERO_I80F48());
|
||||
}
|
||||
|
||||
static emptyFromPerpMarket(perpMarket: PerpMarket): PerpInfo {
|
||||
if (!perpMarket.price)
|
||||
throw new Error(
|
||||
`Failed to create PerpInfo. Oracle price unavailable. ${perpMarket.oracle.toString()}`,
|
||||
);
|
||||
return new PerpInfo(
|
||||
perpMarket.perpMarketIndex,
|
||||
perpMarket.maintAssetWeight,
|
||||
perpMarket.initAssetWeight,
|
||||
perpMarket.maintLiabWeight,
|
||||
perpMarket.initLiabWeight,
|
||||
ZERO_I80F48(),
|
||||
ZERO_I80F48(),
|
||||
I80F48.fromNumber(perpMarket.price),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class HealthCacheDto {
|
||||
|
@ -913,6 +1098,7 @@ export class Serum3InfoDto {
|
|||
}
|
||||
|
||||
export class PerpInfoDto {
|
||||
perpMarketIndex: number;
|
||||
maintAssetWeight: I80F48Dto;
|
||||
initAssetWeight: I80F48Dto;
|
||||
maintLiabWeight: I80F48Dto;
|
||||
|
@ -921,4 +1107,6 @@ export class PerpInfoDto {
|
|||
base: I80F48Dto;
|
||||
// in health-reference-token native units, no asset/liab factor needed
|
||||
quote: I80F48Dto;
|
||||
oraclePrice: I80F48Dto;
|
||||
hasOpenOrders: boolean;
|
||||
}
|
||||
|
|
|
@ -3,13 +3,18 @@ import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
|
|||
import { Order, Orderbook } from '@project-serum/serum/lib/market';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import { MangoClient } from '../client';
|
||||
import { nativeI80F48ToUi, toNative, toUiDecimals } from '../utils';
|
||||
import {
|
||||
nativeI80F48ToUi,
|
||||
toNative,
|
||||
toUiDecimals,
|
||||
toUiDecimalsForQuote,
|
||||
} from '../utils';
|
||||
import { Bank } from './bank';
|
||||
import { Group } from './group';
|
||||
import { HealthCache, HealthCacheDto } from './healthCache';
|
||||
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48';
|
||||
import { PerpOrder } from './perp';
|
||||
import { Serum3Market, Serum3Side } from './serum3';
|
||||
import { PerpOrder, PerpOrderSide } from './perp';
|
||||
import { Serum3Side } from './serum3';
|
||||
export class MangoAccount {
|
||||
public tokens: TokenPosition[];
|
||||
public serum3: Serum3Orders[];
|
||||
|
@ -310,13 +315,6 @@ export class MangoAccount {
|
|||
// can withdraw without borrowing until initHealth reaches 0
|
||||
if (existingPositionHealthContrib.gt(initHealth)) {
|
||||
const withdrawAbleExistingPositionHealthContrib = initHealth;
|
||||
// console.log(`initHealth ${initHealth}`);
|
||||
// console.log(
|
||||
// `existingPositionHealthContrib ${existingPositionHealthContrib}`,
|
||||
// );
|
||||
// console.log(
|
||||
// `withdrawAbleExistingPositionHealthContrib ${withdrawAbleExistingPositionHealthContrib}`,
|
||||
// );
|
||||
return withdrawAbleExistingPositionHealthContrib
|
||||
.div(tokenBank.initAssetWeight)
|
||||
.div(tokenBank.price);
|
||||
|
@ -332,15 +330,6 @@ export class MangoAccount {
|
|||
const maxBorrowNativeWithoutFees = maxBorrowNative.div(
|
||||
ONE_I80F48().add(tokenBank.loanOriginationFeeRate),
|
||||
);
|
||||
// console.log(`initHealth ${initHealth}`);
|
||||
// console.log(
|
||||
// `existingPositionHealthContrib ${existingPositionHealthContrib}`,
|
||||
// );
|
||||
// console.log(
|
||||
// `initHealthWithoutExistingPosition ${initHealthWithoutExistingPosition}`,
|
||||
// );
|
||||
// console.log(`maxBorrowNative ${maxBorrowNative}`);
|
||||
// console.log(`maxBorrowNativeWithoutFees ${maxBorrowNativeWithoutFees}`);
|
||||
return maxBorrowNativeWithoutFees.add(existingTokenDeposits);
|
||||
}
|
||||
|
||||
|
@ -359,78 +348,46 @@ export class MangoAccount {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The max amount of given source native token you can swap to a target token.
|
||||
* note: slippageAndFeesFactor is a normalized number, <1,
|
||||
* e.g. a slippage of 5% and some fees which are 1%, then slippageAndFeesFactor = 0.94
|
||||
* the factor is used to compute how much target can be obtained by swapping source
|
||||
* @returns max amount of given source native token you can swap to a target token, in native token
|
||||
*/
|
||||
getMaxSourceForTokenSwap(
|
||||
group: Group,
|
||||
sourceMintPk: PublicKey,
|
||||
targetMintPk: PublicKey,
|
||||
slippageAndFeesFactor: number,
|
||||
): I80F48 | undefined {
|
||||
if (!this.accountData) return undefined;
|
||||
return this.accountData.healthCache
|
||||
.getMaxSourceForTokenSwap(
|
||||
group,
|
||||
sourceMintPk,
|
||||
targetMintPk,
|
||||
ONE_I80F48(), // target 1% health
|
||||
)
|
||||
.mul(I80F48.fromNumber(slippageAndFeesFactor));
|
||||
}
|
||||
|
||||
/**
|
||||
* The max amount of given source ui token you can swap to a target token.
|
||||
* note: slippageAndFeesFactor is a normalized number, <1,
|
||||
* e.g. a slippage of 5% and some fees which are 1%, then slippageAndFeesFactor = 0.94
|
||||
* 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
|
||||
* @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,
|
||||
slippageAndFeesFactor: number,
|
||||
priceFactor: number,
|
||||
): number | undefined {
|
||||
const maxSource = this.getMaxSourceForTokenSwap(
|
||||
group,
|
||||
sourceMintPk,
|
||||
targetMintPk,
|
||||
slippageAndFeesFactor,
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
if (sourceMintPk.equals(targetMintPk)) {
|
||||
return 0;
|
||||
}
|
||||
const maxSource = this.accountData.healthCache.getMaxSourceForTokenSwap(
|
||||
group.getFirstBankByMint(sourceMintPk),
|
||||
group.getFirstBankByMint(targetMintPk),
|
||||
ONE_I80F48(), // target 1% health
|
||||
I80F48.fromNumber(priceFactor),
|
||||
);
|
||||
maxSource.idiv(
|
||||
ONE_I80F48().add(
|
||||
group.getFirstBankByMint(sourceMintPk).loanOriginationFeeRate,
|
||||
),
|
||||
);
|
||||
if (maxSource) {
|
||||
return toUiDecimals(maxSource, group.getMintDecimals(sourceMintPk));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates new health ratio after applying tokenChanges to the token positions.
|
||||
* Note: token changes are expected in native amounts
|
||||
*
|
||||
* e.g. useful to simulate health after a potential swap.
|
||||
* Note: health ratio is technically ∞ if liabs are 0
|
||||
* @returns health ratio, in percentage form
|
||||
*/
|
||||
simHealthRatioWithTokenPositionChanges(
|
||||
group: Group,
|
||||
nativeTokenChanges: {
|
||||
nativeTokenAmount: I80F48;
|
||||
mintPk: PublicKey;
|
||||
}[],
|
||||
healthType: HealthType = HealthType.init,
|
||||
): I80F48 | undefined {
|
||||
if (!this.accountData) return undefined;
|
||||
return this.accountData.healthCache.simHealthRatioWithTokenPositionChanges(
|
||||
group,
|
||||
nativeTokenChanges,
|
||||
healthType,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates new health ratio after applying tokenChanges to the token positions.
|
||||
* Note: token changes are expected in ui amounts
|
||||
|
@ -506,28 +463,11 @@ export class MangoAccount {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* TODO priceFactor
|
||||
* @param group
|
||||
* @param serum3Market
|
||||
* @returns maximum native quote which can be traded for base token given current health
|
||||
* @param externalMarketPk
|
||||
* @returns maximum ui quote which can be traded for base token given current health
|
||||
*/
|
||||
public getMaxQuoteForSerum3Bid(
|
||||
group: Group,
|
||||
serum3Market: Serum3Market,
|
||||
): I80F48 {
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
return this.accountData.healthCache.getMaxForSerum3Order(
|
||||
group,
|
||||
serum3Market,
|
||||
Serum3Side.bid,
|
||||
I80F48.fromNumber(1),
|
||||
);
|
||||
}
|
||||
|
||||
public getMaxQuoteForSerum3BidUi(
|
||||
group: Group,
|
||||
externalMarketPk: PublicKey,
|
||||
|
@ -540,7 +480,18 @@ export class MangoAccount {
|
|||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
||||
);
|
||||
}
|
||||
const nativeAmount = this.getMaxQuoteForSerum3Bid(group, serum3Market);
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
const nativeAmount =
|
||||
this.accountData.healthCache.getMaxSerum3OrderForHealthRatio(
|
||||
group,
|
||||
serum3Market,
|
||||
Serum3Side.bid,
|
||||
I80F48.fromNumber(1),
|
||||
);
|
||||
return toUiDecimals(
|
||||
nativeAmount,
|
||||
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex).mintDecimals,
|
||||
|
@ -548,28 +499,11 @@ export class MangoAccount {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* TODO priceFactor
|
||||
* @param group
|
||||
* @param serum3Market
|
||||
* @returns maximum native base which can be traded for quote token given current health
|
||||
* @param externalMarketPk
|
||||
* @returns maximum ui base which can be traded for quote token given current health
|
||||
*/
|
||||
public getMaxBaseForSerum3Ask(
|
||||
group: Group,
|
||||
serum3Market: Serum3Market,
|
||||
): I80F48 {
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
return this.accountData.healthCache.getMaxForSerum3Order(
|
||||
group,
|
||||
serum3Market,
|
||||
Serum3Side.ask,
|
||||
I80F48.fromNumber(1),
|
||||
);
|
||||
}
|
||||
|
||||
public getMaxBaseForSerum3AskUi(
|
||||
group: Group,
|
||||
externalMarketPk: PublicKey,
|
||||
|
@ -582,7 +516,18 @@ export class MangoAccount {
|
|||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
||||
);
|
||||
}
|
||||
const nativeAmount = this.getMaxBaseForSerum3Ask(group, serum3Market);
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
const nativeAmount =
|
||||
this.accountData.healthCache.getMaxSerum3OrderForHealthRatio(
|
||||
group,
|
||||
serum3Market,
|
||||
Serum3Side.ask,
|
||||
I80F48.fromNumber(1),
|
||||
);
|
||||
return toUiDecimals(
|
||||
nativeAmount,
|
||||
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex).mintDecimals,
|
||||
|
@ -592,31 +537,12 @@ export class MangoAccount {
|
|||
/**
|
||||
*
|
||||
* @param group
|
||||
* @param nativeQuoteAmount
|
||||
* @param serum3Market
|
||||
* @param uiQuoteAmount
|
||||
* @param externalMarketPk
|
||||
* @param healthType
|
||||
* @returns health ratio after a bid with nativeQuoteAmount is placed
|
||||
* @returns health ratio after a bid with uiQuoteAmount is placed
|
||||
*/
|
||||
simHealthRatioWithSerum3BidChanges(
|
||||
group: Group,
|
||||
nativeQuoteAmount: I80F48,
|
||||
serum3Market: Serum3Market,
|
||||
healthType: HealthType = HealthType.init,
|
||||
): I80F48 {
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
return this.accountData.healthCache.simHealthRatioWithSerum3BidChanges(
|
||||
group,
|
||||
nativeQuoteAmount,
|
||||
serum3Market,
|
||||
healthType,
|
||||
);
|
||||
}
|
||||
|
||||
simHealthRatioWithSerum3BidUiChanges(
|
||||
public simHealthRatioWithSerum3BidUiChanges(
|
||||
group: Group,
|
||||
uiQuoteAmount: number,
|
||||
externalMarketPk: PublicKey,
|
||||
|
@ -630,7 +556,13 @@ export class MangoAccount {
|
|||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
||||
);
|
||||
}
|
||||
return this.simHealthRatioWithSerum3BidChanges(
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
return this.accountData.healthCache
|
||||
.simHealthRatioWithSerum3BidChanges(
|
||||
group,
|
||||
toNative(
|
||||
uiQuoteAmount,
|
||||
|
@ -639,37 +571,19 @@ export class MangoAccount {
|
|||
),
|
||||
serum3Market,
|
||||
healthType,
|
||||
).toNumber();
|
||||
)
|
||||
.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param group
|
||||
* @param nativeBaseAmount
|
||||
* @param serum3Market
|
||||
* @param uiBaseAmount
|
||||
* @param externalMarketPk
|
||||
* @param healthType
|
||||
* @returns health ratio after an ask with nativeBaseAmount is placed
|
||||
* @returns health ratio after an ask with uiBaseAmount is placed
|
||||
*/
|
||||
simHealthRatioWithSerum3AskChanges(
|
||||
group: Group,
|
||||
nativeBaseAmount: I80F48,
|
||||
serum3Market: Serum3Market,
|
||||
healthType: HealthType = HealthType.init,
|
||||
): I80F48 {
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
return this.accountData.healthCache.simHealthRatioWithSerum3AskChanges(
|
||||
group,
|
||||
nativeBaseAmount,
|
||||
serum3Market,
|
||||
healthType,
|
||||
);
|
||||
}
|
||||
|
||||
simHealthRatioWithSerum3AskUiChanges(
|
||||
public simHealthRatioWithSerum3AskUiChanges(
|
||||
group: Group,
|
||||
uiBaseAmount: number,
|
||||
externalMarketPk: PublicKey,
|
||||
|
@ -683,7 +597,13 @@ export class MangoAccount {
|
|||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
||||
);
|
||||
}
|
||||
return this.simHealthRatioWithSerum3AskChanges(
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
return this.accountData.healthCache
|
||||
.simHealthRatioWithSerum3AskChanges(
|
||||
group,
|
||||
toNative(
|
||||
uiBaseAmount,
|
||||
|
@ -692,7 +612,72 @@ export class MangoAccount {
|
|||
),
|
||||
serum3Market,
|
||||
healthType,
|
||||
).toNumber();
|
||||
)
|
||||
.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param group
|
||||
* @param perpMarketName
|
||||
* @param uiPrice ui price at which bid would be placed at
|
||||
* @returns max ui quote bid
|
||||
*/
|
||||
public getMaxQuoteForPerpBidUi(
|
||||
group: Group,
|
||||
perpMarketName: string,
|
||||
uiPrice: number,
|
||||
): number {
|
||||
const perpMarket = group.perpMarketsMap.get(perpMarketName);
|
||||
if (!perpMarket) {
|
||||
throw new Error(`PerpMarket for ${perpMarketName} not found!`);
|
||||
}
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
const baseLots = this.accountData.healthCache.getMaxPerpForHealthRatio(
|
||||
perpMarket,
|
||||
PerpOrderSide.bid,
|
||||
I80F48.fromNumber(1),
|
||||
group.toNativePrice(uiPrice, perpMarket.baseDecimals),
|
||||
);
|
||||
const nativeBase = baseLots.mul(
|
||||
I80F48.fromString(perpMarket.baseLotSize.toString()),
|
||||
);
|
||||
const nativeQuote = nativeBase.mul(I80F48.fromNumber(perpMarket.price));
|
||||
return toUiDecimalsForQuote(nativeQuote.toNumber());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param group
|
||||
* @param perpMarketName
|
||||
* @param uiPrice ui price at which ask would be placed at
|
||||
* @returns max ui base ask
|
||||
*/
|
||||
public getMaxBaseForPerpAskUi(
|
||||
group: Group,
|
||||
perpMarketName: string,
|
||||
uiPrice: number,
|
||||
): number {
|
||||
const perpMarket = group.perpMarketsMap.get(perpMarketName);
|
||||
if (!perpMarket) {
|
||||
throw new Error(`PerpMarket for ${perpMarketName} not found!`);
|
||||
}
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
const baseLots = this.accountData.healthCache.getMaxPerpForHealthRatio(
|
||||
perpMarket,
|
||||
PerpOrderSide.ask,
|
||||
I80F48.fromNumber(1),
|
||||
group.toNativePrice(uiPrice, perpMarket.baseDecimals),
|
||||
);
|
||||
return perpMarket.baseLotsToUi(new BN(baseLots.toString()));
|
||||
}
|
||||
|
||||
public async loadPerpOpenOrdersForMarket(
|
||||
|
|
|
@ -364,7 +364,7 @@ export class BookSide {
|
|||
for (const order of this.items()) {
|
||||
s.iadd(order.sizeLots);
|
||||
if (s.gte(baseLots)) {
|
||||
return order.price;
|
||||
return order.uiPrice;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
@ -391,7 +391,7 @@ export class BookSide {
|
|||
|
||||
public getL2Ui(depth: number): [number, number][] {
|
||||
const levels: [number, number][] = [];
|
||||
for (const { price, size } of this.items()) {
|
||||
for (const { uiPrice: price, uiSize: size } of this.items()) {
|
||||
if (levels.length > 0 && levels[levels.length - 1][0] === price) {
|
||||
levels[levels.length - 1][1] += size;
|
||||
} else if (levels.length === depth) {
|
||||
|
@ -463,7 +463,7 @@ export class InnerNode {
|
|||
constructor(public children: [number]) {}
|
||||
}
|
||||
|
||||
export class Side {
|
||||
export class PerpOrderSide {
|
||||
static bid = { bid: {} };
|
||||
static ask = { ask: {} };
|
||||
}
|
||||
|
@ -478,7 +478,8 @@ export class PerpOrderType {
|
|||
|
||||
export class PerpOrder {
|
||||
static from(perpMarket: PerpMarket, leafNode: LeafNode, type: BookSideType) {
|
||||
const side = type == BookSideType.bids ? Side.bid : Side.ask;
|
||||
const side =
|
||||
type == BookSideType.bids ? PerpOrderSide.bid : PerpOrderSide.ask;
|
||||
const price = BookSide.getPriceFromKey(leafNode.key);
|
||||
const expiryTimestamp = leafNode.timeInForce
|
||||
? leafNode.timestamp.add(new BN(leafNode.timeInForce))
|
||||
|
@ -506,11 +507,11 @@ export class PerpOrder {
|
|||
public owner: PublicKey,
|
||||
public openOrdersSlot: number,
|
||||
public feeTier: 0,
|
||||
public price: number,
|
||||
public uiPrice: number,
|
||||
public priceLots: BN,
|
||||
public size: number,
|
||||
public uiSize: number,
|
||||
public sizeLots: BN,
|
||||
public side: Side,
|
||||
public side: PerpOrderSide,
|
||||
public timestamp: BN,
|
||||
public expiryTimestamp: BN,
|
||||
) {}
|
||||
|
|
|
@ -34,7 +34,7 @@ import {
|
|||
PerpPosition,
|
||||
} from './accounts/mangoAccount';
|
||||
import { StubOracle } from './accounts/oracle';
|
||||
import { PerpMarket, PerpOrderType, Side } from './accounts/perp';
|
||||
import { PerpMarket, PerpOrderSide, PerpOrderType } from './accounts/perp';
|
||||
import {
|
||||
generateSerum3MarketExternalVaultSignerAddress,
|
||||
Serum3Market,
|
||||
|
@ -1504,7 +1504,7 @@ export class MangoClient {
|
|||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
perpMarketName: string,
|
||||
side: Side,
|
||||
side: PerpOrderSide,
|
||||
price: number,
|
||||
quantity: number,
|
||||
maxQuoteQuantity: number,
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair } from '@solana/web3.js';
|
||||
import { Cluster, Connection, Keypair } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { Group } from '../accounts/group';
|
||||
import { I80F48 } from '../accounts/I80F48';
|
||||
import { HealthType, MangoAccount } from '../accounts/mangoAccount';
|
||||
import { PerpMarket } from '../accounts/perp';
|
||||
import { Serum3Market } from '../accounts/serum3';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
import { toUiDecimalsForQuote } from '../utils';
|
||||
|
||||
const CLUSTER_URL =
|
||||
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
|
||||
const PAYER_KEYPAIR =
|
||||
process.env.PAYER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
|
||||
const USER_KEYPAIR =
|
||||
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
|
||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 2);
|
||||
const CLUSTER: Cluster =
|
||||
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
|
||||
|
||||
async function debugUser(
|
||||
client: MangoClient,
|
||||
group: Group,
|
||||
|
@ -107,19 +117,12 @@ async function debugUser(
|
|||
function getMaxSourceForTokenSwapWrapper(src, tgt) {
|
||||
console.log(
|
||||
`getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` +
|
||||
mangoAccount
|
||||
.getMaxSourceForTokenSwap(
|
||||
mangoAccount.getMaxSourceUiForTokenSwap(
|
||||
group,
|
||||
group.banksMapByName.get(src)![0].mint,
|
||||
group.banksMapByName.get(tgt)![0].mint,
|
||||
1,
|
||||
)!
|
||||
.div(
|
||||
I80F48.fromNumber(
|
||||
Math.pow(10, group.banksMapByName.get(src)![0].mintDecimals),
|
||||
),
|
||||
)
|
||||
.toNumber(),
|
||||
);
|
||||
}
|
||||
for (const srcToken of Array.from(group.banksMapByName.keys())) {
|
||||
|
@ -130,6 +133,28 @@ async function debugUser(
|
|||
}
|
||||
}
|
||||
|
||||
function getMaxForPerpWrapper(perpMarket: PerpMarket) {
|
||||
console.log(
|
||||
`getMaxQuoteForPerpBidUi ${perpMarket.name} ` +
|
||||
mangoAccount.getMaxQuoteForPerpBidUi(
|
||||
group,
|
||||
perpMarket.name,
|
||||
perpMarket.price,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
`getMaxBaseForPerpAskUi ${perpMarket.name} ` +
|
||||
mangoAccount.getMaxBaseForPerpAskUi(
|
||||
group,
|
||||
perpMarket.name,
|
||||
perpMarket.price,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (const perpMarket of Array.from(group.perpMarketsMap.values())) {
|
||||
getMaxForPerpWrapper(perpMarket);
|
||||
}
|
||||
|
||||
function getMaxForSerum3Wrapper(serum3Market: Serum3Market) {
|
||||
// if (serum3Market.name !== 'SOL/USDC') return;
|
||||
console.log(
|
||||
|
@ -156,12 +181,10 @@ async function debugUser(
|
|||
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(process.env.MB_CLUSTER_URL!, options);
|
||||
const connection = new Connection(CLUSTER_URL!, options);
|
||||
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(fs.readFileSync(process.env.MB_PAYER_KEYPAIR!, 'utf-8')),
|
||||
),
|
||||
Buffer.from(JSON.parse(fs.readFileSync(PAYER_KEYPAIR!, 'utf-8'))),
|
||||
);
|
||||
console.log(`Admin ${admin.publicKey.toBase58()}`);
|
||||
|
||||
|
@ -169,16 +192,15 @@ async function main() {
|
|||
const adminProvider = new AnchorProvider(connection, adminWallet, options);
|
||||
const client = MangoClient.connect(
|
||||
adminProvider,
|
||||
'mainnet-beta',
|
||||
MANGO_V4_ID['mainnet-beta'],
|
||||
CLUSTER,
|
||||
MANGO_V4_ID[CLUSTER],
|
||||
{},
|
||||
'get-program-accounts',
|
||||
);
|
||||
|
||||
const group = await client.getGroupForCreator(admin.publicKey, 2);
|
||||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
|
||||
for (const keypair of [
|
||||
process.env.MB_PAYER_KEYPAIR!,
|
||||
process.env.MB_USER2_KEYPAIR!,
|
||||
]) {
|
||||
for (const keypair of [USER_KEYPAIR!]) {
|
||||
console.log();
|
||||
const user = Keypair.fromSecretKey(
|
||||
Buffer.from(JSON.parse(fs.readFileSync(keypair, 'utf-8'))),
|
||||
|
|
|
@ -357,7 +357,6 @@ async function main() {
|
|||
0,
|
||||
'BTC-PERP',
|
||||
0.1,
|
||||
1,
|
||||
6,
|
||||
1,
|
||||
10,
|
||||
|
@ -372,15 +371,14 @@ async function main() {
|
|||
0.05,
|
||||
0.05,
|
||||
100,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
console.log('done');
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
const perpMarkets = await client.perpGetMarkets(
|
||||
group,
|
||||
group.getFirstBankByMint(btcDevnetMint).tokenIndex,
|
||||
);
|
||||
const perpMarkets = await client.perpGetMarkets(group);
|
||||
console.log(`...created perp market ${perpMarkets[0].publicKey}`);
|
||||
|
||||
//
|
||||
|
@ -480,6 +478,34 @@ async function main() {
|
|||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log(`Editing BTC-PERP...`);
|
||||
try {
|
||||
let sig = await client.perpEditMarket(
|
||||
group,
|
||||
'BTC-PERP',
|
||||
btcDevnetOracle,
|
||||
0.1,
|
||||
6,
|
||||
0.975,
|
||||
0.95,
|
||||
1.025,
|
||||
1.05,
|
||||
0.012,
|
||||
0.0002,
|
||||
0.0,
|
||||
0.05,
|
||||
0.05,
|
||||
100,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.getFirstBankByMint(btcDevnetMint).toString());
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
process.exit();
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { I80F48 } from '../accounts/I80F48';
|
||||
import { HealthType } from '../accounts/mangoAccount';
|
||||
import { BookSide, PerpOrderType, Side } from '../accounts/perp';
|
||||
import {
|
||||
|
@ -350,19 +349,12 @@ async function main() {
|
|||
// console.log();
|
||||
console.log(
|
||||
`getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` +
|
||||
mangoAccount
|
||||
.getMaxSourceForTokenSwap(
|
||||
mangoAccount.getMaxSourceUiForTokenSwap(
|
||||
group,
|
||||
group.banksMapByName.get(src)![0].mint,
|
||||
group.banksMapByName.get(tgt)![0].mint,
|
||||
1,
|
||||
)!
|
||||
.div(
|
||||
I80F48.fromNumber(
|
||||
Math.pow(10, group.banksMapByName.get(src)![0].mintDecimals),
|
||||
),
|
||||
)
|
||||
.toNumber(),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
for (const srcToken of Array.from(group.banksMapByName.keys())) {
|
||||
|
@ -407,39 +399,45 @@ async function main() {
|
|||
|
||||
// perps
|
||||
if (true) {
|
||||
let sig;
|
||||
const orders = await mangoAccount.loadPerpOpenOrdersForMarket(
|
||||
client,
|
||||
group,
|
||||
'BTC-PERP',
|
||||
);
|
||||
for (const order of orders) {
|
||||
console.log(`Current order - ${order.price} ${order.size} ${order.side}`);
|
||||
console.log(
|
||||
`Current order - ${order.uiPrice} ${order.uiSize} ${order.side}`,
|
||||
);
|
||||
}
|
||||
console.log(`...cancelling all perp orders`);
|
||||
let sig = await client.perpCancelAllOrders(
|
||||
group,
|
||||
mangoAccount,
|
||||
'BTC-PERP',
|
||||
10,
|
||||
);
|
||||
sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
|
||||
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
|
||||
// scenario 1
|
||||
// not going to be hit orders, far from each other
|
||||
// bid max perp
|
||||
try {
|
||||
const clientId = Math.floor(Math.random() * 99999);
|
||||
const price =
|
||||
group.banksMapByName.get('BTC')![0].uiPrice! -
|
||||
Math.floor(Math.random() * 100);
|
||||
console.log(`...placing perp bid ${clientId} at ${price}`);
|
||||
const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi(
|
||||
group,
|
||||
'BTC-PERP',
|
||||
1,
|
||||
);
|
||||
const baseQty = quoteQty / price;
|
||||
console.log(
|
||||
`...placing max qty perp bid clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
|
||||
);
|
||||
const sig = await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
'BTC-PERP',
|
||||
Side.bid,
|
||||
price,
|
||||
0.01,
|
||||
price * 0.01,
|
||||
baseQty,
|
||||
quoteQty,
|
||||
clientId,
|
||||
PerpOrderType.limit,
|
||||
0, //Date.now() + 200,
|
||||
|
@ -449,20 +447,59 @@ async function main() {
|
|||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
console.log(`...cancelling all perp orders`);
|
||||
sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
|
||||
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
|
||||
// bid max perp + some
|
||||
try {
|
||||
const clientId = Math.floor(Math.random() * 99999);
|
||||
const price =
|
||||
group.banksMapByName.get('BTC')![0].uiPrice! -
|
||||
Math.floor(Math.random() * 100);
|
||||
const quoteQty =
|
||||
mangoAccount.getMaxQuoteForPerpBidUi(group, 'BTC-PERP', 1) * 1.02;
|
||||
const baseQty = quoteQty / price;
|
||||
console.log(
|
||||
`...placing max qty * 1.02 perp bid clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
|
||||
);
|
||||
const sig = await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
'BTC-PERP',
|
||||
Side.bid,
|
||||
price,
|
||||
baseQty,
|
||||
quoteQty,
|
||||
clientId,
|
||||
PerpOrderType.limit,
|
||||
0, //Date.now() + 200,
|
||||
1,
|
||||
);
|
||||
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
} catch (error) {
|
||||
console.log('Errored out as expected');
|
||||
}
|
||||
|
||||
// bid max ask
|
||||
try {
|
||||
const clientId = Math.floor(Math.random() * 99999);
|
||||
const price =
|
||||
group.banksMapByName.get('BTC')![0].uiPrice! +
|
||||
Math.floor(Math.random() * 100);
|
||||
console.log(`...placing perp ask ${clientId} at ${price}`);
|
||||
const baseQty = mangoAccount.getMaxBaseForPerpAskUi(group, 'BTC-PERP', 1);
|
||||
const quoteQty = baseQty * price;
|
||||
console.log(
|
||||
`...placing max qty perp ask clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
|
||||
);
|
||||
const sig = await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
'BTC-PERP',
|
||||
Side.ask,
|
||||
price,
|
||||
0.01,
|
||||
price * 0.01,
|
||||
baseQty,
|
||||
quoteQty,
|
||||
clientId,
|
||||
PerpOrderType.limit,
|
||||
0, //Date.now() + 200,
|
||||
|
@ -472,59 +509,89 @@ async function main() {
|
|||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
// should be able to cancel them
|
||||
|
||||
// bid max ask + some
|
||||
try {
|
||||
const clientId = Math.floor(Math.random() * 99999);
|
||||
const price =
|
||||
group.banksMapByName.get('BTC')![0].uiPrice! +
|
||||
Math.floor(Math.random() * 100);
|
||||
const baseQty =
|
||||
mangoAccount.getMaxBaseForPerpAskUi(group, 'BTC-PERP', 1) * 1.02;
|
||||
const quoteQty = baseQty * price;
|
||||
console.log(
|
||||
`...placing max qty perp ask * 1.02 clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
|
||||
);
|
||||
const sig = await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
'BTC-PERP',
|
||||
Side.ask,
|
||||
price,
|
||||
baseQty,
|
||||
quoteQty,
|
||||
clientId,
|
||||
PerpOrderType.limit,
|
||||
0, //Date.now() + 200,
|
||||
1,
|
||||
);
|
||||
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
} catch (error) {
|
||||
console.log('Errored out as expected');
|
||||
}
|
||||
|
||||
console.log(`...cancelling all perp orders`);
|
||||
sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
|
||||
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
|
||||
// scenario 2
|
||||
// make + take orders
|
||||
try {
|
||||
const clientId = Math.floor(Math.random() * 99999);
|
||||
const price = group.banksMapByName.get('BTC')![0].uiPrice!;
|
||||
console.log(`...placing perp bid ${clientId} at ${price}`);
|
||||
const sig = await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
'BTC-PERP',
|
||||
Side.bid,
|
||||
price,
|
||||
0.01,
|
||||
price * 0.01,
|
||||
clientId,
|
||||
PerpOrderType.limit,
|
||||
0, //Date.now() + 200,
|
||||
1,
|
||||
);
|
||||
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
try {
|
||||
const clientId = Math.floor(Math.random() * 99999);
|
||||
const price = group.banksMapByName.get('BTC')![0].uiPrice!;
|
||||
console.log(`...placing perp ask ${clientId} at ${price}`);
|
||||
const sig = await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
'BTC-PERP',
|
||||
Side.ask,
|
||||
price,
|
||||
0.01,
|
||||
price * 0.011,
|
||||
clientId,
|
||||
PerpOrderType.limit,
|
||||
0, //Date.now() + 200,
|
||||
1,
|
||||
);
|
||||
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
// // should be able to cancel them : know bug
|
||||
// console.log(`...cancelling all perp orders`);
|
||||
// sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
|
||||
// // scenario 2
|
||||
// // make + take orders
|
||||
// try {
|
||||
// const clientId = Math.floor(Math.random() * 99999);
|
||||
// const price = group.banksMapByName.get('BTC')![0].uiPrice!;
|
||||
// console.log(`...placing perp bid ${clientId} at ${price}`);
|
||||
// const sig = await client.perpPlaceOrder(
|
||||
// group,
|
||||
// mangoAccount,
|
||||
// 'BTC-PERP',
|
||||
// Side.bid,
|
||||
// price,
|
||||
// 0.01,
|
||||
// price * 0.01,
|
||||
// clientId,
|
||||
// PerpOrderType.limit,
|
||||
// 0, //Date.now() + 200,
|
||||
// 1,
|
||||
// );
|
||||
// console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// }
|
||||
// try {
|
||||
// const clientId = Math.floor(Math.random() * 99999);
|
||||
// const price = group.banksMapByName.get('BTC')![0].uiPrice!;
|
||||
// console.log(`...placing perp ask ${clientId} at ${price}`);
|
||||
// const sig = await client.perpPlaceOrder(
|
||||
// group,
|
||||
// mangoAccount,
|
||||
// 'BTC-PERP',
|
||||
// Side.ask,
|
||||
// price,
|
||||
// 0.01,
|
||||
// price * 0.011,
|
||||
// clientId,
|
||||
// PerpOrderType.limit,
|
||||
// 0, //Date.now() + 200,
|
||||
// 1,
|
||||
// );
|
||||
// console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// }
|
||||
// // // should be able to cancel them : know bug
|
||||
// // console.log(`...cancelling all perp orders`);
|
||||
// // sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
|
||||
// // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
|
||||
const perpMarket = group.perpMarketsMap.get('BTC-PERP');
|
||||
|
||||
|
|
Loading…
Reference in New Issue