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:
microwavedcola1 2022-09-23 11:43:26 +02:00 committed by GitHub
parent 39bdf20813
commit bb6790e678
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 753 additions and 366 deletions

View File

@ -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)
}

View File

@ -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,

View File

@ -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;

View File

@ -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,
),
),
);
}

View File

@ -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');
});
});

View File

@ -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);
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
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 {
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;
}

View File

@ -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,46 +556,34 @@ export class MangoAccount {
`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) {
throw new Error(
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
);
}
return this.accountData.healthCache.simHealthRatioWithSerum3AskChanges(
group,
nativeBaseAmount,
serum3Market,
healthType,
);
return this.accountData.healthCache
.simHealthRatioWithSerum3BidChanges(
group,
toNative(
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,
uiBaseAmount: number,
externalMarketPk: PublicKey,
@ -683,16 +597,87 @@ export class MangoAccount {
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
return this.simHealthRatioWithSerum3AskChanges(
group,
toNative(
uiBaseAmount,
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
.mintDecimals,
),
serum3Market,
healthType,
).toNumber();
if (!this.accountData) {
throw new Error(
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
);
}
return this.accountData.healthCache
.simHealthRatioWithSerum3AskChanges(
group,
toNative(
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(

View File

@ -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,
) {}

View File

@ -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,

View File

@ -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(
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(),
mangoAccount.getMaxSourceUiForTokenSwap(
group,
group.banksMapByName.get(src)![0].mint,
group.banksMapByName.get(tgt)![0].mint,
1,
),
);
}
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'))),

View File

@ -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();

View File

@ -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(
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(),
mangoAccount.getMaxSourceUiForTokenSwap(
group,
group.banksMapByName.get(src)![0].mint,
group.banksMapByName.get(tgt)![0].mint,
1,
)!,
);
}
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);
// 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);
// // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
const perpMarket = group.perpMarketsMap.get('BTC-PERP');