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)
|
instructions::perp_close_market(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO perp_change_perp_market_params
|
||||||
|
|
||||||
pub fn perp_deactivate_position(ctx: Context<PerpDeactivatePosition>) -> Result<()> {
|
pub fn perp_deactivate_position(ctx: Context<PerpDeactivatePosition>) -> Result<()> {
|
||||||
instructions::perp_deactivate_position(ctx)
|
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
|
/// 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
|
/// 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.
|
/// so 1e6 native_BTC gives you 500e9 native_SOL.
|
||||||
|
///
|
||||||
|
/// NOTE: keep getMaxSourceForTokenSwap in ts/client in sync with changes here
|
||||||
#[cfg(feature = "client")]
|
#[cfg(feature = "client")]
|
||||||
pub fn max_swap_source_for_health_ratio(
|
pub fn max_swap_source_for_health_ratio(
|
||||||
&self,
|
&self,
|
||||||
|
@ -1018,6 +1020,7 @@ impl HealthCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "client")]
|
#[cfg(feature = "client")]
|
||||||
|
/// NOTE: keep getMaxSourceForTokenSwap in ts/client in sync with changes here
|
||||||
pub fn max_perp_for_health_ratio(
|
pub fn max_perp_for_health_ratio(
|
||||||
&self,
|
&self,
|
||||||
perp_market_index: PerpMarketIndex,
|
perp_market_index: PerpMarketIndex,
|
||||||
|
|
|
@ -10,7 +10,18 @@ export type OracleConfig = {
|
||||||
confFilter: I80F48Dto;
|
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 name: string;
|
||||||
public depositIndex: I80F48;
|
public depositIndex: I80F48;
|
||||||
public borrowIndex: I80F48;
|
public borrowIndex: I80F48;
|
||||||
|
|
|
@ -256,7 +256,6 @@ export class Group {
|
||||||
bank.oracle,
|
bank.oracle,
|
||||||
ai,
|
ai,
|
||||||
this.getMintDecimals(bank.mint),
|
this.getMintDecimals(bank.mint),
|
||||||
this.getMintDecimals(this.insuranceMint),
|
|
||||||
);
|
);
|
||||||
bank.price = price;
|
bank.price = price;
|
||||||
bank.uiPrice = uiPrice;
|
bank.uiPrice = uiPrice;
|
||||||
|
@ -283,7 +282,6 @@ export class Group {
|
||||||
perpMarket.oracle,
|
perpMarket.oracle,
|
||||||
ai,
|
ai,
|
||||||
perpMarket.baseDecimals,
|
perpMarket.baseDecimals,
|
||||||
this.getMintDecimals(this.insuranceMint),
|
|
||||||
);
|
);
|
||||||
perpMarket.price = price;
|
perpMarket.price = price;
|
||||||
perpMarket.uiPrice = uiPrice;
|
perpMarket.uiPrice = uiPrice;
|
||||||
|
@ -295,7 +293,6 @@ export class Group {
|
||||||
oracle: PublicKey,
|
oracle: PublicKey,
|
||||||
ai: AccountInfo<Buffer>,
|
ai: AccountInfo<Buffer>,
|
||||||
baseDecimals: number,
|
baseDecimals: number,
|
||||||
quoteDecimals: number,
|
|
||||||
) {
|
) {
|
||||||
let price, uiPrice;
|
let price, uiPrice;
|
||||||
if (
|
if (
|
||||||
|
@ -305,13 +302,13 @@ export class Group {
|
||||||
) {
|
) {
|
||||||
const stubOracle = coder.decode('stubOracle', ai.data);
|
const stubOracle = coder.decode('stubOracle', ai.data);
|
||||||
price = new I80F48(stubOracle.price.val);
|
price = new I80F48(stubOracle.price.val);
|
||||||
uiPrice = this?.toUiPrice(price, baseDecimals, quoteDecimals);
|
uiPrice = this?.toUiPrice(price, baseDecimals);
|
||||||
} else if (isPythOracle(ai)) {
|
} else if (isPythOracle(ai)) {
|
||||||
uiPrice = parsePriceData(ai.data).previousPrice;
|
uiPrice = parsePriceData(ai.data).previousPrice;
|
||||||
price = this?.toNativePrice(uiPrice, baseDecimals, quoteDecimals);
|
price = this?.toNativePrice(uiPrice, baseDecimals);
|
||||||
} else if (isSwitchboardOracle(ai)) {
|
} else if (isSwitchboardOracle(ai)) {
|
||||||
uiPrice = await parseSwitchboardOracle(ai);
|
uiPrice = await parseSwitchboardOracle(ai);
|
||||||
price = this?.toNativePrice(uiPrice, baseDecimals, quoteDecimals);
|
price = this?.toNativePrice(uiPrice, baseDecimals);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown oracle provider for oracle ${oracle}, with owner ${ai.owner}`,
|
`Unknown oracle provider for oracle ${oracle}, with owner ${ai.owner}`,
|
||||||
|
@ -347,6 +344,10 @@ export class Group {
|
||||||
return banks[0].mintDecimals;
|
return banks[0].mintDecimals;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getInsuranceMintDecimals(): number {
|
||||||
|
return this.getMintDecimals(this.insuranceMint);
|
||||||
|
}
|
||||||
|
|
||||||
public getFirstBankByMint(mintPk: PublicKey): Bank {
|
public getFirstBankByMint(mintPk: PublicKey): Bank {
|
||||||
const banks = this.banksMapByMint.get(mintPk.toString());
|
const banks = this.banksMapByMint.get(mintPk.toString());
|
||||||
if (!banks) throw new Error(`Unable to find bank for ${mintPk.toString()}`);
|
if (!banks) throw new Error(`Unable to find bank for ${mintPk.toString()}`);
|
||||||
|
@ -478,23 +479,26 @@ export class Group {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public toUiPrice(
|
public toUiPrice(price: I80F48, baseDecimals: number): number {
|
||||||
price: I80F48,
|
|
||||||
baseDecimals: number,
|
|
||||||
quoteDecimals: number,
|
|
||||||
): number {
|
|
||||||
return price
|
return price
|
||||||
.mul(I80F48.fromNumber(Math.pow(10, baseDecimals - quoteDecimals)))
|
.mul(
|
||||||
|
I80F48.fromNumber(
|
||||||
|
Math.pow(10, baseDecimals - this.getInsuranceMintDecimals()),
|
||||||
|
),
|
||||||
|
)
|
||||||
.toNumber();
|
.toNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
public toNativePrice(
|
public toNativePrice(uiPrice: number, baseDecimals: number): I80F48 {
|
||||||
uiPrice: number,
|
|
||||||
baseDecimals: number,
|
|
||||||
quoteDecimals: number,
|
|
||||||
): I80F48 {
|
|
||||||
return I80F48.fromNumber(uiPrice).mul(
|
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 { PublicKey } from '@solana/web3.js';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Bank } from './bank';
|
import { Bank, BankForHealth } from './bank';
|
||||||
import { Group } from './group';
|
import { Group } from './group';
|
||||||
import {
|
import {
|
||||||
HUNDRED_I80F48,
|
HUNDRED_I80F48,
|
||||||
|
@ -11,6 +11,7 @@ import {
|
||||||
ZERO_I80F48,
|
ZERO_I80F48,
|
||||||
} from './I80F48';
|
} from './I80F48';
|
||||||
import { HealthType } from './mangoAccount';
|
import { HealthType } from './mangoAccount';
|
||||||
|
import { PerpMarket, PerpOrderSide } from './perp';
|
||||||
import { Serum3Market, Serum3Side } from './serum3';
|
import { Serum3Market, Serum3Side } from './serum3';
|
||||||
|
|
||||||
// ░░░░
|
// ░░░░
|
||||||
|
@ -45,10 +46,26 @@ export class HealthCache {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
static fromDto(dto) {
|
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(
|
return new HealthCache(
|
||||||
dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)),
|
dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)),
|
||||||
dto.serum3Infos.map((dto) => Serum3Info.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);
|
const index = this.findTokenInfoIndex(bank.tokenIndex);
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
this.tokenInfos.push(TokenInfo.emptyFromBank(bank));
|
this.tokenInfos.push(TokenInfo.emptyFromBank(bank));
|
||||||
|
@ -242,6 +259,20 @@ export class HealthCache {
|
||||||
serum3Info.reserved = serum3Info.reserved.add(reservedAmount);
|
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) {
|
public static logHealthCache(debug: string, healthCache: HealthCache) {
|
||||||
if (debug) console.log(debug);
|
if (debug) console.log(debug);
|
||||||
for (const token of healthCache.tokenInfos) {
|
for (const token of healthCache.tokenInfos) {
|
||||||
|
@ -407,19 +438,17 @@ export class HealthCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
getMaxSourceForTokenSwap(
|
getMaxSourceForTokenSwap(
|
||||||
group: Group,
|
sourceBank: BankForHealth,
|
||||||
sourceMintPk: PublicKey,
|
targetBank: BankForHealth,
|
||||||
targetMintPk: PublicKey,
|
|
||||||
minRatio: I80F48,
|
minRatio: I80F48,
|
||||||
|
priceFactor: I80F48,
|
||||||
): I80F48 {
|
): I80F48 {
|
||||||
const sourceBank: Bank = group.getFirstBankByMint(sourceMintPk);
|
if (
|
||||||
const targetBank: Bank = group.getFirstBankByMint(targetMintPk);
|
!sourceBank.price ||
|
||||||
|
sourceBank.price.lte(ZERO_I80F48()) ||
|
||||||
if (sourceMintPk.equals(targetMintPk)) {
|
!targetBank.price ||
|
||||||
return ZERO_I80F48();
|
targetBank.price.lte(ZERO_I80F48())
|
||||||
}
|
) {
|
||||||
|
|
||||||
if (!sourceBank.price || sourceBank.price.lte(ZERO_I80F48())) {
|
|
||||||
return ZERO_I80F48();
|
return ZERO_I80F48();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -441,10 +470,21 @@ export class HealthCache {
|
||||||
// - be careful about finding the minRatio point: the function isn't convex
|
// - be careful about finding the minRatio point: the function isn't convex
|
||||||
|
|
||||||
const initialRatio = this.healthRatio(HealthType.init);
|
const initialRatio = this.healthRatio(HealthType.init);
|
||||||
|
const initialHealth = this.health(HealthType.init);
|
||||||
if (initialRatio.lte(ZERO_I80F48())) {
|
if (initialRatio.lte(ZERO_I80F48())) {
|
||||||
return 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 healthCacheClone: HealthCache = _.cloneDeep(this);
|
||||||
const sourceIndex = healthCacheClone.getOrCreateTokenInfoIndex(sourceBank);
|
const sourceIndex = healthCacheClone.getOrCreateTokenInfoIndex(sourceBank);
|
||||||
const targetIndex = healthCacheClone.getOrCreateTokenInfoIndex(targetBank);
|
const targetIndex = healthCacheClone.getOrCreateTokenInfoIndex(targetBank);
|
||||||
|
@ -461,7 +501,9 @@ export class HealthCache {
|
||||||
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
|
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
|
||||||
// HealthCache.logHealthCache('beforeSwap', adjustedCache);
|
// HealthCache.logHealthCache('beforeSwap', adjustedCache);
|
||||||
adjustedCache.tokenInfos[sourceIndex].balance.isub(amount);
|
adjustedCache.tokenInfos[sourceIndex].balance.isub(amount);
|
||||||
adjustedCache.tokenInfos[targetIndex].balance.iadd(amount);
|
adjustedCache.tokenInfos[targetIndex].balance.iadd(
|
||||||
|
amount.mul(priceFactor),
|
||||||
|
);
|
||||||
// HealthCache.logHealthCache('afterSwap', adjustedCache);
|
// HealthCache.logHealthCache('afterSwap', adjustedCache);
|
||||||
return adjustedCache;
|
return adjustedCache;
|
||||||
}
|
}
|
||||||
|
@ -470,11 +512,16 @@ export class HealthCache {
|
||||||
return cacheAfterSwap(amount).healthRatio(HealthType.init);
|
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
|
const point0Amount = source.balance
|
||||||
.min(target.balance.neg())
|
.min(sourceForZeroTargetBalance)
|
||||||
.max(ZERO_I80F48());
|
.max(ZERO_I80F48());
|
||||||
const point1Amount = source.balance
|
const point1Amount = source.balance
|
||||||
.max(target.balance.neg())
|
.max(sourceForZeroTargetBalance)
|
||||||
.max(ZERO_I80F48());
|
.max(ZERO_I80F48());
|
||||||
const cache0 = cacheAfterSwap(point0Amount);
|
const cache0 = cacheAfterSwap(point0Amount);
|
||||||
const point0Ratio = cache0.healthRatio(HealthType.init);
|
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
|
// If point1Ratio is still bigger than minRatio, the target amount must be >point1Amount
|
||||||
// search to the right of point1Amount: but how far?
|
// search to the right of point1Amount: but how far?
|
||||||
// At point1, source.balance < 0 and target.balance > 0, so use a simple estimation for
|
// 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())) {
|
if (point1Health.lte(ZERO_I80F48())) {
|
||||||
return ZERO_I80F48();
|
return ZERO_I80F48();
|
||||||
}
|
}
|
||||||
const zeroHealthAmount = point1Amount.add(
|
const zeroHealthAmount = point1Amount.sub(
|
||||||
point1Health.div(source.initLiabWeight.sub(target.initAssetWeight)),
|
point1Health.div(finalHealthSlope),
|
||||||
);
|
);
|
||||||
const zeroHealthRatio = healthRatioAfterSwap(zeroHealthAmount);
|
const zeroHealthRatio = healthRatioAfterSwap(zeroHealthAmount);
|
||||||
amount = HealthCache.binaryApproximationSearch(
|
amount = HealthCache.binaryApproximationSearch(
|
||||||
|
@ -532,21 +579,21 @@ export class HealthCache {
|
||||||
healthRatioAfterSwap,
|
healthRatioAfterSwap,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
// Must be between 0 and point0_amount
|
||||||
`internal error: assert that init ratio ${initialRatio.toNumber()} <= point0 ratio ${point0Ratio.toNumber()}`,
|
amount = HealthCache.binaryApproximationSearch(
|
||||||
|
ZERO_I80F48(),
|
||||||
|
initialRatio,
|
||||||
|
point0Amount,
|
||||||
|
point0Ratio,
|
||||||
|
minRatio,
|
||||||
|
healthRatioAfterSwap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return amount
|
return amount.div(source.oraclePrice);
|
||||||
.div(source.oraclePrice)
|
|
||||||
.div(
|
|
||||||
ONE_I80F48().add(
|
|
||||||
group.getFirstBankByMint(sourceMintPk).loanOriginationFeeRate,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getMaxForSerum3Order(
|
getMaxSerum3OrderForHealthRatio(
|
||||||
group: Group,
|
group: Group,
|
||||||
serum3Market: Serum3Market,
|
serum3Market: Serum3Market,
|
||||||
side: Serum3Side,
|
side: Serum3Side,
|
||||||
|
@ -669,6 +716,115 @@ export class HealthCache {
|
||||||
.div(ONE_I80F48().add(quoteBank.loanOriginationFeeRate))
|
.div(ONE_I80F48().add(quoteBank.loanOriginationFeeRate))
|
||||||
.div(ONE_I80F48().add(I80F48.fromNumber(group.getFeeRate(false))));
|
.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 {
|
export class TokenInfo {
|
||||||
|
@ -699,10 +855,10 @@ export class TokenInfo {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static emptyFromBank(bank: Bank): TokenInfo {
|
static emptyFromBank(bank: BankForHealth): TokenInfo {
|
||||||
if (!bank.price)
|
if (!bank.price)
|
||||||
throw new Error(
|
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(
|
return new TokenInfo(
|
||||||
bank.tokenIndex,
|
bank.tokenIndex,
|
||||||
|
@ -824,22 +980,33 @@ export class Serum3Info {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PerpInfo {
|
export class PerpInfo {
|
||||||
constructor(dto: PerpInfoDto) {
|
constructor(
|
||||||
this.maintAssetWeight = I80F48.from(dto.maintAssetWeight);
|
public perpMarketIndex: number,
|
||||||
this.initAssetWeight = I80F48.from(dto.initAssetWeight);
|
public maintAssetWeight: I80F48,
|
||||||
this.maintLiabWeight = I80F48.from(dto.maintLiabWeight);
|
public initAssetWeight: I80F48,
|
||||||
this.initLiabWeight = I80F48.from(dto.initLiabWeight);
|
public maintLiabWeight: I80F48,
|
||||||
this.base = I80F48.from(dto.base);
|
public initLiabWeight: I80F48,
|
||||||
this.quote = I80F48.from(dto.quote);
|
// in health-reference-token native units, needs scaling by asset/liab
|
||||||
|
public base: I80F48,
|
||||||
|
// in health-reference-token native units, no asset/liab factor needed
|
||||||
|
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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
maintAssetWeight: I80F48;
|
|
||||||
initAssetWeight: I80F48;
|
|
||||||
maintLiabWeight: I80F48;
|
|
||||||
initLiabWeight: I80F48;
|
|
||||||
// in health-reference-token native units, needs scaling by asset/liab
|
|
||||||
base: I80F48;
|
|
||||||
// in health-reference-token native units, no asset/liab factor needed
|
|
||||||
quote: I80F48;
|
|
||||||
|
|
||||||
healthContribution(healthType: HealthType): I80F48 {
|
healthContribution(healthType: HealthType): I80F48 {
|
||||||
let weight;
|
let weight;
|
||||||
|
@ -859,6 +1026,24 @@ export class PerpInfo {
|
||||||
// `self.quote + weight * self.base` here
|
// `self.quote + weight * self.base` here
|
||||||
return this.quote.add(weight.mul(this.base)).min(ZERO_I80F48());
|
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 {
|
export class HealthCacheDto {
|
||||||
|
@ -913,6 +1098,7 @@ export class Serum3InfoDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PerpInfoDto {
|
export class PerpInfoDto {
|
||||||
|
perpMarketIndex: number;
|
||||||
maintAssetWeight: I80F48Dto;
|
maintAssetWeight: I80F48Dto;
|
||||||
initAssetWeight: I80F48Dto;
|
initAssetWeight: I80F48Dto;
|
||||||
maintLiabWeight: I80F48Dto;
|
maintLiabWeight: I80F48Dto;
|
||||||
|
@ -921,4 +1107,6 @@ export class PerpInfoDto {
|
||||||
base: I80F48Dto;
|
base: I80F48Dto;
|
||||||
// in health-reference-token native units, no asset/liab factor needed
|
// in health-reference-token native units, no asset/liab factor needed
|
||||||
quote: I80F48Dto;
|
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 { Order, Orderbook } from '@project-serum/serum/lib/market';
|
||||||
import { PublicKey } from '@solana/web3.js';
|
import { PublicKey } from '@solana/web3.js';
|
||||||
import { MangoClient } from '../client';
|
import { MangoClient } from '../client';
|
||||||
import { nativeI80F48ToUi, toNative, toUiDecimals } from '../utils';
|
import {
|
||||||
|
nativeI80F48ToUi,
|
||||||
|
toNative,
|
||||||
|
toUiDecimals,
|
||||||
|
toUiDecimalsForQuote,
|
||||||
|
} from '../utils';
|
||||||
import { Bank } from './bank';
|
import { Bank } from './bank';
|
||||||
import { Group } from './group';
|
import { Group } from './group';
|
||||||
import { HealthCache, HealthCacheDto } from './healthCache';
|
import { HealthCache, HealthCacheDto } from './healthCache';
|
||||||
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48';
|
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48';
|
||||||
import { PerpOrder } from './perp';
|
import { PerpOrder, PerpOrderSide } from './perp';
|
||||||
import { Serum3Market, Serum3Side } from './serum3';
|
import { Serum3Side } from './serum3';
|
||||||
export class MangoAccount {
|
export class MangoAccount {
|
||||||
public tokens: TokenPosition[];
|
public tokens: TokenPosition[];
|
||||||
public serum3: Serum3Orders[];
|
public serum3: Serum3Orders[];
|
||||||
|
@ -310,13 +315,6 @@ export class MangoAccount {
|
||||||
// can withdraw without borrowing until initHealth reaches 0
|
// can withdraw without borrowing until initHealth reaches 0
|
||||||
if (existingPositionHealthContrib.gt(initHealth)) {
|
if (existingPositionHealthContrib.gt(initHealth)) {
|
||||||
const withdrawAbleExistingPositionHealthContrib = initHealth;
|
const withdrawAbleExistingPositionHealthContrib = initHealth;
|
||||||
// console.log(`initHealth ${initHealth}`);
|
|
||||||
// console.log(
|
|
||||||
// `existingPositionHealthContrib ${existingPositionHealthContrib}`,
|
|
||||||
// );
|
|
||||||
// console.log(
|
|
||||||
// `withdrawAbleExistingPositionHealthContrib ${withdrawAbleExistingPositionHealthContrib}`,
|
|
||||||
// );
|
|
||||||
return withdrawAbleExistingPositionHealthContrib
|
return withdrawAbleExistingPositionHealthContrib
|
||||||
.div(tokenBank.initAssetWeight)
|
.div(tokenBank.initAssetWeight)
|
||||||
.div(tokenBank.price);
|
.div(tokenBank.price);
|
||||||
|
@ -332,15 +330,6 @@ export class MangoAccount {
|
||||||
const maxBorrowNativeWithoutFees = maxBorrowNative.div(
|
const maxBorrowNativeWithoutFees = maxBorrowNative.div(
|
||||||
ONE_I80F48().add(tokenBank.loanOriginationFeeRate),
|
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);
|
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.
|
* The max amount of given source ui token you can swap to a target token.
|
||||||
* note: slippageAndFeesFactor is a normalized number, <1,
|
* PriceFactor is ratio between A - how many source tokens can be traded for target tokens
|
||||||
* e.g. a slippage of 5% and some fees which are 1%, then slippageAndFeesFactor = 0.94
|
* 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
|
* 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,
|
||||||
slippageAndFeesFactor: number,
|
priceFactor: number,
|
||||||
): number | undefined {
|
): number | undefined {
|
||||||
const maxSource = this.getMaxSourceForTokenSwap(
|
if (!this.accountData) {
|
||||||
group,
|
throw new Error(
|
||||||
sourceMintPk,
|
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||||
targetMintPk,
|
);
|
||||||
slippageAndFeesFactor,
|
}
|
||||||
|
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) {
|
if (maxSource) {
|
||||||
return toUiDecimals(maxSource, group.getMintDecimals(sourceMintPk));
|
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.
|
* Simulates new health ratio after applying tokenChanges to the token positions.
|
||||||
* Note: token changes are expected in ui amounts
|
* Note: token changes are expected in ui amounts
|
||||||
|
@ -506,28 +463,11 @@ export class MangoAccount {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* TODO priceFactor
|
||||||
* @param group
|
* @param group
|
||||||
* @param serum3Market
|
* @param externalMarketPk
|
||||||
* @returns maximum native quote which can be traded for base token given current health
|
* @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(
|
public getMaxQuoteForSerum3BidUi(
|
||||||
group: Group,
|
group: Group,
|
||||||
externalMarketPk: PublicKey,
|
externalMarketPk: PublicKey,
|
||||||
|
@ -540,7 +480,18 @@ export class MangoAccount {
|
||||||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
`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(
|
return toUiDecimals(
|
||||||
nativeAmount,
|
nativeAmount,
|
||||||
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex).mintDecimals,
|
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex).mintDecimals,
|
||||||
|
@ -548,28 +499,11 @@ export class MangoAccount {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* TODO priceFactor
|
||||||
* @param group
|
* @param group
|
||||||
* @param serum3Market
|
* @param externalMarketPk
|
||||||
* @returns maximum native base which can be traded for quote token given current health
|
* @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(
|
public getMaxBaseForSerum3AskUi(
|
||||||
group: Group,
|
group: Group,
|
||||||
externalMarketPk: PublicKey,
|
externalMarketPk: PublicKey,
|
||||||
|
@ -582,7 +516,18 @@ export class MangoAccount {
|
||||||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
`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(
|
return toUiDecimals(
|
||||||
nativeAmount,
|
nativeAmount,
|
||||||
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex).mintDecimals,
|
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex).mintDecimals,
|
||||||
|
@ -592,31 +537,12 @@ export class MangoAccount {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param group
|
* @param group
|
||||||
* @param nativeQuoteAmount
|
* @param uiQuoteAmount
|
||||||
* @param serum3Market
|
* @param externalMarketPk
|
||||||
* @param healthType
|
* @param healthType
|
||||||
* @returns health ratio after a bid with nativeQuoteAmount is placed
|
* @returns health ratio after a bid with uiQuoteAmount is placed
|
||||||
*/
|
*/
|
||||||
simHealthRatioWithSerum3BidChanges(
|
public simHealthRatioWithSerum3BidUiChanges(
|
||||||
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(
|
|
||||||
group: Group,
|
group: Group,
|
||||||
uiQuoteAmount: number,
|
uiQuoteAmount: number,
|
||||||
externalMarketPk: PublicKey,
|
externalMarketPk: PublicKey,
|
||||||
|
@ -630,46 +556,34 @@ export class MangoAccount {
|
||||||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.simHealthRatioWithSerum3BidChanges(
|
|
||||||
group,
|
|
||||||
toNative(
|
|
||||||
uiQuoteAmount,
|
|
||||||
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex)
|
|
||||||
.mintDecimals,
|
|
||||||
),
|
|
||||||
serum3Market,
|
|
||||||
healthType,
|
|
||||||
).toNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param group
|
|
||||||
* @param nativeBaseAmount
|
|
||||||
* @param serum3Market
|
|
||||||
* @param healthType
|
|
||||||
* @returns health ratio after an ask with nativeBaseAmount is placed
|
|
||||||
*/
|
|
||||||
simHealthRatioWithSerum3AskChanges(
|
|
||||||
group: Group,
|
|
||||||
nativeBaseAmount: I80F48,
|
|
||||||
serum3Market: Serum3Market,
|
|
||||||
healthType: HealthType = HealthType.init,
|
|
||||||
): I80F48 {
|
|
||||||
if (!this.accountData) {
|
if (!this.accountData) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.accountData.healthCache.simHealthRatioWithSerum3AskChanges(
|
return this.accountData.healthCache
|
||||||
group,
|
.simHealthRatioWithSerum3BidChanges(
|
||||||
nativeBaseAmount,
|
group,
|
||||||
serum3Market,
|
toNative(
|
||||||
healthType,
|
uiQuoteAmount,
|
||||||
);
|
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex)
|
||||||
|
.mintDecimals,
|
||||||
|
),
|
||||||
|
serum3Market,
|
||||||
|
healthType,
|
||||||
|
)
|
||||||
|
.toNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
simHealthRatioWithSerum3AskUiChanges(
|
/**
|
||||||
|
*
|
||||||
|
* @param group
|
||||||
|
* @param uiBaseAmount
|
||||||
|
* @param externalMarketPk
|
||||||
|
* @param healthType
|
||||||
|
* @returns health ratio after an ask with uiBaseAmount is placed
|
||||||
|
*/
|
||||||
|
public simHealthRatioWithSerum3AskUiChanges(
|
||||||
group: Group,
|
group: Group,
|
||||||
uiBaseAmount: number,
|
uiBaseAmount: number,
|
||||||
externalMarketPk: PublicKey,
|
externalMarketPk: PublicKey,
|
||||||
|
@ -683,16 +597,87 @@ export class MangoAccount {
|
||||||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.simHealthRatioWithSerum3AskChanges(
|
if (!this.accountData) {
|
||||||
group,
|
throw new Error(
|
||||||
toNative(
|
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||||
uiBaseAmount,
|
);
|
||||||
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
|
}
|
||||||
.mintDecimals,
|
return this.accountData.healthCache
|
||||||
),
|
.simHealthRatioWithSerum3AskChanges(
|
||||||
serum3Market,
|
group,
|
||||||
healthType,
|
toNative(
|
||||||
).toNumber();
|
uiBaseAmount,
|
||||||
|
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
|
||||||
|
.mintDecimals,
|
||||||
|
),
|
||||||
|
serum3Market,
|
||||||
|
healthType,
|
||||||
|
)
|
||||||
|
.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(
|
public async loadPerpOpenOrdersForMarket(
|
||||||
|
|
|
@ -364,7 +364,7 @@ export class BookSide {
|
||||||
for (const order of this.items()) {
|
for (const order of this.items()) {
|
||||||
s.iadd(order.sizeLots);
|
s.iadd(order.sizeLots);
|
||||||
if (s.gte(baseLots)) {
|
if (s.gte(baseLots)) {
|
||||||
return order.price;
|
return order.uiPrice;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -391,7 +391,7 @@ export class BookSide {
|
||||||
|
|
||||||
public getL2Ui(depth: number): [number, number][] {
|
public getL2Ui(depth: number): [number, number][] {
|
||||||
const levels: [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) {
|
if (levels.length > 0 && levels[levels.length - 1][0] === price) {
|
||||||
levels[levels.length - 1][1] += size;
|
levels[levels.length - 1][1] += size;
|
||||||
} else if (levels.length === depth) {
|
} else if (levels.length === depth) {
|
||||||
|
@ -463,7 +463,7 @@ export class InnerNode {
|
||||||
constructor(public children: [number]) {}
|
constructor(public children: [number]) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Side {
|
export class PerpOrderSide {
|
||||||
static bid = { bid: {} };
|
static bid = { bid: {} };
|
||||||
static ask = { ask: {} };
|
static ask = { ask: {} };
|
||||||
}
|
}
|
||||||
|
@ -478,7 +478,8 @@ export class PerpOrderType {
|
||||||
|
|
||||||
export class PerpOrder {
|
export class PerpOrder {
|
||||||
static from(perpMarket: PerpMarket, leafNode: LeafNode, type: BookSideType) {
|
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 price = BookSide.getPriceFromKey(leafNode.key);
|
||||||
const expiryTimestamp = leafNode.timeInForce
|
const expiryTimestamp = leafNode.timeInForce
|
||||||
? leafNode.timestamp.add(new BN(leafNode.timeInForce))
|
? leafNode.timestamp.add(new BN(leafNode.timeInForce))
|
||||||
|
@ -506,11 +507,11 @@ export class PerpOrder {
|
||||||
public owner: PublicKey,
|
public owner: PublicKey,
|
||||||
public openOrdersSlot: number,
|
public openOrdersSlot: number,
|
||||||
public feeTier: 0,
|
public feeTier: 0,
|
||||||
public price: number,
|
public uiPrice: number,
|
||||||
public priceLots: BN,
|
public priceLots: BN,
|
||||||
public size: number,
|
public uiSize: number,
|
||||||
public sizeLots: BN,
|
public sizeLots: BN,
|
||||||
public side: Side,
|
public side: PerpOrderSide,
|
||||||
public timestamp: BN,
|
public timestamp: BN,
|
||||||
public expiryTimestamp: BN,
|
public expiryTimestamp: BN,
|
||||||
) {}
|
) {}
|
||||||
|
|
|
@ -34,7 +34,7 @@ import {
|
||||||
PerpPosition,
|
PerpPosition,
|
||||||
} from './accounts/mangoAccount';
|
} from './accounts/mangoAccount';
|
||||||
import { StubOracle } from './accounts/oracle';
|
import { StubOracle } from './accounts/oracle';
|
||||||
import { PerpMarket, PerpOrderType, Side } from './accounts/perp';
|
import { PerpMarket, PerpOrderSide, PerpOrderType } from './accounts/perp';
|
||||||
import {
|
import {
|
||||||
generateSerum3MarketExternalVaultSignerAddress,
|
generateSerum3MarketExternalVaultSignerAddress,
|
||||||
Serum3Market,
|
Serum3Market,
|
||||||
|
@ -1504,7 +1504,7 @@ export class MangoClient {
|
||||||
group: Group,
|
group: Group,
|
||||||
mangoAccount: MangoAccount,
|
mangoAccount: MangoAccount,
|
||||||
perpMarketName: string,
|
perpMarketName: string,
|
||||||
side: Side,
|
side: PerpOrderSide,
|
||||||
price: number,
|
price: number,
|
||||||
quantity: number,
|
quantity: number,
|
||||||
maxQuoteQuantity: number,
|
maxQuoteQuantity: number,
|
||||||
|
|
|
@ -1,14 +1,24 @@
|
||||||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
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 fs from 'fs';
|
||||||
import { Group } from '../accounts/group';
|
import { Group } from '../accounts/group';
|
||||||
import { I80F48 } from '../accounts/I80F48';
|
|
||||||
import { HealthType, MangoAccount } from '../accounts/mangoAccount';
|
import { HealthType, MangoAccount } from '../accounts/mangoAccount';
|
||||||
|
import { PerpMarket } from '../accounts/perp';
|
||||||
import { Serum3Market } from '../accounts/serum3';
|
import { Serum3Market } 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';
|
||||||
|
|
||||||
|
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(
|
async function debugUser(
|
||||||
client: MangoClient,
|
client: MangoClient,
|
||||||
group: Group,
|
group: Group,
|
||||||
|
@ -107,19 +117,12 @@ async function debugUser(
|
||||||
function getMaxSourceForTokenSwapWrapper(src, tgt) {
|
function getMaxSourceForTokenSwapWrapper(src, tgt) {
|
||||||
console.log(
|
console.log(
|
||||||
`getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` +
|
`getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` +
|
||||||
mangoAccount
|
mangoAccount.getMaxSourceUiForTokenSwap(
|
||||||
.getMaxSourceForTokenSwap(
|
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,
|
||||||
1,
|
),
|
||||||
)!
|
|
||||||
.div(
|
|
||||||
I80F48.fromNumber(
|
|
||||||
Math.pow(10, group.banksMapByName.get(src)![0].mintDecimals),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toNumber(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (const srcToken of Array.from(group.banksMapByName.keys())) {
|
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) {
|
function getMaxForSerum3Wrapper(serum3Market: Serum3Market) {
|
||||||
// if (serum3Market.name !== 'SOL/USDC') return;
|
// if (serum3Market.name !== 'SOL/USDC') return;
|
||||||
console.log(
|
console.log(
|
||||||
|
@ -156,12 +181,10 @@ async function debugUser(
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const options = AnchorProvider.defaultOptions();
|
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(
|
const admin = Keypair.fromSecretKey(
|
||||||
Buffer.from(
|
Buffer.from(JSON.parse(fs.readFileSync(PAYER_KEYPAIR!, 'utf-8'))),
|
||||||
JSON.parse(fs.readFileSync(process.env.MB_PAYER_KEYPAIR!, 'utf-8')),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
console.log(`Admin ${admin.publicKey.toBase58()}`);
|
console.log(`Admin ${admin.publicKey.toBase58()}`);
|
||||||
|
|
||||||
|
@ -169,16 +192,15 @@ async function main() {
|
||||||
const adminProvider = new AnchorProvider(connection, adminWallet, options);
|
const adminProvider = new AnchorProvider(connection, adminWallet, options);
|
||||||
const client = MangoClient.connect(
|
const client = MangoClient.connect(
|
||||||
adminProvider,
|
adminProvider,
|
||||||
'mainnet-beta',
|
CLUSTER,
|
||||||
MANGO_V4_ID['mainnet-beta'],
|
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 [
|
for (const keypair of [USER_KEYPAIR!]) {
|
||||||
process.env.MB_PAYER_KEYPAIR!,
|
|
||||||
process.env.MB_USER2_KEYPAIR!,
|
|
||||||
]) {
|
|
||||||
console.log();
|
console.log();
|
||||||
const user = Keypair.fromSecretKey(
|
const user = Keypair.fromSecretKey(
|
||||||
Buffer.from(JSON.parse(fs.readFileSync(keypair, 'utf-8'))),
|
Buffer.from(JSON.parse(fs.readFileSync(keypair, 'utf-8'))),
|
||||||
|
|
|
@ -357,7 +357,6 @@ async function main() {
|
||||||
0,
|
0,
|
||||||
'BTC-PERP',
|
'BTC-PERP',
|
||||||
0.1,
|
0.1,
|
||||||
1,
|
|
||||||
6,
|
6,
|
||||||
1,
|
1,
|
||||||
10,
|
10,
|
||||||
|
@ -372,15 +371,14 @@ async function main() {
|
||||||
0.05,
|
0.05,
|
||||||
0.05,
|
0.05,
|
||||||
100,
|
100,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
console.log('done');
|
console.log('done');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
const perpMarkets = await client.perpGetMarkets(
|
const perpMarkets = await client.perpGetMarkets(group);
|
||||||
group,
|
|
||||||
group.getFirstBankByMint(btcDevnetMint).tokenIndex,
|
|
||||||
);
|
|
||||||
console.log(`...created perp market ${perpMarkets[0].publicKey}`);
|
console.log(`...created perp market ${perpMarkets[0].publicKey}`);
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -480,6 +478,34 @@ async function main() {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw 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();
|
process.exit();
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { I80F48 } from '../accounts/I80F48';
|
|
||||||
import { HealthType } from '../accounts/mangoAccount';
|
import { HealthType } from '../accounts/mangoAccount';
|
||||||
import { BookSide, PerpOrderType, Side } from '../accounts/perp';
|
import { BookSide, PerpOrderType, Side } from '../accounts/perp';
|
||||||
import {
|
import {
|
||||||
|
@ -350,19 +349,12 @@ async function main() {
|
||||||
// console.log();
|
// console.log();
|
||||||
console.log(
|
console.log(
|
||||||
`getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` +
|
`getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` +
|
||||||
mangoAccount
|
mangoAccount.getMaxSourceUiForTokenSwap(
|
||||||
.getMaxSourceForTokenSwap(
|
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,
|
||||||
1,
|
)!,
|
||||||
)!
|
|
||||||
.div(
|
|
||||||
I80F48.fromNumber(
|
|
||||||
Math.pow(10, group.banksMapByName.get(src)![0].mintDecimals),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toNumber(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (const srcToken of Array.from(group.banksMapByName.keys())) {
|
for (const srcToken of Array.from(group.banksMapByName.keys())) {
|
||||||
|
@ -407,39 +399,45 @@ async function main() {
|
||||||
|
|
||||||
// perps
|
// perps
|
||||||
if (true) {
|
if (true) {
|
||||||
|
let sig;
|
||||||
const orders = await mangoAccount.loadPerpOpenOrdersForMarket(
|
const orders = await mangoAccount.loadPerpOpenOrdersForMarket(
|
||||||
client,
|
client,
|
||||||
group,
|
group,
|
||||||
'BTC-PERP',
|
'BTC-PERP',
|
||||||
);
|
);
|
||||||
for (const order of orders) {
|
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`);
|
console.log(`...cancelling all perp orders`);
|
||||||
let sig = await client.perpCancelAllOrders(
|
sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
|
||||||
group,
|
|
||||||
mangoAccount,
|
|
||||||
'BTC-PERP',
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||||
|
|
||||||
// scenario 1
|
// scenario 1
|
||||||
// not going to be hit orders, far from each other
|
// bid max perp
|
||||||
try {
|
try {
|
||||||
const clientId = Math.floor(Math.random() * 99999);
|
const clientId = Math.floor(Math.random() * 99999);
|
||||||
const price =
|
const price =
|
||||||
group.banksMapByName.get('BTC')![0].uiPrice! -
|
group.banksMapByName.get('BTC')![0].uiPrice! -
|
||||||
Math.floor(Math.random() * 100);
|
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(
|
const sig = await client.perpPlaceOrder(
|
||||||
group,
|
group,
|
||||||
mangoAccount,
|
mangoAccount,
|
||||||
'BTC-PERP',
|
'BTC-PERP',
|
||||||
Side.bid,
|
Side.bid,
|
||||||
price,
|
price,
|
||||||
0.01,
|
baseQty,
|
||||||
price * 0.01,
|
quoteQty,
|
||||||
clientId,
|
clientId,
|
||||||
PerpOrderType.limit,
|
PerpOrderType.limit,
|
||||||
0, //Date.now() + 200,
|
0, //Date.now() + 200,
|
||||||
|
@ -449,20 +447,59 @@ async function main() {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(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 {
|
try {
|
||||||
const clientId = Math.floor(Math.random() * 99999);
|
const clientId = Math.floor(Math.random() * 99999);
|
||||||
const price =
|
const price =
|
||||||
group.banksMapByName.get('BTC')![0].uiPrice! +
|
group.banksMapByName.get('BTC')![0].uiPrice! +
|
||||||
Math.floor(Math.random() * 100);
|
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(
|
const sig = await client.perpPlaceOrder(
|
||||||
group,
|
group,
|
||||||
mangoAccount,
|
mangoAccount,
|
||||||
'BTC-PERP',
|
'BTC-PERP',
|
||||||
Side.ask,
|
Side.ask,
|
||||||
price,
|
price,
|
||||||
0.01,
|
baseQty,
|
||||||
price * 0.01,
|
quoteQty,
|
||||||
clientId,
|
clientId,
|
||||||
PerpOrderType.limit,
|
PerpOrderType.limit,
|
||||||
0, //Date.now() + 200,
|
0, //Date.now() + 200,
|
||||||
|
@ -472,59 +509,89 @@ async function main() {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(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`);
|
console.log(`...cancelling all perp orders`);
|
||||||
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`);
|
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||||
|
|
||||||
// scenario 2
|
// // scenario 2
|
||||||
// make + take orders
|
// // make + take orders
|
||||||
try {
|
// try {
|
||||||
const clientId = Math.floor(Math.random() * 99999);
|
// const clientId = Math.floor(Math.random() * 99999);
|
||||||
const price = group.banksMapByName.get('BTC')![0].uiPrice!;
|
// const price = group.banksMapByName.get('BTC')![0].uiPrice!;
|
||||||
console.log(`...placing perp bid ${clientId} at ${price}`);
|
// console.log(`...placing perp bid ${clientId} at ${price}`);
|
||||||
const sig = await client.perpPlaceOrder(
|
// const sig = await client.perpPlaceOrder(
|
||||||
group,
|
// group,
|
||||||
mangoAccount,
|
// mangoAccount,
|
||||||
'BTC-PERP',
|
// 'BTC-PERP',
|
||||||
Side.bid,
|
// Side.bid,
|
||||||
price,
|
// price,
|
||||||
0.01,
|
// 0.01,
|
||||||
price * 0.01,
|
// price * 0.01,
|
||||||
clientId,
|
// clientId,
|
||||||
PerpOrderType.limit,
|
// PerpOrderType.limit,
|
||||||
0, //Date.now() + 200,
|
// 0, //Date.now() + 200,
|
||||||
1,
|
// 1,
|
||||||
);
|
// );
|
||||||
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
// console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.log(error);
|
// console.log(error);
|
||||||
}
|
// }
|
||||||
try {
|
// try {
|
||||||
const clientId = Math.floor(Math.random() * 99999);
|
// const clientId = Math.floor(Math.random() * 99999);
|
||||||
const price = group.banksMapByName.get('BTC')![0].uiPrice!;
|
// const price = group.banksMapByName.get('BTC')![0].uiPrice!;
|
||||||
console.log(`...placing perp ask ${clientId} at ${price}`);
|
// console.log(`...placing perp ask ${clientId} at ${price}`);
|
||||||
const sig = await client.perpPlaceOrder(
|
// const sig = await client.perpPlaceOrder(
|
||||||
group,
|
// group,
|
||||||
mangoAccount,
|
// mangoAccount,
|
||||||
'BTC-PERP',
|
// 'BTC-PERP',
|
||||||
Side.ask,
|
// Side.ask,
|
||||||
price,
|
// price,
|
||||||
0.01,
|
// 0.01,
|
||||||
price * 0.011,
|
// price * 0.011,
|
||||||
clientId,
|
// clientId,
|
||||||
PerpOrderType.limit,
|
// PerpOrderType.limit,
|
||||||
0, //Date.now() + 200,
|
// 0, //Date.now() + 200,
|
||||||
1,
|
// 1,
|
||||||
);
|
// );
|
||||||
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
// console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.log(error);
|
// console.log(error);
|
||||||
}
|
// }
|
||||||
// // should be able to cancel them : know bug
|
// // // should be able to cancel them : know bug
|
||||||
// console.log(`...cancelling all perp orders`);
|
// // console.log(`...cancelling all perp orders`);
|
||||||
// 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`);
|
// // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||||
|
|
||||||
const perpMarket = group.perpMarketsMap.get('BTC-PERP');
|
const perpMarket = group.perpMarketsMap.get('BTC-PERP');
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue