ts client improvement (#254)

* Perps: Support trusted markets

* ts: health on client side

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: change perp lookup

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: reword error messages, refactor common uses of lookups

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: reformat

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: improve typing

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: fix some todos

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: fix some todos

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: type aliasing

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: remove '| undefined' where not required as return type

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: use trusted market flag for perp health

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
Co-authored-by: Christian Kamm <mail@ckamm.de>
This commit is contained in:
microwavedcola1 2022-09-29 15:51:09 +02:00 committed by GitHub
parent 7e180c7b3a
commit c22302a1da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1385 additions and 791 deletions

View File

@ -14,12 +14,21 @@
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"plugins": [
"@typescript-eslint"
],
"rules": {
"linebreak-style": ["error", "unix"],
"semi": ["error", "always"],
"linebreak-style": [
"error",
"unix"
],
"semi": [
"error",
"always"
],
"@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/ban-ts-comment": 0,
"@typescript-eslint/no-explicit-any": 0
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/explicit-function-return-type": "warn"
}
}
}

View File

@ -1,17 +1,19 @@
import { BN } from '@project-serum/anchor';
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { PublicKey } from '@solana/web3.js';
import { nativeI80F48ToUi } from '../utils';
import { As, nativeI80F48ToUi } from '../utils';
import { I80F48, I80F48Dto, ZERO_I80F48 } from './I80F48';
export const QUOTE_DECIMALS = 6;
export type TokenIndex = number & As<'token-index'>;
export type OracleConfig = {
confFilter: I80F48Dto;
};
export interface BankForHealth {
tokenIndex: number;
tokenIndex: TokenIndex;
maintAssetWeight: I80F48;
initAssetWeight: I80F48;
maintLiabWeight: I80F48;
@ -85,7 +87,7 @@ export class Bank implements BankForHealth {
mintDecimals: number;
bankNum: number;
},
) {
): Bank {
return new Bank(
publicKey,
obj.name,
@ -120,7 +122,7 @@ export class Bank implements BankForHealth {
obj.dust,
obj.flashLoanTokenAccountInitial,
obj.flashLoanApprovedAmount,
obj.tokenIndex,
obj.tokenIndex as TokenIndex,
obj.mintDecimals,
obj.bankNum,
);
@ -160,7 +162,7 @@ export class Bank implements BankForHealth {
dust: I80F48Dto,
flashLoanTokenAccountInitial: BN,
flashLoanApprovedAmount: BN,
public tokenIndex: number,
public tokenIndex: TokenIndex,
public mintDecimals: number,
public bankNum: number,
) {
@ -207,9 +209,9 @@ export class Bank implements BankForHealth {
'\n oracle - ' +
this.oracle.toBase58() +
'\n price - ' +
this.price?.toNumber() +
this._price?.toNumber() +
'\n uiPrice - ' +
this.uiPrice +
this._uiPrice +
'\n deposit index - ' +
this.depositIndex.toNumber() +
'\n borrow index - ' +
@ -268,7 +270,7 @@ export class Bank implements BankForHealth {
get price(): I80F48 {
if (!this._price) {
throw new Error(
`Undefined price for bank ${this.publicKey}, tokenIndex ${this.tokenIndex}`,
`Undefined price for bank ${this.publicKey} with tokenIndex ${this.tokenIndex}!`,
);
}
return this._price;
@ -277,7 +279,7 @@ export class Bank implements BankForHealth {
get uiPrice(): number {
if (!this._uiPrice) {
throw new Error(
`Undefined uiPrice for bank ${this.publicKey}, tokenIndex ${this.tokenIndex}`,
`Undefined uiPrice for bank ${this.publicKey} with tokenIndex ${this.tokenIndex}!`,
);
}
return this._uiPrice;
@ -388,11 +390,11 @@ export class MintInfo {
registrationTime: BN;
groupInsuranceFund: number;
},
) {
): MintInfo {
return new MintInfo(
publicKey,
obj.group,
obj.tokenIndex,
obj.tokenIndex as TokenIndex,
obj.mint,
obj.banks,
obj.vaults,
@ -405,7 +407,7 @@ export class MintInfo {
constructor(
public publicKey: PublicKey,
public group: PublicKey,
public tokenIndex: number,
public tokenIndex: TokenIndex,
public mint: PublicKey,
public banks: PublicKey[],
public vaults: PublicKey[],

View File

@ -6,7 +6,7 @@ import {
Market,
Orderbook,
} from '@project-serum/serum';
import { parsePriceData, PriceData } from '@pythnetwork/client';
import { parsePriceData } from '@pythnetwork/client';
import {
AccountInfo,
AddressLookupTableAccount,
@ -17,15 +17,15 @@ import { MangoClient } from '../client';
import { SERUM3_PROGRAM_ID } from '../constants';
import { Id } from '../ids';
import { toNativeDecimals, toUiDecimals } from '../utils';
import { Bank, MintInfo } from './bank';
import { Bank, MintInfo, TokenIndex } from './bank';
import { I80F48, ONE_I80F48 } from './I80F48';
import {
isPythOracle,
isSwitchboardOracle,
parseSwitchboardOracle,
} from './oracle';
import { BookSide, PerpMarket } from './perp';
import { Serum3Market } from './serum3';
import { BookSide, PerpMarket, PerpMarketIndex } from './perp';
import { MarketIndex, Serum3Market } from './serum3';
export class Group {
static from(
@ -57,12 +57,14 @@ export class Group {
new Map(), // banksMapByName
new Map(), // banksMapByMint
new Map(), // banksMapByTokenIndex
new Map(), // serum3MarketsMap
new Map(), // serum3MarketsMapByExternal
new Map(), // serum3MarketsMapByMarketIndex
new Map(), // serum3MarketExternalsMap
new Map(), // perpMarketsMap
new Map(), // perpMarketsMapByOracle
new Map(), // perpMarketsMapByMarketIndex
new Map(), // perpMarketsMapByName
new Map(), // mintInfosMapByTokenIndex
new Map(), // mintInfosMapByMint
new Map(), // oraclesMap
new Map(), // vaultAmountsMap
);
}
@ -81,18 +83,19 @@ export class Group {
public addressLookupTablesList: AddressLookupTableAccount[],
public banksMapByName: Map<string, Bank[]>,
public banksMapByMint: Map<string, Bank[]>,
public banksMapByTokenIndex: Map<number, Bank[]>,
public banksMapByTokenIndex: Map<TokenIndex, Bank[]>,
public serum3MarketsMapByExternal: Map<string, Serum3Market>,
public serum3MarketExternalsMap: Map<string, Market>,
// TODO rethink key
public perpMarketsMap: Map<string, PerpMarket>,
public mintInfosMapByTokenIndex: Map<number, MintInfo>,
public serum3MarketsMapByMarketIndex: Map<MarketIndex, Serum3Market>,
public serum3ExternalMarketsMap: Map<string, Market>,
public perpMarketsMapByOracle: Map<string, PerpMarket>,
public perpMarketsMapByMarketIndex: Map<PerpMarketIndex, PerpMarket>,
public perpMarketsMapByName: Map<string, PerpMarket>,
public mintInfosMapByTokenIndex: Map<TokenIndex, MintInfo>,
public mintInfosMapByMint: Map<string, MintInfo>,
private oraclesMap: Map<string, PriceData>, // UNUSED
public vaultAmountsMap: Map<string, number>,
) {}
public async reloadAll(client: MangoClient) {
public async reloadAll(client: MangoClient): Promise<void> {
let ids: Id | undefined = undefined;
if (client.idsSource === 'api') {
@ -109,12 +112,12 @@ export class Group {
this.reloadBanks(client, ids).then(() =>
Promise.all([
this.reloadBankOraclePrices(client),
this.reloadVaults(client, ids),
this.reloadVaults(client),
]),
),
this.reloadMintInfos(client, ids),
this.reloadSerum3Markets(client, ids).then(() =>
this.reloadSerum3ExternalMarkets(client, ids),
this.reloadSerum3ExternalMarkets(client),
),
this.reloadPerpMarkets(client, ids).then(() =>
this.reloadPerpMarketOraclePrices(client),
@ -123,7 +126,7 @@ export class Group {
// console.timeEnd('group.reload');
}
public async reloadAlts(client: MangoClient) {
public async reloadAlts(client: MangoClient): Promise<void> {
const alts = await Promise.all(
this.addressLookupTables
.filter((alt) => !alt.equals(PublicKey.default))
@ -133,13 +136,13 @@ export class Group {
);
this.addressLookupTablesList = alts.map((res, i) => {
if (!res || !res.value) {
throw new Error(`Error in getting ALT ${this.addressLookupTables[i]}`);
throw new Error(`Undefined ALT ${this.addressLookupTables[i]}!`);
}
return res.value;
});
}
public async reloadBanks(client: MangoClient, ids?: Id) {
public async reloadBanks(client: MangoClient, ids?: Id): Promise<void> {
let banks: Bank[];
if (ids && ids.getBanks().length) {
@ -169,7 +172,7 @@ export class Group {
}
}
public async reloadMintInfos(client: MangoClient, ids?: Id) {
public async reloadMintInfos(client: MangoClient, ids?: Id): Promise<void> {
let mintInfos: MintInfo[];
if (ids && ids.getMintInfos().length) {
mintInfos = (
@ -194,7 +197,10 @@ export class Group {
);
}
public async reloadSerum3Markets(client: MangoClient, ids?: Id) {
public async reloadSerum3Markets(
client: MangoClient,
ids?: Id,
): Promise<void> {
let serum3Markets: Serum3Market[];
if (ids && ids.getSerum3Markets().length) {
serum3Markets = (
@ -214,9 +220,15 @@ export class Group {
serum3Market,
]),
);
this.serum3MarketsMapByMarketIndex = new Map(
serum3Markets.map((serum3Market) => [
serum3Market.marketIndex,
serum3Market,
]),
);
}
public async reloadSerum3ExternalMarkets(client: MangoClient, ids?: Id) {
public async reloadSerum3ExternalMarkets(client: MangoClient): Promise<void> {
const externalMarkets = await Promise.all(
Array.from(this.serum3MarketsMapByExternal.values()).map((serum3Market) =>
Market.load(
@ -228,7 +240,7 @@ export class Group {
),
);
this.serum3MarketExternalsMap = new Map(
this.serum3ExternalMarketsMap = new Map(
Array.from(this.serum3MarketsMapByExternal.values()).map(
(serum3Market, index) => [
serum3Market.serumMarketExternal.toBase58(),
@ -238,7 +250,7 @@ export class Group {
);
}
public async reloadPerpMarkets(client: MangoClient, ids?: Id) {
public async reloadPerpMarkets(client: MangoClient, ids?: Id): Promise<void> {
let perpMarkets: PerpMarket[];
if (ids && ids.getPerpMarkets().length) {
perpMarkets = (
@ -252,9 +264,18 @@ export class Group {
perpMarkets = await client.perpGetMarkets(this);
}
this.perpMarketsMap = new Map(
this.perpMarketsMapByName = new Map(
perpMarkets.map((perpMarket) => [perpMarket.name, perpMarket]),
);
this.perpMarketsMapByOracle = new Map(
perpMarkets.map((perpMarket) => [
perpMarket.oracle.toBase58(),
perpMarket,
]),
);
this.perpMarketsMapByMarketIndex = new Map(
perpMarkets.map((perpMarket) => [perpMarket.perpMarketIndex, perpMarket]),
);
}
public async reloadBankOraclePrices(client: MangoClient): Promise<void> {
@ -293,7 +314,9 @@ export class Group {
public async reloadPerpMarketOraclePrices(
client: MangoClient,
): Promise<void> {
const perpMarkets: PerpMarket[] = Array.from(this.perpMarketsMap.values());
const perpMarkets: PerpMarket[] = Array.from(
this.perpMarketsMapByName.values(),
);
const oracles = perpMarkets.map((b) => b.oracle);
const ais =
await client.program.provider.connection.getMultipleAccountsInfo(oracles);
@ -302,15 +325,17 @@ export class Group {
ais.forEach(async (ai, i) => {
const perpMarket = perpMarkets[i];
if (!ai)
throw new Error('Undefined ai object in reloadPerpMarketOraclePrices!');
throw new Error(
`Undefined ai object in reloadPerpMarketOraclePrices for ${perpMarket.oracle}!`,
);
const { price, uiPrice } = await this.decodePriceFromOracleAi(
coder,
perpMarket.oracle,
ai,
perpMarket.baseDecimals,
);
perpMarket.price = price;
perpMarket.uiPrice = uiPrice;
perpMarket._price = price;
perpMarket._uiPrice = uiPrice;
});
}
@ -319,7 +344,7 @@ export class Group {
oracle: PublicKey,
ai: AccountInfo<Buffer>,
baseDecimals: number,
) {
): Promise<{ price: I80F48; uiPrice: number }> {
let price, uiPrice;
if (
!BorshAccountsCoder.accountDiscriminator('stubOracle').compare(
@ -337,13 +362,13 @@ export class Group {
price = this?.toNativePrice(uiPrice, baseDecimals);
} else {
throw new Error(
`Unknown oracle provider for oracle ${oracle}, with owner ${ai.owner}`,
`Unknown oracle provider (parsing not implemented) for oracle ${oracle}, with owner ${ai.owner}!`,
);
}
return { price, uiPrice };
}
public async reloadVaults(client: MangoClient, ids?: Id): Promise<void> {
public async reloadVaults(client: MangoClient): Promise<void> {
const vaultPks = Array.from(this.banksMapByMint.values())
.flat()
.map((bank) => bank.vault);
@ -354,7 +379,9 @@ export class Group {
this.vaultAmountsMap = new Map(
vaultAccounts.map((vaultAi, i) => {
if (!vaultAi) throw new Error('Missing vault account info');
if (!vaultAi) {
throw new Error(`Undefined vaultAi for ${vaultPks[i]}`!);
}
const vaultAmount = coder()
.accounts.decode('token', vaultAi.data)
.amount.toNumber();
@ -365,8 +392,7 @@ export class Group {
public getMintDecimals(mintPk: PublicKey): number {
const banks = this.banksMapByMint.get(mintPk.toString());
if (!banks)
throw new Error(`Unable to find mint decimals for ${mintPk.toString()}`);
if (!banks) throw new Error(`No bank found for mint ${mintPk}!`);
return banks[0].mintDecimals;
}
@ -376,14 +402,13 @@ export class Group {
public getFirstBankByMint(mintPk: PublicKey): Bank {
const banks = this.banksMapByMint.get(mintPk.toString());
if (!banks) throw new Error(`Unable to find bank for ${mintPk.toString()}`);
if (!banks) throw new Error(`No bank found for mint ${mintPk}!`);
return banks[0];
}
public getFirstBankByTokenIndex(tokenIndex: number): Bank {
public getFirstBankByTokenIndex(tokenIndex: TokenIndex): Bank {
const banks = this.banksMapByTokenIndex.get(tokenIndex);
if (!banks)
throw new Error(`Unable to find banks for tokenIndex ${tokenIndex}`);
if (!banks) throw new Error(`No bank found for tokenIndex ${tokenIndex}!`);
return banks[0];
}
@ -394,10 +419,7 @@ export class Group {
*/
public getTokenVaultBalanceByMint(mintPk: PublicKey): I80F48 {
const banks = this.banksMapByMint.get(mintPk.toBase58());
if (!banks)
throw new Error(
`Mint does not exist in getTokenVaultBalanceByMint ${mintPk.toString()}`,
);
if (!banks) throw new Error(`No bank found for mint ${mintPk}!`);
let totalAmount = 0;
for (const bank of banks) {
const amount = this.vaultAmountsMap.get(bank.vault.toBase58());
@ -408,83 +430,6 @@ export class Group {
return I80F48.fromNumber(totalAmount);
}
public findPerpMarket(marketIndex: number): PerpMarket | undefined {
return Array.from(this.perpMarketsMap.values()).find(
(perpMarket) => perpMarket.perpMarketIndex === marketIndex,
);
}
public findSerum3Market(marketIndex: number): Serum3Market | undefined {
return Array.from(this.serum3MarketsMapByExternal.values()).find(
(serum3Market) => serum3Market.marketIndex === marketIndex,
);
}
public findSerum3MarketByName(name: string): Serum3Market | undefined {
return Array.from(this.serum3MarketsMapByExternal.values()).find(
(serum3Market) => serum3Market.name === name,
);
}
public async loadSerum3BidsForMarket(
client: MangoClient,
externalMarketPk: PublicKey,
): Promise<Orderbook> {
const serum3Market = this.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
);
if (!serum3Market) {
throw new Error(
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
return await serum3Market.loadBids(client, this);
}
public async loadSerum3AsksForMarket(
client: MangoClient,
externalMarketPk: PublicKey,
): Promise<Orderbook> {
const serum3Market = this.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
);
if (!serum3Market) {
throw new Error(
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
return await serum3Market.loadAsks(client, this);
}
public getFeeRate(maker = true) {
// TODO: fetch msrm/srm vault balance
const feeTier = getFeeTier(0, 0);
const rates = getFeeRates(feeTier);
return maker ? rates.maker : rates.taker;
}
public async loadPerpBidsForMarket(
client: MangoClient,
marketName: string,
): Promise<BookSide> {
const perpMarket = this.perpMarketsMap.get(marketName);
if (!perpMarket) {
throw new Error(`Perp Market ${marketName} not found!`);
}
return await perpMarket.loadBids(client);
}
public async loadPerpAsksForMarket(
client: MangoClient,
marketName: string,
): Promise<BookSide> {
const perpMarket = this.perpMarketsMap.get(marketName);
if (!perpMarket) {
throw new Error(`Perp Market ${marketName} not found!`);
}
return await perpMarket.loadAsks(client);
}
/**
*
* @param mintPk
@ -497,7 +442,131 @@ export class Group {
return toUiDecimals(vaultBalance, mintDecimals);
}
public consoleLogBanks() {
public getSerum3MarketByMarketIndex(marketIndex: MarketIndex): Serum3Market {
const serum3Market = this.serum3MarketsMapByMarketIndex.get(marketIndex);
if (!serum3Market) {
throw new Error(`No serum3Market found for marketIndex ${marketIndex}!`);
}
return serum3Market;
}
public getSerum3MarketByName(name: string): Serum3Market {
const serum3Market = Array.from(
this.serum3MarketsMapByExternal.values(),
).find((serum3Market) => serum3Market.name === name);
if (!serum3Market) {
throw new Error(`No serum3Market found by name ${name}!`);
}
return serum3Market;
}
public getSerum3MarketByExternalMarket(
externalMarketPk: PublicKey,
): Serum3Market {
const serum3Market = Array.from(
this.serum3MarketsMapByExternal.values(),
).find((serum3Market) =>
serum3Market.serumMarketExternal.equals(externalMarketPk),
);
if (!serum3Market) {
throw new Error(
`No serum3Market found for external serum3 market ${externalMarketPk.toString()}!`,
);
}
return serum3Market;
}
public getSerum3ExternalMarket(externalMarketPk: PublicKey): Market {
const market = this.serum3ExternalMarketsMap.get(
externalMarketPk.toBase58(),
);
if (!market) {
throw new Error(
`No external market found for pk ${externalMarketPk.toString()}!`,
);
}
return market;
}
public async loadSerum3BidsForMarket(
client: MangoClient,
externalMarketPk: PublicKey,
): Promise<Orderbook> {
const serum3Market = this.getSerum3MarketByExternalMarket(externalMarketPk);
return await serum3Market.loadBids(client, this);
}
public async loadSerum3AsksForMarket(
client: MangoClient,
externalMarketPk: PublicKey,
): Promise<Orderbook> {
const serum3Market = this.getSerum3MarketByExternalMarket(externalMarketPk);
return await serum3Market.loadAsks(client, this);
}
public getSerum3FeeRates(maker = true): number {
// TODO: fetch msrm/srm vault balance
const feeTier = getFeeTier(0, 0);
const rates = getFeeRates(feeTier);
return maker ? rates.maker : rates.taker;
}
public findPerpMarket(marketIndex: PerpMarketIndex): PerpMarket {
const perpMarket = Array.from(this.perpMarketsMapByName.values()).find(
(perpMarket) => perpMarket.perpMarketIndex === marketIndex,
);
if (!perpMarket) {
throw new Error(
`No perpMarket found for perpMarketIndex ${marketIndex}!`,
);
}
return perpMarket;
}
public getPerpMarketByOracle(oracle: PublicKey): PerpMarket {
const perpMarket = this.perpMarketsMapByOracle.get(oracle.toBase58());
if (!perpMarket) {
throw new Error(`No PerpMarket found for oracle ${oracle}!`);
}
return perpMarket;
}
public getPerpMarketByMarketIndex(marketIndex: PerpMarketIndex): PerpMarket {
const perpMarket = this.perpMarketsMapByMarketIndex.get(marketIndex);
if (!perpMarket) {
throw new Error(`No PerpMarket found with marketIndex ${marketIndex}!`);
}
return perpMarket;
}
public getPerpMarketByName(perpMarketName: string): PerpMarket {
const perpMarket = Array.from(
this.perpMarketsMapByMarketIndex.values(),
).find((perpMarket) => perpMarket.name === perpMarketName);
if (!perpMarket) {
throw new Error(`No PerpMarket found by name ${perpMarketName}!`);
}
return perpMarket;
}
public async loadPerpBidsForMarket(
client: MangoClient,
perpMarketIndex: PerpMarketIndex,
): Promise<BookSide> {
const perpMarket = this.getPerpMarketByMarketIndex(perpMarketIndex);
return await perpMarket.loadBids(client);
}
public async loadPerpAsksForMarket(
client: MangoClient,
group: Group,
perpMarketIndex: PerpMarketIndex,
): Promise<BookSide> {
const perpMarket = this.getPerpMarketByMarketIndex(perpMarketIndex);
return await perpMarket.loadAsks(client);
}
public consoleLogBanks(): void {
for (const mintBanks of this.banksMapByMint.values()) {
for (const bank of mintBanks) {
console.log(bank.toString());

View File

@ -1,14 +1,371 @@
import { BN } from '@project-serum/anchor';
import { OpenOrders } from '@project-serum/serum';
import { expect } from 'chai';
import { toUiDecimalsForQuote } from '../utils';
import { BankForHealth } from './bank';
import { HealthCache, TokenInfo } from './healthCache';
import { BankForHealth, TokenIndex } from './bank';
import { HealthCache, PerpInfo, Serum3Info, TokenInfo } from './healthCache';
import { I80F48, ZERO_I80F48 } from './I80F48';
import { HealthType, PerpPosition } from './mangoAccount';
import { PerpMarket } from './perp';
import { MarketIndex } from './serum3';
function mockBankAndOracle(
tokenIndex: TokenIndex,
maintWeight: number,
initWeight: number,
price: number,
): BankForHealth {
return {
tokenIndex,
maintAssetWeight: I80F48.fromNumber(1 - maintWeight),
initAssetWeight: I80F48.fromNumber(1 - initWeight),
maintLiabWeight: I80F48.fromNumber(1 + maintWeight),
initLiabWeight: I80F48.fromNumber(1 + initWeight),
price: I80F48.fromNumber(price),
};
}
function mockPerpMarket(
perpMarketIndex: number,
maintWeight: number,
initWeight: number,
price: I80F48,
): PerpMarket {
return {
perpMarketIndex,
maintAssetWeight: I80F48.fromNumber(1 - maintWeight),
initAssetWeight: I80F48.fromNumber(1 - initWeight),
maintLiabWeight: I80F48.fromNumber(1 + maintWeight),
initLiabWeight: I80F48.fromNumber(1 + initWeight),
price,
quoteLotSize: new BN(100),
baseLotSize: new BN(10),
longFunding: ZERO_I80F48(),
shortFunding: ZERO_I80F48(),
} as unknown as PerpMarket;
}
describe('Health Cache', () => {
it('test_health0', () => {
const sourceBank: BankForHealth = mockBankAndOracle(
1 as TokenIndex,
0.1,
0.2,
1,
);
const targetBank: BankForHealth = mockBankAndOracle(
4 as TokenIndex,
0.3,
0.5,
5,
);
const ti1 = TokenInfo.fromBank(sourceBank, I80F48.fromNumber(100));
const ti2 = TokenInfo.fromBank(targetBank, I80F48.fromNumber(-10));
const si1 = Serum3Info.fromOoModifyingTokenInfos(
1,
ti2,
0,
ti1,
2 as MarketIndex,
{
quoteTokenTotal: new BN(21),
baseTokenTotal: new BN(18),
quoteTokenFree: new BN(1),
baseTokenFree: new BN(3),
referrerRebatesAccrued: new BN(2),
} as any as OpenOrders,
);
const pM = mockPerpMarket(9, 0.1, 0.2, targetBank.price);
const pp = new PerpPosition(
pM.perpMarketIndex,
3,
I80F48.fromNumber(-310),
7,
11,
1,
2,
I80F48.fromNumber(0),
I80F48.fromNumber(0),
);
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
const hc = new HealthCache([ti1, ti2], [si1], [pi1]);
// for bank1/oracle1, including open orders (scenario: bids execute)
const health1 = (100.0 + 1.0 + 2.0 + (20.0 + 15.0 * 5.0)) * 0.8;
// for bank2/oracle2
const health2 = (-10.0 + 3.0) * 5.0 * 1.5;
// for perp (scenario: bids execute)
const health3 =
(3.0 + 7.0 + 1.0) * 10.0 * 5.0 * 0.8 +
(-310.0 + 2.0 * 100.0 - 7.0 * 10.0 * 5.0);
const health = hc.health(HealthType.init).toNumber();
console.log(
`health ${health
.toFixed(3)
.padStart(
10,
)}, case "test that includes all the side values (like referrer_rebates_accrued)"`,
);
expect(health - (health1 + health2 + health3)).lessThan(0.0000001);
});
it('test_health1', () => {
function testFixture(fixture: {
name: string;
token1: number;
token2: number;
token3: number;
oo12: [number, number];
oo13: [number, number];
perp1: [number, number, number, number];
expectedHealth: number;
}): void {
const bank1: BankForHealth = mockBankAndOracle(
1 as TokenIndex,
0.1,
0.2,
1,
);
const bank2: BankForHealth = mockBankAndOracle(
4 as TokenIndex,
0.3,
0.5,
5,
);
const bank3: BankForHealth = mockBankAndOracle(
5 as TokenIndex,
0.3,
0.5,
10,
);
const ti1 = TokenInfo.fromBank(bank1, I80F48.fromNumber(fixture.token1));
const ti2 = TokenInfo.fromBank(bank2, I80F48.fromNumber(fixture.token2));
const ti3 = TokenInfo.fromBank(bank3, I80F48.fromNumber(fixture.token3));
const si1 = Serum3Info.fromOoModifyingTokenInfos(
1,
ti2,
0,
ti1,
2 as MarketIndex,
{
quoteTokenTotal: new BN(fixture.oo12[0]),
baseTokenTotal: new BN(fixture.oo12[1]),
quoteTokenFree: new BN(0),
baseTokenFree: new BN(0),
referrerRebatesAccrued: new BN(0),
} as any as OpenOrders,
);
const si2 = Serum3Info.fromOoModifyingTokenInfos(
2,
ti3,
0,
ti1,
2 as MarketIndex,
{
quoteTokenTotal: new BN(fixture.oo13[0]),
baseTokenTotal: new BN(fixture.oo13[1]),
quoteTokenFree: new BN(0),
baseTokenFree: new BN(0),
referrerRebatesAccrued: new BN(0),
} as any as OpenOrders,
);
const pM = mockPerpMarket(9, 0.1, 0.2, bank2.price);
const pp = new PerpPosition(
pM.perpMarketIndex,
fixture.perp1[0],
I80F48.fromNumber(fixture.perp1[1]),
fixture.perp1[2],
fixture.perp1[3],
0,
0,
I80F48.fromNumber(0),
I80F48.fromNumber(0),
);
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
const hc = new HealthCache([ti1, ti2, ti3], [si1, si2], [pi1]);
const health = hc.health(HealthType.init).toNumber();
console.log(
`health ${health.toFixed(3).padStart(10)}, case "${fixture.name}"`,
);
expect(health - fixture.expectedHealth).lessThan(0.0000001);
}
const basePrice = 5;
const baseLotsToQuote = 10.0 * basePrice;
testFixture({
name: '0',
token1: 100,
token2: -10,
token3: 0,
oo12: [20, 15],
oo13: [0, 0],
perp1: [3, -131, 7, 11],
expectedHealth:
// for token1, including open orders (scenario: bids execute)
(100.0 + (20.0 + 15.0 * basePrice)) * 0.8 -
// for token2
10.0 * basePrice * 1.5 +
// for perp (scenario: bids execute)
(3.0 + 7.0) * baseLotsToQuote * 0.8 +
(-131.0 - 7.0 * baseLotsToQuote),
});
testFixture({
name: '1',
token1: -100,
token2: 10,
token3: 0,
oo12: [20, 15],
oo13: [0, 0],
perp1: [-10, -131, 7, 11],
expectedHealth:
// for token1
-100.0 * 1.2 +
// for token2, including open orders (scenario: asks execute)
(10.0 * basePrice + (20.0 + 15.0 * basePrice)) * 0.5 +
// for perp (scenario: asks execute)
(-10.0 - 11.0) * baseLotsToQuote * 1.2 +
(-131.0 + 11.0 * baseLotsToQuote),
});
testFixture({
name: '2',
token1: 0,
token2: 0,
token3: 0,
oo12: [0, 0],
oo13: [0, 0],
perp1: [-10, 100, 0, 0],
expectedHealth: 0,
});
testFixture({
name: '3',
token1: 0,
token2: 0,
token3: 0,
oo12: [0, 0],
oo13: [0, 0],
perp1: [1, -100, 0, 0],
expectedHealth: -100.0 + 0.8 * 1.0 * baseLotsToQuote,
});
testFixture({
name: '4',
token1: 0,
token2: 0,
token3: 0,
oo12: [0, 0],
oo13: [0, 0],
perp1: [10, 100, 0, 0],
expectedHealth: 0,
});
testFixture({
name: '5',
token1: 0,
token2: 0,
token3: 0,
oo12: [0, 0],
oo13: [0, 0],
perp1: [30, -100, 0, 0],
expectedHealth: 0,
});
testFixture({
name: '6, reserved oo funds',
token1: -100,
token2: -10,
token3: -10,
oo12: [1, 1],
oo13: [1, 1],
perp1: [30, -100, 0, 0],
expectedHealth:
// tokens
-100.0 * 1.2 -
10.0 * 5.0 * 1.5 -
10.0 * 10.0 * 1.5 +
// oo_1_2 (-> token1)
(1.0 + 5.0) * 1.2 +
// oo_1_3 (-> token1)
(1.0 + 10.0) * 1.2,
});
testFixture({
name: '7, reserved oo funds cross the zero balance level',
token1: -14,
token2: -10,
token3: -10,
oo12: [1, 1],
oo13: [1, 1],
perp1: [0, 0, 0, 0],
expectedHealth:
-14.0 * 1.2 -
10.0 * 5.0 * 1.5 -
10.0 * 10.0 * 1.5 +
// oo_1_2 (-> token1)
3.0 * 1.2 +
3.0 * 0.8 +
// oo_1_3 (-> token1)
8.0 * 1.2 +
3.0 * 0.8,
});
testFixture({
name: '8, reserved oo funds in a non-quote currency',
token1: -100,
token2: -100,
token3: -1,
oo12: [0, 0],
oo13: [10, 1],
perp1: [0, 0, 0, 0],
expectedHealth:
// tokens
-100.0 * 1.2 -
100.0 * 5.0 * 1.5 -
10.0 * 1.5 +
// oo_1_3 (-> token3)
10.0 * 1.5 +
10.0 * 0.5,
});
testFixture({
name: '9, like 8 but oo_1_2 flips the oo_1_3 target',
token1: -100,
token2: -100,
token3: -1,
oo12: [100, 0],
oo13: [10, 1],
perp1: [0, 0, 0, 0],
expectedHealth:
// tokens
-100.0 * 1.2 -
100.0 * 5.0 * 1.5 -
10.0 * 1.5 +
// oo_1_2 (-> token1)
80.0 * 1.2 +
20.0 * 0.8 +
// oo_1_3 (-> token1)
20.0 * 0.8,
});
});
it('max swap tokens for min ratio', () => {
// USDC like
const sourceBank: BankForHealth = {
tokenIndex: 0,
tokenIndex: 0 as TokenIndex,
maintAssetWeight: I80F48.fromNumber(1),
initAssetWeight: I80F48.fromNumber(1),
maintLiabWeight: I80F48.fromNumber(1),
@ -17,7 +374,7 @@ describe('Health Cache', () => {
};
// BTC like
const targetBank: BankForHealth = {
tokenIndex: 1,
tokenIndex: 1 as TokenIndex,
maintAssetWeight: I80F48.fromNumber(0.9),
initAssetWeight: I80F48.fromNumber(0.8),
maintLiabWeight: I80F48.fromNumber(1.1),
@ -28,7 +385,7 @@ describe('Health Cache', () => {
const hc = new HealthCache(
[
new TokenInfo(
0,
0 as TokenIndex,
sourceBank.maintAssetWeight,
sourceBank.initAssetWeight,
sourceBank.maintLiabWeight,
@ -39,7 +396,7 @@ describe('Health Cache', () => {
),
new TokenInfo(
1,
1 as TokenIndex,
targetBank.maintAssetWeight,
targetBank.initAssetWeight,
targetBank.maintLiabWeight,

View File

@ -1,18 +1,20 @@
import { BN } from '@project-serum/anchor';
import { OpenOrders } from '@project-serum/serum';
import { PublicKey } from '@solana/web3.js';
import _ from 'lodash';
import { Bank, BankForHealth } from './bank';
import { Bank, BankForHealth, TokenIndex } from './bank';
import { Group } from './group';
import {
HUNDRED_I80F48,
I80F48,
I80F48Dto,
MAX_I80F48,
ONE_I80F48,
ZERO_I80F48,
} from './I80F48';
import { HealthType } from './mangoAccount';
import { HealthType, MangoAccount, PerpPosition } from './mangoAccount';
import { PerpMarket, PerpOrderSide } from './perp';
import { Serum3Market, Serum3Side } from './serum3';
import { MarketIndex, Serum3Market, Serum3Side } from './serum3';
// ░░░░
//
@ -45,7 +47,63 @@ export class HealthCache {
public perpInfos: PerpInfo[],
) {}
static fromDto(dto) {
static fromMangoAccount(
group: Group,
mangoAccount: MangoAccount,
): HealthCache {
// token contribution from token accounts
const tokenInfos = mangoAccount.tokensActive().map((tokenPosition) => {
const bank = group.getFirstBankByTokenIndex(tokenPosition.tokenIndex);
return TokenInfo.fromBank(bank, tokenPosition.balance(bank));
});
// Fill the TokenInfo balance with free funds in serum3 oo accounts, and fill
// the serum3MaxReserved with their reserved funds. Also build Serum3Infos.
const serum3Infos = mangoAccount.serum3Active().map((serum3) => {
const oo = mangoAccount.getSerum3OoAccount(serum3.marketIndex);
// find the TokenInfos for the market's base and quote tokens
const baseIndex = tokenInfos.findIndex(
(tokenInfo) => tokenInfo.tokenIndex === serum3.baseTokenIndex,
);
const baseInfo = tokenInfos[baseIndex];
if (!baseInfo) {
throw new Error(
`BaseInfo not found for market with marketIndex ${serum3.marketIndex}!`,
);
}
const quoteIndex = tokenInfos.findIndex(
(tokenInfo) => tokenInfo.tokenIndex === serum3.quoteTokenIndex,
);
const quoteInfo = tokenInfos[quoteIndex];
if (!quoteInfo) {
throw new Error(
`QuoteInfo not found for market with marketIndex ${serum3.marketIndex}!`,
);
}
return Serum3Info.fromOoModifyingTokenInfos(
baseIndex,
baseInfo,
quoteIndex,
quoteInfo,
serum3.marketIndex,
oo,
);
});
// health contribution from perp accounts
const perpInfos = mangoAccount.perpActive().map((perpPosition) => {
const perpMarket = group.getPerpMarketByMarketIndex(
perpPosition.marketIndex,
);
return PerpInfo.fromPerpPosition(perpMarket, perpPosition);
});
return new HealthCache(tokenInfos, serum3Infos, perpInfos);
}
static fromDto(dto): HealthCache {
return new HealthCache(
dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)),
dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)),
@ -57,6 +115,7 @@ export class HealthCache {
const health = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType);
// console.log(` - ti ${contrib}`);
health.iadd(contrib);
}
for (const serum3Info of this.serum3Infos) {
@ -64,10 +123,12 @@ export class HealthCache {
healthType,
this.tokenInfos,
);
// console.log(` - si ${contrib}`);
health.iadd(contrib);
}
for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType);
// console.log(` - pi ${contrib}`);
health.iadd(contrib);
}
return health;
@ -164,34 +225,32 @@ export class HealthCache {
}
}
findTokenInfoIndex(tokenIndex: number): number {
findTokenInfoIndex(tokenIndex: TokenIndex): number {
return this.tokenInfos.findIndex(
(tokenInfo) => tokenInfo.tokenIndex == tokenIndex,
(tokenInfo) => tokenInfo.tokenIndex === tokenIndex,
);
}
getOrCreateTokenInfoIndex(bank: BankForHealth): number {
const index = this.findTokenInfoIndex(bank.tokenIndex);
if (index == -1) {
this.tokenInfos.push(TokenInfo.emptyFromBank(bank));
this.tokenInfos.push(TokenInfo.fromBank(bank));
}
return this.findTokenInfoIndex(bank.tokenIndex);
}
findSerum3InfoIndex(marketIndex: number): number {
findSerum3InfoIndex(marketIndex: MarketIndex): number {
return this.serum3Infos.findIndex(
(serum3Info) => serum3Info.marketIndex === marketIndex,
);
}
getOrCreateSerum3InfoIndex(group: Group, serum3Market: Serum3Market): number {
getOrCreateSerum3InfoIndex(
baseBank: BankForHealth,
quoteBank: BankForHealth,
serum3Market: Serum3Market,
): number {
const index = this.findSerum3InfoIndex(serum3Market.marketIndex);
const baseBank = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
);
const quoteBank = group.getFirstBankByTokenIndex(
serum3Market.quoteTokenIndex,
);
const baseEntryIndex = this.getOrCreateTokenInfoIndex(baseBank);
const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank);
if (index == -1) {
@ -208,20 +267,14 @@ export class HealthCache {
adjustSerum3Reserved(
// todo change indices to types from numbers
group: Group,
baseBank: BankForHealth,
quoteBank: BankForHealth,
serum3Market: Serum3Market,
reservedBaseChange: I80F48,
freeBaseChange: I80F48,
reservedQuoteChange: I80F48,
freeQuoteChange: I80F48,
) {
const baseBank = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
);
const quoteBank = group.getFirstBankByTokenIndex(
serum3Market.quoteTokenIndex,
);
): void {
const baseEntryIndex = this.getOrCreateTokenInfoIndex(baseBank);
const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank);
@ -238,7 +291,11 @@ export class HealthCache {
quoteEntry.balance.iadd(freeQuoteChange.mul(quoteEntry.oraclePrice));
// Apply it to the serum3 info
const index = this.getOrCreateSerum3InfoIndex(group, serum3Market);
const index = this.getOrCreateSerum3InfoIndex(
baseBank,
quoteBank,
serum3Market,
);
const serum3Info = this.serum3Infos[index];
serum3Info.reserved = serum3Info.reserved.add(reservedAmount);
}
@ -257,7 +314,7 @@ export class HealthCache {
return this.findPerpInfoIndex(perpMarket.perpMarketIndex);
}
public static logHealthCache(debug: string, healthCache: HealthCache) {
public static logHealthCache(debug: string, healthCache: HealthCache): void {
if (debug) console.log(debug);
for (const token of healthCache.tokenInfos) {
console.log(` ${token.toString()}`);
@ -293,10 +350,6 @@ export class HealthCache {
for (const change of nativeTokenChanges) {
const bank: Bank = group.getFirstBankByMint(change.mintPk);
const changeIndex = adjustedCache.getOrCreateTokenInfoIndex(bank);
if (!bank.price)
throw new Error(
`Oracle price not loaded for ${change.mintPk.toString()}`,
);
adjustedCache.tokenInfos[changeIndex].balance.iadd(
change.nativeTokenAmount.mul(bank.price),
);
@ -306,18 +359,13 @@ export class HealthCache {
}
simHealthRatioWithSerum3BidChanges(
group: Group,
baseBank: BankForHealth,
quoteBank: BankForHealth,
bidNativeQuoteAmount: I80F48,
serum3Market: Serum3Market,
healthType: HealthType = HealthType.init,
): I80F48 {
const adjustedCache: HealthCache = _.cloneDeep(this);
const quoteBank = group.getFirstBankByTokenIndex(
serum3Market.quoteTokenIndex,
);
if (!quoteBank) {
throw new Error(`No bank for index ${serum3Market.quoteTokenIndex}`);
}
const quoteIndex = adjustedCache.getOrCreateTokenInfoIndex(quoteBank);
const quote = adjustedCache.tokenInfos[quoteIndex];
@ -331,7 +379,8 @@ export class HealthCache {
// Increase reserved in Serum3Info for quote
adjustedCache.adjustSerum3Reserved(
group,
baseBank,
quoteBank,
serum3Market,
ZERO_I80F48(),
ZERO_I80F48(),
@ -342,18 +391,13 @@ export class HealthCache {
}
simHealthRatioWithSerum3AskChanges(
group: Group,
baseBank: BankForHealth,
quoteBank: BankForHealth,
askNativeBaseAmount: I80F48,
serum3Market: Serum3Market,
healthType: HealthType = HealthType.init,
): I80F48 {
const adjustedCache: HealthCache = _.cloneDeep(this);
const baseBank = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
);
if (!baseBank) {
throw new Error(`No bank for index ${serum3Market.quoteTokenIndex}`);
}
const baseIndex = adjustedCache.getOrCreateTokenInfoIndex(baseBank);
const base = adjustedCache.tokenInfos[baseIndex];
@ -367,7 +411,8 @@ export class HealthCache {
// Increase reserved in Serum3Info for base
adjustedCache.adjustSerum3Reserved(
group,
baseBank,
quoteBank,
serum3Market,
askNativeBaseAmount,
ZERO_I80F48(),
@ -384,7 +429,7 @@ export class HealthCache {
rightRatio: I80F48,
targetRatio: I80F48,
healthRatioAfterActionFn: (I80F48) => I80F48,
) {
): I80F48 {
const maxIterations = 40;
// TODO: make relative to health ratio decimals? Might be over engineering
const targetError = I80F48.fromNumber(0.001);
@ -396,11 +441,12 @@ export class HealthCache {
rightRatio.sub(targetRatio).isNeg())
) {
throw new Error(
`internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}`,
`Internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}, likely reason is the zeroAmount not been tight enough!`,
);
}
let newAmount;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const key of Array(maxIterations).fill(0).keys()) {
newAmount = left.add(right).mul(I80F48.fromNumber(0.5));
const newAmountRatio = healthRatioAfterActionFn(newAmount);
@ -427,15 +473,6 @@ export class HealthCache {
minRatio: I80F48,
priceFactor: I80F48,
): I80F48 {
if (
!sourceBank.price ||
sourceBank.price.lte(ZERO_I80F48()) ||
!targetBank.price ||
targetBank.price.lte(ZERO_I80F48())
) {
return ZERO_I80F48();
}
if (
sourceBank.initLiabWeight
.sub(targetBank.initAssetWeight)
@ -454,6 +491,7 @@ export class HealthCache {
// - be careful about finding the minRatio point: the function isn't convex
const initialRatio = this.healthRatio(HealthType.init);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const initialHealth = this.health(HealthType.init);
if (initialRatio.lte(ZERO_I80F48())) {
return ZERO_I80F48();
@ -481,7 +519,7 @@ export class HealthCache {
// negative.
// The maximum will be at one of these points (ignoring serum3 effects).
function cacheAfterSwap(amount: I80F48) {
function cacheAfterSwap(amount: I80F48): HealthCache {
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
// HealthCache.logHealthCache('beforeSwap', adjustedCache);
adjustedCache.tokenInfos[sourceIndex].balance.isub(amount);
@ -578,24 +616,12 @@ export class HealthCache {
}
getMaxSerum3OrderForHealthRatio(
group: Group,
baseBank: BankForHealth,
quoteBank: BankForHealth,
serum3Market: Serum3Market,
side: Serum3Side,
minRatio: I80F48,
) {
const baseBank = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
);
if (!baseBank) {
throw new Error(`No bank for index ${serum3Market.baseTokenIndex}`);
}
const quoteBank = group.getFirstBankByTokenIndex(
serum3Market.quoteTokenIndex,
);
if (!quoteBank) {
throw new Error(`No bank for index ${serum3Market.quoteTokenIndex}`);
}
): I80F48 {
const healthCacheClone: HealthCache = _.cloneDeep(this);
const baseIndex = healthCacheClone.getOrCreateTokenInfoIndex(baseBank);
@ -652,10 +678,11 @@ export class HealthCache {
}
const cache = cacheAfterPlacingOrder(zeroAmount);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const zeroAmountHealth = cache.health(HealthType.init);
const zeroAmountRatio = cache.healthRatio(HealthType.init);
function cacheAfterPlacingOrder(amount: I80F48) {
function cacheAfterPlacingOrder(amount: I80F48): HealthCache {
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
side === Serum3Side.ask
@ -663,7 +690,8 @@ export class HealthCache {
: adjustedCache.tokenInfos[quoteIndex].balance.isub(amount);
adjustedCache.adjustSerum3Reserved(
group,
baseBank,
quoteBank,
serum3Market,
side === Serum3Side.ask ? amount.div(base.oraclePrice) : ZERO_I80F48(),
ZERO_I80F48(),
@ -687,18 +715,7 @@ export class HealthCache {
healthRatioAfterPlacingOrder,
);
// If its a bid then the reserved fund and potential loan is in quote,
// If its a ask then the reserved fund and potential loan is in base,
// also keep some buffer for fees, use taker fees for worst case simulation.
return side === Serum3Side.bid
? amount
.div(quote.oraclePrice)
.div(ONE_I80F48().add(baseBank.loanOriginationFeeRate))
.div(ONE_I80F48().add(I80F48.fromNumber(group.getFeeRate(false))))
: amount
.div(base.oraclePrice)
.div(ONE_I80F48().add(quoteBank.loanOriginationFeeRate))
.div(ONE_I80F48().add(I80F48.fromNumber(group.getFeeRate(false))));
return amount;
}
getMaxPerpForHealthRatio(
@ -813,7 +830,7 @@ export class HealthCache {
export class TokenInfo {
constructor(
public tokenIndex: number,
public tokenIndex: TokenIndex,
public maintAssetWeight: I80F48,
public initAssetWeight: I80F48,
public maintLiabWeight: I80F48,
@ -828,7 +845,7 @@ export class TokenInfo {
static fromDto(dto: TokenInfoDto): TokenInfo {
return new TokenInfo(
dto.tokenIndex,
dto.tokenIndex as TokenIndex,
I80F48.from(dto.maintAssetWeight),
I80F48.from(dto.initAssetWeight),
I80F48.from(dto.maintLiabWeight),
@ -839,11 +856,11 @@ export class TokenInfo {
);
}
static emptyFromBank(bank: BankForHealth): TokenInfo {
if (!bank.price)
throw new Error(
`Failed to create TokenInfo. Bank price unavailable for bank with tokenIndex ${bank.tokenIndex}`,
);
static fromBank(
bank: BankForHealth,
nativeBalance?: I80F48,
serum3MaxReserved?: I80F48,
): TokenInfo {
return new TokenInfo(
bank.tokenIndex,
bank.maintAssetWeight,
@ -851,8 +868,8 @@ export class TokenInfo {
bank.maintLiabWeight,
bank.initLiabWeight,
bank.price,
ZERO_I80F48(),
ZERO_I80F48(),
nativeBalance ? nativeBalance.mul(bank.price) : ZERO_I80F48(),
serum3MaxReserved ? serum3MaxReserved : ZERO_I80F48(),
);
}
@ -876,7 +893,7 @@ export class TokenInfo {
).mul(this.balance);
}
toString() {
toString(): string {
return ` tokenIndex: ${this.tokenIndex}, balance: ${
this.balance
}, serum3MaxReserved: ${
@ -890,15 +907,15 @@ export class Serum3Info {
public reserved: I80F48,
public baseIndex: number,
public quoteIndex: number,
public marketIndex: number,
public marketIndex: MarketIndex,
) {}
static fromDto(dto: Serum3InfoDto) {
static fromDto(dto: Serum3InfoDto): Serum3Info {
return new Serum3Info(
I80F48.from(dto.reserved),
dto.baseIndex,
dto.quoteIndex,
dto.marketIndex,
dto.marketIndex as MarketIndex,
);
}
@ -906,7 +923,7 @@ export class Serum3Info {
serum3Market: Serum3Market,
baseEntryIndex: number,
quoteEntryIndex: number,
) {
): Serum3Info {
return new Serum3Info(
ZERO_I80F48(),
baseEntryIndex,
@ -915,10 +932,47 @@ export class Serum3Info {
);
}
static fromOoModifyingTokenInfos(
baseIndex: number,
baseInfo: TokenInfo,
quoteIndex: number,
quoteInfo: TokenInfo,
marketIndex: MarketIndex,
oo: OpenOrders,
): Serum3Info {
// add the amounts that are freely settleable
const baseFree = I80F48.fromString(oo.baseTokenFree.toString());
// NOTE: referrerRebatesAccrued is not declared on oo class, but the layout
// is aware of it
const quoteFree = I80F48.fromString(
oo.quoteTokenFree.add((oo as any).referrerRebatesAccrued).toString(),
);
baseInfo.balance.iadd(baseFree.mul(baseInfo.oraclePrice));
quoteInfo.balance.iadd(quoteFree.mul(quoteInfo.oraclePrice));
// add the reserved amount to both sides, to have the worst-case covered
const reservedBase = I80F48.fromString(
oo.baseTokenTotal.sub(oo.baseTokenFree).toString(),
);
const reservedQuote = I80F48.fromString(
oo.quoteTokenTotal.sub(oo.quoteTokenFree).toString(),
);
const reservedBalance = reservedBase
.mul(baseInfo.oraclePrice)
.add(reservedQuote.mul(quoteInfo.oraclePrice));
baseInfo.serum3MaxReserved.iadd(reservedBalance);
quoteInfo.serum3MaxReserved.iadd(reservedBalance);
return new Serum3Info(reservedBalance, baseIndex, quoteIndex, marketIndex);
}
healthContribution(healthType: HealthType, tokenInfos: TokenInfo[]): I80F48 {
const baseInfo = tokenInfos[this.baseIndex];
const quoteInfo = tokenInfos[this.quoteIndex];
const reserved = this.reserved;
// console.log(` - reserved ${reserved}`);
// console.log(` - this.baseIndex ${this.baseIndex}`);
// console.log(` - this.quoteIndex ${this.quoteIndex}`);
if (reserved.isZero()) {
return ZERO_I80F48();
@ -926,7 +980,7 @@ export class Serum3Info {
// How much the health would increase if the reserved balance were applied to the passed
// token info?
const computeHealthEffect = function (tokenInfo: TokenInfo) {
const computeHealthEffect = function (tokenInfo: TokenInfo): I80F48 {
// This balance includes all possible reserved funds from markets that relate to the
// token, including this market itself: `reserved` is already included in `max_balance`.
const maxBalance = tokenInfo.balance.add(tokenInfo.serum3MaxReserved);
@ -946,15 +1000,25 @@ export class Serum3Info {
}
const assetWeight = tokenInfo.assetWeight(healthType);
const liabWeight = tokenInfo.liabWeight(healthType);
// console.log(` - tokenInfo.index ${tokenInfo.tokenIndex}`);
// console.log(` - tokenInfo.balance ${tokenInfo.balance}`);
// console.log(
// ` - tokenInfo.serum3MaxReserved ${tokenInfo.serum3MaxReserved}`,
// );
// console.log(` - assetPart ${assetPart}`);
// console.log(` - liabPart ${liabPart}`);
return assetWeight.mul(assetPart).add(liabWeight.mul(liabPart));
};
const reservedAsBase = computeHealthEffect(baseInfo);
const reservedAsQuote = computeHealthEffect(quoteInfo);
// console.log(` - reservedAsBase ${reservedAsBase}`);
// console.log(` - reservedAsQuote ${reservedAsQuote}`);
return reservedAsBase.min(reservedAsQuote);
}
toString(tokenInfos: TokenInfo[]) {
toString(tokenInfos: TokenInfo[]): string {
return ` marketIndex: ${this.marketIndex}, baseIndex: ${
this.baseIndex
}, quoteIndex: ${this.quoteIndex}, reserved: ${
@ -970,15 +1034,14 @@ export class PerpInfo {
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,
public trustedMarket: boolean,
) {}
static fromDto(dto: PerpInfoDto) {
static fromDto(dto: PerpInfoDto): PerpInfo {
return new PerpInfo(
dto.perpMarketIndex,
I80F48.from(dto.maintAssetWeight),
@ -989,6 +1052,114 @@ export class PerpInfo {
I80F48.from(dto.quote),
I80F48.from(dto.oraclePrice),
dto.hasOpenOrders,
dto.trustedMarket,
);
}
static fromPerpPosition(
perpMarket: PerpMarket,
perpPosition: PerpPosition,
): PerpInfo {
const baseLotSize = I80F48.fromString(perpMarket.baseLotSize.toString());
const baseLots = I80F48.fromNumber(
perpPosition.basePositionLots + perpPosition.takerBaseLots,
);
const unsettledFunding = perpPosition.unsettledFunding(perpMarket);
const takerQuote = I80F48.fromString(
new BN(perpPosition.takerQuoteLots)
.mul(perpMarket.quoteLotSize)
.toString(),
);
const quoteCurrent = I80F48.fromString(
perpPosition.quotePositionNative.toString(),
)
.sub(unsettledFunding)
.add(takerQuote);
// Two scenarios:
// 1. The price goes low and all bids execute, converting to base.
// That means the perp position is increased by `bids` and the quote position
// is decreased by `bids * baseLotSize * price`.
// The health for this case is:
// (weighted(baseLots + bids) - bids) * baseLotSize * price + quote
// 2. The price goes high and all asks execute, converting to quote.
// The health for this case is:
// (weighted(baseLots - asks) + asks) * baseLotSize * price + quote
//
// Comparing these makes it clear we need to pick the worse subfactor
// weighted(baseLots + bids) - bids =: scenario1
// or
// weighted(baseLots - asks) + asks =: scenario2
//
// Additionally, we want this scenario choice to be the same no matter whether we're
// computing init or maint health. This can be guaranteed by requiring the weights
// to satisfy the property (P):
//
// (1 - initAssetWeight) / (initLiabWeight - 1)
// == (1 - maintAssetWeight) / (maintLiabWeight - 1)
//
// Derivation:
// Set asksNetLots := baseLots - asks, bidsNetLots := baseLots + bids.
// Now
// scenario1 = weighted(bidsNetLots) - bidsNetLots + baseLots and
// scenario2 = weighted(asksNetLots) - asksNetLots + baseLots
// So with expanding weigthed(a) = weightFactorForA * a, the question
// scenario1 < scenario2
// becomes:
// (weightFactorForBidsNetLots - 1) * bidsNetLots
// < (weightFactorForAsksNetLots - 1) * asksNetLots
// Since asksNetLots < 0 and bidsNetLots > 0 is the only interesting case, (P) follows.
//
// We satisfy (P) by requiring
// assetWeight = 1 - x and liabWeight = 1 + x
//
// And with that assumption the scenario choice condition further simplifies to:
// scenario1 < scenario2
// iff abs(bidsNetLots) > abs(asksNetLots)
const bidsNetLots = baseLots.add(
I80F48.fromNumber(perpPosition.bidsBaseLots),
);
const asksNetLots = baseLots.sub(
I80F48.fromNumber(perpPosition.asksBaseLots),
);
const lotsToQuote = baseLotSize.mul(perpMarket.price);
let base, quote;
if (bidsNetLots.abs().gt(asksNetLots.abs())) {
const bidsBaseLots = I80F48.fromString(
perpPosition.bidsBaseLots.toString(),
);
base = bidsNetLots.mul(lotsToQuote);
quote = quoteCurrent.sub(bidsBaseLots.mul(lotsToQuote));
} else {
const asksBaseLots = I80F48.fromString(
perpPosition.asksBaseLots.toString(),
);
base = asksNetLots.mul(lotsToQuote);
quote = quoteCurrent.add(asksBaseLots.mul(lotsToQuote));
}
// console.log(`bidsNetLots ${bidsNetLots}`);
// console.log(`asksNetLots ${asksNetLots}`);
// console.log(`quoteCurrent ${quoteCurrent}`);
// console.log(`base ${base}`);
// console.log(`quote ${quote}`);
return new PerpInfo(
perpMarket.perpMarketIndex,
perpMarket.maintAssetWeight,
perpMarket.initAssetWeight,
perpMarket.maintLiabWeight,
perpMarket.initLiabWeight,
base,
quote,
perpMarket.price,
perpPosition.hasOpenOrders(),
perpMarket.trustedMarket,
);
}
@ -1006,16 +1177,21 @@ export class PerpInfo {
weight = this.maintAssetWeight;
}
// FUTURE: Allow v3-style "reliable" markets where we can return
// `self.quote + weight * self.base` here
return this.quote.add(weight.mul(this.base)).min(ZERO_I80F48());
// console.log(`initLiabWeight ${this.initLiabWeight}`);
// console.log(`initAssetWeight ${this.initAssetWeight}`);
// console.log(`weight ${weight}`);
// console.log(`this.quote ${this.quote}`);
// console.log(`this.base ${this.base}`);
const uncappedHealthContribution = this.quote.add(weight.mul(this.base));
if (this.trustedMarket) {
return uncappedHealthContribution;
} else {
return uncappedHealthContribution.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,
@ -1024,10 +1200,19 @@ export class PerpInfo {
perpMarket.initLiabWeight,
ZERO_I80F48(),
ZERO_I80F48(),
I80F48.fromNumber(perpMarket.price),
perpMarket.price,
false,
perpMarket.trustedMarket,
);
}
toString(): string {
return ` perpMarketIndex: ${this.perpMarketIndex}, base: ${
this.base
}, quote: ${this.quote}, oraclePrice: ${
this.oraclePrice
}, initHealth ${this.healthContribution(HealthType.init)}`;
}
}
export class HealthCacheDto {
@ -1087,10 +1272,9 @@ export class PerpInfoDto {
initAssetWeight: I80F48Dto;
maintLiabWeight: I80F48Dto;
initLiabWeight: I80F48Dto;
// in health-reference-token native units, needs scaling by asset/liab
base: I80F48Dto;
// in health-reference-token native units, no asset/liab factor needed
quote: I80F48Dto;
oraclePrice: I80F48Dto;
hasOpenOrders: boolean;
trustedMarket: boolean;
}

View File

@ -1,20 +1,21 @@
import { BN } from '@project-serum/anchor';
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { Order, Orderbook } from '@project-serum/serum/lib/market';
import { OpenOrders, Order, Orderbook } from '@project-serum/serum/lib/market';
import { PublicKey } from '@solana/web3.js';
import { MangoClient } from '../client';
import { SERUM3_PROGRAM_ID } from '../constants';
import {
nativeI80F48ToUi,
toNative,
toUiDecimals,
toUiDecimalsForQuote,
} from '../utils';
import { Bank } from './bank';
import { Bank, TokenIndex } from './bank';
import { Group } from './group';
import { HealthCache, HealthCacheDto } from './healthCache';
import { HealthCache } from './healthCache';
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48';
import { PerpOrder, PerpOrderSide } from './perp';
import { Serum3Side } from './serum3';
import { PerpMarket, PerpMarketIndex, PerpOrder, PerpOrderSide } from './perp';
import { MarketIndex, Serum3Side } from './serum3';
export class MangoAccount {
public tokens: TokenPosition[];
public serum3: Serum3Orders[];
@ -58,7 +59,7 @@ export class MangoAccount {
obj.serum3 as Serum3PositionDto[],
obj.perps as PerpPositionDto[],
obj.perpOpenOrders as PerpOoDto[],
{} as any,
new Map(), // serum3OosMapByMarketIndex
);
}
@ -78,39 +79,56 @@ export class MangoAccount {
serum3: Serum3PositionDto[],
perps: PerpPositionDto[],
perpOpenOrders: PerpOoDto[],
public accountData: undefined | MangoAccountData,
public serum3OosMapByMarketIndex: Map<number, OpenOrders>,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
this.tokens = tokens.map((dto) => TokenPosition.from(dto));
this.serum3 = serum3.map((dto) => Serum3Orders.from(dto));
this.perps = perps.map((dto) => PerpPosition.from(dto));
this.perpOpenOrders = perpOpenOrders.map((dto) => PerpOo.from(dto));
this.accountData = undefined;
this.netDeposits = netDeposits;
}
async reload(client: MangoClient, group: Group): Promise<MangoAccount> {
async reload(client: MangoClient): Promise<MangoAccount> {
const mangoAccount = await client.getMangoAccount(this);
await mangoAccount.reloadAccountData(client, group);
await mangoAccount.reloadAccountData(client);
Object.assign(this, mangoAccount);
return mangoAccount;
}
async reloadWithSlot(
client: MangoClient,
group: Group,
): Promise<{ value: MangoAccount; slot: number }> {
const resp = await client.getMangoAccountWithSlot(this.publicKey);
await resp?.value.reloadAccountData(client, group);
await resp?.value.reloadAccountData(client);
Object.assign(this, resp?.value);
return { value: resp!.value, slot: resp!.slot };
}
async reloadAccountData(
client: MangoClient,
group: Group,
): Promise<MangoAccount> {
this.accountData = await client.computeAccountData(group, this);
async reloadAccountData(client: MangoClient): Promise<MangoAccount> {
const serum3Active = this.serum3Active();
const ais =
await client.program.provider.connection.getMultipleAccountsInfo(
serum3Active.map((serum3) => serum3.openOrders),
);
this.serum3OosMapByMarketIndex = new Map(
Array.from(
ais.map((ai, i) => {
if (!ai) {
throw new Error(
`Undefined AI for open orders ${serum3Active[i].openOrders} and market ${serum3Active[i].marketIndex}!`,
);
}
const oo = OpenOrders.fromAccountInfo(
serum3Active[i].openOrders,
ai,
SERUM3_PROGRAM_ID[client.cluster],
);
return [serum3Active[i].marketIndex, oo];
}),
),
);
return this;
}
@ -132,14 +150,26 @@ export class MangoAccount {
);
}
findToken(tokenIndex: number): TokenPosition | undefined {
getToken(tokenIndex: TokenIndex): TokenPosition | undefined {
return this.tokens.find((ta) => ta.tokenIndex == tokenIndex);
}
findSerum3Account(marketIndex: number): Serum3Orders | undefined {
getSerum3Account(marketIndex: MarketIndex): Serum3Orders | undefined {
return this.serum3.find((sa) => sa.marketIndex == marketIndex);
}
getSerum3OoAccount(marketIndex: MarketIndex): OpenOrders {
const oo: OpenOrders | undefined =
this.serum3OosMapByMarketIndex.get(marketIndex);
if (!oo) {
throw new Error(
`Open orders account not loaded for market with marketIndex ${marketIndex}!`,
);
}
return oo;
}
// How to navigate
// * if a function is returning a I80F48, then usually the return value is in native quote or native token, unless specified
// * if a function is returning a number, then usually the return value is in ui tokens, unless specified
@ -152,7 +182,7 @@ export class MangoAccount {
* @returns native balance for a token, is signed
*/
getTokenBalance(bank: Bank): I80F48 {
const tp = this.findToken(bank.tokenIndex);
const tp = this.getToken(bank.tokenIndex);
return tp ? tp.balance(bank) : ZERO_I80F48();
}
@ -162,7 +192,7 @@ export class MangoAccount {
* @returns native deposits for a token, 0 if position has borrows
*/
getTokenDeposits(bank: Bank): I80F48 {
const tp = this.findToken(bank.tokenIndex);
const tp = this.getToken(bank.tokenIndex);
return tp ? tp.deposits(bank) : ZERO_I80F48();
}
@ -172,7 +202,7 @@ export class MangoAccount {
* @returns native borrows for a token, 0 if position has deposits
*/
getTokenBorrows(bank: Bank): I80F48 {
const tp = this.findToken(bank.tokenIndex);
const tp = this.getToken(bank.tokenIndex);
return tp ? tp.borrows(bank) : ZERO_I80F48();
}
@ -182,7 +212,7 @@ export class MangoAccount {
* @returns UI balance for a token, is signed
*/
getTokenBalanceUi(bank: Bank): number {
const tp = this.findToken(bank.tokenIndex);
const tp = this.getToken(bank.tokenIndex);
return tp ? tp.balanceUi(bank) : 0;
}
@ -192,7 +222,7 @@ export class MangoAccount {
* @returns UI deposits for a token, 0 or more
*/
getTokenDepositsUi(bank: Bank): number {
const ta = this.findToken(bank.tokenIndex);
const ta = this.getToken(bank.tokenIndex);
return ta ? ta.depositsUi(bank) : 0;
}
@ -202,7 +232,7 @@ export class MangoAccount {
* @returns UI borrows for a token, 0 or less
*/
getTokenBorrowsUi(bank: Bank): number {
const ta = this.findToken(bank.tokenIndex);
const ta = this.getToken(bank.tokenIndex);
return ta ? ta.borrowsUi(bank) : 0;
}
@ -211,10 +241,9 @@ export class MangoAccount {
* @param healthType
* @returns raw health number, in native quote
*/
getHealth(healthType: HealthType): I80F48 | undefined {
return healthType == HealthType.init
? this.accountData?.initHealth
: this.accountData?.maintHealth;
getHealth(group: Group, healthType: HealthType): I80F48 {
const hc = HealthCache.fromMangoAccount(group, this);
return hc.health(healthType);
}
/**
@ -223,8 +252,9 @@ export class MangoAccount {
* @param healthType
* @returns health ratio, in percentage form
*/
getHealthRatio(healthType: HealthType): I80F48 | undefined {
return this.accountData?.healthCache.healthRatio(healthType);
getHealthRatio(group: Group, healthType: HealthType): I80F48 {
const hc = HealthCache.fromMangoAccount(group, this);
return hc.healthRatio(healthType);
}
/**
@ -232,8 +262,8 @@ export class MangoAccount {
* @param healthType
* @returns health ratio, in percentage form, capped to 100
*/
getHealthRatioUi(healthType: HealthType): number | undefined {
const ratio = this.getHealthRatio(healthType)?.toNumber();
getHealthRatioUi(group: Group, healthType: HealthType): number | undefined {
const ratio = this.getHealthRatio(group, healthType).toNumber();
if (ratio) {
return ratio > 100 ? 100 : Math.trunc(ratio);
} else {
@ -245,40 +275,73 @@ export class MangoAccount {
* Sum of all the assets i.e. token deposits, borrows, total assets in spot open orders, (perps positions is todo) in terms of quote value.
* @returns equity, in native quote
*/
getEquity(): I80F48 | undefined {
if (this.accountData) {
const equity = this.accountData.equity;
const total_equity = equity.tokens.reduce(
(a, b) => a.add(b.value),
ZERO_I80F48(),
);
return total_equity;
getEquity(group: Group): I80F48 {
const tokensMap = new Map<number, I80F48>();
for (const tp of this.tokensActive()) {
const bank = group.getFirstBankByTokenIndex(tp.tokenIndex);
tokensMap.set(tp.tokenIndex, tp.balance(bank).mul(bank.price));
}
return undefined;
for (const sp of this.serum3Active()) {
const oo = this.getSerum3OoAccount(sp.marketIndex);
const baseBank = group.getFirstBankByTokenIndex(sp.baseTokenIndex);
tokensMap
.get(baseBank.tokenIndex)!
.iadd(
I80F48.fromString(oo.baseTokenTotal.toString()).mul(baseBank.price),
);
const quoteBank = group.getFirstBankByTokenIndex(sp.quoteTokenIndex);
// NOTE: referrerRebatesAccrued is not declared on oo class, but the layout
// is aware of it
tokensMap
.get(baseBank.tokenIndex)!
.iadd(
I80F48.fromString(
oo.quoteTokenTotal
.add((oo as any).referrerRebatesAccrued)
.toString(),
).mul(quoteBank.price),
);
}
const tokenEquity = Array.from(tokensMap.values()).reduce(
(a, b) => a.add(b),
ZERO_I80F48(),
);
const perpEquity = this.perpActive().reduce(
(a, b) =>
a.add(b.getEquity(group.getPerpMarketByMarketIndex(b.marketIndex))),
ZERO_I80F48(),
);
return tokenEquity.add(perpEquity);
}
/**
* The amount of native quote you could withdraw against your existing assets.
* @returns collateral value, in native quote
*/
getCollateralValue(): I80F48 | undefined {
return this.getHealth(HealthType.init);
getCollateralValue(group: Group): I80F48 {
return this.getHealth(group, HealthType.init);
}
/**
* Sum of all positive assets.
* @returns assets, in native quote
*/
getAssetsValue(healthType: HealthType): I80F48 | undefined {
return this.accountData?.healthCache.assets(healthType);
getAssetsValue(group: Group, healthType: HealthType): I80F48 {
const hc = HealthCache.fromMangoAccount(group, this);
return hc.assets(healthType);
}
/**
* Sum of all negative assets.
* @returns liabs, in native quote
*/
getLiabsValue(healthType: HealthType): I80F48 | undefined {
return this.accountData?.healthCache.liabs(healthType);
getLiabsValue(group: Group, healthType: HealthType): I80F48 {
const hc = HealthCache.fromMangoAccount(group, this);
return hc.liabs(healthType);
}
/**
@ -286,8 +349,8 @@ export class MangoAccount {
* PNL is defined here as spot value + serum3 open orders value + perp value - net deposits value (evaluated at native quote price at the time of the deposit/withdraw)
* spot value + serum3 open orders value + perp value is returned by getEquity (open orders values are added to spot token values implicitly)
*/
getPnl(): I80F48 | undefined {
return this.getEquity()?.add(
getPnl(group: Group): I80F48 {
return this.getEquity(group)?.add(
I80F48.fromI64(this.netDeposits).mul(I80F48.fromNumber(-1)),
);
}
@ -301,7 +364,7 @@ export class MangoAccount {
mintPk: PublicKey,
): I80F48 | undefined {
const tokenBank: Bank = group.getFirstBankByMint(mintPk);
const initHealth = this.accountData?.initHealth;
const initHealth = this.getHealth(group, HealthType.init);
if (!initHealth) return undefined;
@ -314,8 +377,7 @@ export class MangoAccount {
// Deposits need special treatment since they would neither count towards liabilities
// nor would be charged loanOriginationFeeRate when withdrawn
const tp = this.findToken(tokenBank.tokenIndex);
if (!tokenBank.price) return undefined;
const tp = this.getToken(tokenBank.tokenIndex);
const existingTokenDeposits = tp ? tp.deposits(tokenBank) : ZERO_I80F48();
let existingPositionHealthContrib = ZERO_I80F48();
if (existingTokenDeposits.gt(ZERO_I80F48())) {
@ -377,18 +439,14 @@ export class MangoAccount {
targetMintPk: PublicKey,
priceFactor: number,
): number | undefined {
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(
const hc = HealthCache.fromMangoAccount(group, this);
const maxSource = hc.getMaxSourceForTokenSwap(
group.getFirstBankByMint(sourceMintPk),
group.getFirstBankByMint(targetMintPk),
ONE_I80F48(), // target 1% health
I80F48.fromNumber(2), // target 2% health
I80F48.fromNumber(priceFactor),
);
maxSource.idiv(
@ -426,7 +484,8 @@ export class MangoAccount {
mintPk: tokenChange.mintPk,
};
});
return this.accountData?.healthCache
const hc = HealthCache.fromMangoAccount(group, this);
return hc
.simHealthRatioWithTokenPositionChanges(
group,
nativeTokenChanges,
@ -440,14 +499,8 @@ export class MangoAccount {
group: Group,
externalMarketPk: PublicKey,
): Promise<Order[]> {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
);
if (!serum3Market) {
throw new Error(
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
const serum3Market =
group.getSerum3MarketByExternalMarket(externalMarketPk);
const serum3OO = this.serum3Active().find(
(s) => s.marketIndex === serum3Market.marketIndex,
);
@ -455,7 +508,7 @@ export class MangoAccount {
throw new Error(`No open orders account found for ${externalMarketPk}`);
}
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
const serum3MarketExternal = group.serum3ExternalMarketsMap.get(
externalMarketPk.toBase58(),
)!;
const [bidsInfo, asksInfo] =
@ -463,9 +516,14 @@ export class MangoAccount {
serum3MarketExternal.bidsAddress,
serum3MarketExternal.asksAddress,
]);
if (!bidsInfo || !asksInfo) {
if (!bidsInfo) {
throw new Error(
`bids and asks ai were not fetched for ${externalMarketPk.toString()}`,
`Undefined bidsInfo for serum3Market with externalMarket ${externalMarketPk.toString()!}`,
);
}
if (!asksInfo) {
throw new Error(
`Undefined asksInfo for serum3Market with externalMarket ${externalMarketPk.toString()!}`,
);
}
const bids = Orderbook.decode(serum3MarketExternal, bidsInfo.data);
@ -476,7 +534,6 @@ export class MangoAccount {
}
/**
* TODO priceFactor
* @param group
* @param externalMarketPk
* @returns maximum ui quote which can be traded for base token given current health
@ -485,26 +542,28 @@ export class MangoAccount {
group: Group,
externalMarketPk: PublicKey,
): number {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
const serum3Market =
group.getSerum3MarketByExternalMarket(externalMarketPk);
const baseBank = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
);
if (!serum3Market) {
throw new Error(
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
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),
);
const quoteBank = group.getFirstBankByTokenIndex(
serum3Market.quoteTokenIndex,
);
const hc = HealthCache.fromMangoAccount(group, this);
let nativeAmount = hc.getMaxSerum3OrderForHealthRatio(
baseBank,
quoteBank,
serum3Market,
Serum3Side.bid,
I80F48.fromNumber(2),
);
// If its a bid then the reserved fund and potential loan is in base
// also keep some buffer for fees, use taker fees for worst case simulation.
nativeAmount = nativeAmount
.div(quoteBank.price)
.div(ONE_I80F48().add(baseBank.loanOriginationFeeRate))
.div(ONE_I80F48().add(I80F48.fromNumber(group.getSerum3FeeRates(false))));
return toUiDecimals(
nativeAmount,
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex).mintDecimals,
@ -512,7 +571,6 @@ export class MangoAccount {
}
/**
* TODO priceFactor
* @param group
* @param externalMarketPk
* @returns maximum ui base which can be traded for quote token given current health
@ -521,26 +579,28 @@ export class MangoAccount {
group: Group,
externalMarketPk: PublicKey,
): number {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
const serum3Market =
group.getSerum3MarketByExternalMarket(externalMarketPk);
const baseBank = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
);
if (!serum3Market) {
throw new Error(
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
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),
);
const quoteBank = group.getFirstBankByTokenIndex(
serum3Market.quoteTokenIndex,
);
const hc = HealthCache.fromMangoAccount(group, this);
let nativeAmount = hc.getMaxSerum3OrderForHealthRatio(
baseBank,
quoteBank,
serum3Market,
Serum3Side.ask,
I80F48.fromNumber(2),
);
// If its a ask then the reserved fund and potential loan is in base
// also keep some buffer for fees, use taker fees for worst case simulation.
nativeAmount = nativeAmount
.div(baseBank.price)
.div(ONE_I80F48().add(baseBank.loanOriginationFeeRate))
.div(ONE_I80F48().add(I80F48.fromNumber(group.getSerum3FeeRates(false))));
return toUiDecimals(
nativeAmount,
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex).mintDecimals,
@ -561,22 +621,19 @@ export class MangoAccount {
externalMarketPk: PublicKey,
healthType: HealthType = HealthType.init,
): number {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
const serum3Market =
group.getSerum3MarketByExternalMarket(externalMarketPk);
const baseBank = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
);
if (!serum3Market) {
throw new Error(
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
if (!this.accountData) {
throw new Error(
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
);
}
return this.accountData.healthCache
const quoteBank = group.getFirstBankByTokenIndex(
serum3Market.quoteTokenIndex,
);
const hc = HealthCache.fromMangoAccount(group, this);
return hc
.simHealthRatioWithSerum3BidChanges(
group,
baseBank,
quoteBank,
toNative(
uiQuoteAmount,
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex)
@ -602,22 +659,19 @@ export class MangoAccount {
externalMarketPk: PublicKey,
healthType: HealthType = HealthType.init,
): number {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
const serum3Market =
group.getSerum3MarketByExternalMarket(externalMarketPk);
const baseBank = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
);
if (!serum3Market) {
throw new Error(
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
if (!this.accountData) {
throw new Error(
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
);
}
return this.accountData.healthCache
const quoteBank = group.getFirstBankByTokenIndex(
serum3Market.quoteTokenIndex,
);
const hc = HealthCache.fromMangoAccount(group, this);
return hc
.simHealthRatioWithSerum3AskChanges(
group,
baseBank,
quoteBank,
toNative(
uiBaseAmount,
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
@ -638,28 +692,21 @@ export class MangoAccount {
*/
public getMaxQuoteForPerpBidUi(
group: Group,
perpMarketName: string,
perpMarketIndex: PerpMarketIndex,
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(
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const hc = HealthCache.fromMangoAccount(group, this);
const baseLots = hc.getMaxPerpForHealthRatio(
perpMarket,
PerpOrderSide.bid,
I80F48.fromNumber(1),
I80F48.fromNumber(2),
group.toNativePrice(uiPrice, perpMarket.baseDecimals),
);
const nativeBase = baseLots.mul(
I80F48.fromString(perpMarket.baseLotSize.toString()),
);
const nativeQuote = nativeBase.mul(I80F48.fromNumber(perpMarket.price));
const nativeQuote = nativeBase.mul(perpMarket.price);
return toUiDecimalsForQuote(nativeQuote.toNumber());
}
@ -672,22 +719,15 @@ export class MangoAccount {
*/
public getMaxBaseForPerpAskUi(
group: Group,
perpMarketName: string,
perpMarketIndex: PerpMarketIndex,
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(
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const hc = HealthCache.fromMangoAccount(group, this);
const baseLots = hc.getMaxPerpForHealthRatio(
perpMarket,
PerpOrderSide.ask,
I80F48.fromNumber(1),
I80F48.fromNumber(2),
group.toNativePrice(uiPrice, perpMarket.baseDecimals),
);
return perpMarket.baseLotsToUi(new BN(baseLots.toString()));
@ -696,12 +736,9 @@ export class MangoAccount {
public async loadPerpOpenOrdersForMarket(
client: MangoClient,
group: Group,
perpMarketName: string,
perpMarketIndex: PerpMarketIndex,
): Promise<PerpOrder[]> {
const perpMarket = group.perpMarketsMap.get(perpMarketName);
if (!perpMarket) {
throw new Error(`Perp Market ${perpMarketName} not found!`);
}
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const [bids, asks] = await Promise.all([
perpMarket.loadBids(client),
perpMarket.loadAsks(client),
@ -759,17 +796,17 @@ export class MangoAccount {
export class TokenPosition {
static TokenIndexUnset = 65535;
static from(dto: TokenPositionDto) {
static from(dto: TokenPositionDto): TokenPosition {
return new TokenPosition(
I80F48.from(dto.indexedPosition),
dto.tokenIndex,
dto.tokenIndex as TokenIndex,
dto.inUseCount,
);
}
constructor(
public indexedPosition: I80F48,
public tokenIndex: number,
public tokenIndex: TokenIndex,
public inUseCount: number,
) {}
@ -877,17 +914,17 @@ export class Serum3Orders {
static from(dto: Serum3PositionDto): Serum3Orders {
return new Serum3Orders(
dto.openOrders,
dto.marketIndex,
dto.baseTokenIndex,
dto.quoteTokenIndex,
dto.marketIndex as MarketIndex,
dto.baseTokenIndex as TokenIndex,
dto.quoteTokenIndex as TokenIndex,
);
}
constructor(
public openOrders: PublicKey,
public marketIndex: number,
public baseTokenIndex: number,
public quoteTokenIndex: number,
public marketIndex: MarketIndex,
public baseTokenIndex: TokenIndex,
public quoteTokenIndex: TokenIndex,
) {}
public isActive(): boolean {
@ -907,31 +944,77 @@ export class Serum3PositionDto {
export class PerpPosition {
static PerpMarketIndexUnset = 65535;
static from(dto: PerpPositionDto) {
static from(dto: PerpPositionDto): PerpPosition {
return new PerpPosition(
dto.marketIndex,
dto.marketIndex as PerpMarketIndex,
dto.basePositionLots.toNumber(),
dto.quotePositionNative.val,
I80F48.from(dto.quotePositionNative),
dto.bidsBaseLots.toNumber(),
dto.asksBaseLots.toNumber(),
dto.takerBaseLots.toNumber(),
dto.takerQuoteLots.toNumber(),
I80F48.from(dto.longSettledFunding),
I80F48.from(dto.shortSettledFunding),
);
}
constructor(
public marketIndex: number,
public marketIndex: PerpMarketIndex,
public basePositionLots: number,
public quotePositionNative: BN,
public quotePositionNative: I80F48,
public bidsBaseLots: number,
public asksBaseLots: number,
public takerBaseLots: number,
public takerQuoteLots: number,
public longSettledFunding: I80F48,
public shortSettledFunding: I80F48,
) {}
isActive(): boolean {
return this.marketIndex != PerpPosition.PerpMarketIndexUnset;
}
public unsettledFunding(perpMarket: PerpMarket): I80F48 {
if (this.basePositionLots > 0) {
return perpMarket.longFunding
.sub(this.longSettledFunding)
.mul(I80F48.fromString(this.basePositionLots.toString()));
} else if (this.basePositionLots < 0) {
return perpMarket.shortFunding
.sub(this.shortSettledFunding)
.mul(I80F48.fromString(this.basePositionLots.toString()));
}
return ZERO_I80F48();
}
public getEquity(perpMarket: PerpMarket): I80F48 {
const lotsToQuote = I80F48.fromString(
perpMarket.baseLotSize.toString(),
).mul(perpMarket.price);
const baseLots = I80F48.fromNumber(
this.basePositionLots + this.takerBaseLots,
);
const unsettledFunding = this.unsettledFunding(perpMarket);
const takerQuote = I80F48.fromString(
new BN(this.takerQuoteLots).mul(perpMarket.quoteLotSize).toString(),
);
const quoteCurrent = I80F48.fromString(this.quotePositionNative.toString())
.sub(unsettledFunding)
.add(takerQuote);
return baseLots.mul(lotsToQuote).add(quoteCurrent);
}
public hasOpenOrders(): boolean {
return (
this.asksBaseLots != 0 ||
this.bidsBaseLots != 0 ||
this.takerBaseLots != 0 ||
this.takerQuoteLots != 0
);
}
}
export class PerpPositionDto {
@ -944,12 +1027,14 @@ export class PerpPositionDto {
public asksBaseLots: BN,
public takerBaseLots: BN,
public takerQuoteLots: BN,
public longSettledFunding: I80F48Dto,
public shortSettledFunding: I80F48Dto,
) {}
}
export class PerpOo {
static OrderMarketUnset = 65535;
static from(dto: PerpOoDto) {
static from(dto: PerpOoDto): PerpOo {
return new PerpOo(
dto.orderSide,
dto.orderMarket,
@ -978,66 +1063,3 @@ export class HealthType {
static maint = { maint: {} };
static init = { init: {} };
}
export class MangoAccountData {
constructor(
public healthCache: HealthCache,
public initHealth: I80F48,
public maintHealth: I80F48,
public equity: Equity,
) {}
static from(event: {
healthCache: HealthCacheDto;
initHealth: I80F48Dto;
maintHealth: I80F48Dto;
equity: {
tokens: [{ tokenIndex: number; value: I80F48Dto }];
perps: [{ perpMarketIndex: number; value: I80F48Dto }];
};
initHealthLiabs: I80F48Dto;
tokenAssets: any;
}) {
return new MangoAccountData(
HealthCache.fromDto(event.healthCache),
I80F48.from(event.initHealth),
I80F48.from(event.maintHealth),
Equity.from(event.equity),
);
}
}
export class Equity {
public constructor(
public tokens: TokenEquity[],
public perps: PerpEquity[],
) {}
static from(dto: EquityDto): Equity {
return new Equity(
dto.tokens.map(
(token) => new TokenEquity(token.tokenIndex, I80F48.from(token.value)),
),
dto.perps.map(
(perpAccount) =>
new PerpEquity(
perpAccount.perpMarketIndex,
I80F48.from(perpAccount.value),
),
),
);
}
}
export class TokenEquity {
public constructor(public tokenIndex: number, public value: I80F48) {}
}
export class PerpEquity {
public constructor(public perpMarketIndex: number, public value: I80F48) {}
}
export class EquityDto {
tokens: { tokenIndex: number; value: I80F48Dto }[];
perps: { perpMarketIndex: number; value: I80F48Dto }[];
}

View File

@ -102,7 +102,7 @@ export async function parseSwitchboardOracle(
return parseSwitcboardOracleV1(accountInfo);
}
throw new Error(`Unable to parse switchboard oracle ${accountInfo.owner}`);
throw new Error(`Should not be reached!`);
}
export function isSwitchboardOracle(accountInfo: AccountInfo<Buffer>): boolean {

View File

@ -3,10 +3,12 @@ import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { PublicKey } from '@solana/web3.js';
import Big from 'big.js';
import { MangoClient } from '../client';
import { U64_MAX_BN } from '../utils';
import { As, U64_MAX_BN } from '../utils';
import { OracleConfig, QUOTE_DECIMALS } from './bank';
import { I80F48, I80F48Dto } from './I80F48';
export type PerpMarketIndex = number & As<'perp-market-index'>;
export class PerpMarket {
public name: string;
public maintAssetWeight: I80F48;
@ -18,21 +20,23 @@ export class PerpMarket {
public takerFee: I80F48;
public minFunding: I80F48;
public maxFunding: I80F48;
public longFunding: I80F48;
public shortFunding: I80F48;
public openInterest: number;
public seqNum: number;
public feesAccrued: I80F48;
priceLotsToUiConverter: number;
baseLotsToUiConverter: number;
quoteLotsToUiConverter: number;
public price: number;
public uiPrice: number;
public _price: I80F48;
public _uiPrice: number;
static from(
publicKey: PublicKey,
obj: {
group: PublicKey;
quoteTokenIndex: number;
perpMarketIndex: number;
trustedMarket: number;
name: number[];
oracle: PublicKey;
oracleConfig: OracleConfig;
@ -55,7 +59,7 @@ export class PerpMarket {
shortFunding: I80F48Dto;
fundingLastUpdated: BN;
openInterest: BN;
seqNum: any; // TODO: ts complains that this is unknown for whatever reason
seqNum: BN;
feesAccrued: I80F48Dto;
bump: number;
baseDecimals: number;
@ -65,8 +69,8 @@ export class PerpMarket {
return new PerpMarket(
publicKey,
obj.group,
obj.quoteTokenIndex,
obj.perpMarketIndex,
obj.perpMarketIndex as PerpMarketIndex,
obj.trustedMarket == 1,
obj.name,
obj.oracle,
obj.oracleConfig,
@ -100,8 +104,8 @@ export class PerpMarket {
constructor(
public publicKey: PublicKey,
public group: PublicKey,
public quoteTokenIndex: number,
public perpMarketIndex: number,
public perpMarketIndex: PerpMarketIndex, // TODO rename to marketIndex?
public trustedMarket: boolean,
name: number[],
public oracle: PublicKey,
oracleConfig: OracleConfig,
@ -140,6 +144,8 @@ export class PerpMarket {
this.takerFee = I80F48.from(takerFee);
this.minFunding = I80F48.from(minFunding);
this.maxFunding = I80F48.from(maxFunding);
this.longFunding = I80F48.from(longFunding);
this.shortFunding = I80F48.from(shortFunding);
this.openInterest = openInterest.toNumber();
this.seqNum = seqNum.toNumber();
this.feesAccrued = I80F48.from(feesAccrued);
@ -159,6 +165,23 @@ export class PerpMarket {
.toNumber();
}
get price(): I80F48 {
if (!this._price) {
throw new Error(
`Undefined price for perpMarket ${this.publicKey} with marketIndex ${this.perpMarketIndex}!`,
);
}
return this._price;
}
get uiPrice(): number {
if (!this._uiPrice) {
throw new Error(
`Undefined price for perpMarket ${this.publicKey} with marketIndex ${this.perpMarketIndex}!`,
);
}
return this._uiPrice;
}
public async loadAsks(client: MangoClient): Promise<BookSide> {
const asks = await client.program.account.bookSide.fetch(this.asks);
return BookSide.from(client, this, BookSideType.asks, asks);
@ -176,7 +199,10 @@ export class PerpMarket {
return new PerpEventQueue(client, eventQueue.header, eventQueue.buf);
}
public async loadFills(client: MangoClient, lastSeqNum: BN) {
public async loadFills(
client: MangoClient,
lastSeqNum: BN,
): Promise<(OutEvent | FillEvent | LiquidateEvent)[]> {
const eventQueue = await this.loadEventQueue(client);
return eventQueue
.eventsSince(lastSeqNum)
@ -189,13 +215,13 @@ export class PerpMarket {
* @param asks
* @returns returns funding rate per hour
*/
public getCurrentFundingRate(bids: BookSide, asks: BookSide) {
public getCurrentFundingRate(bids: BookSide, asks: BookSide): number {
const MIN_FUNDING = this.minFunding.toNumber();
const MAX_FUNDING = this.maxFunding.toNumber();
const bid = bids.getImpactPriceUi(new BN(this.impactQuantity));
const ask = asks.getImpactPriceUi(new BN(this.impactQuantity));
const indexPrice = this.uiPrice;
const indexPrice = this._uiPrice;
let funding;
if (bid !== undefined && ask !== undefined) {
@ -284,7 +310,7 @@ export class BookSide {
leafCount: number;
nodes: unknown;
},
) {
): BookSide {
return new BookSide(
client,
perpMarket,
@ -311,7 +337,6 @@ export class BookSide {
public includeExpired = false,
maxBookDelay?: number,
) {
// TODO why? Ask Daffy
// Determine the maxTimestamp found on the book to use for tif
// If maxBookDelay is not provided, use 3600 as a very large number
maxBookDelay = maxBookDelay === undefined ? 3600 : maxBookDelay;
@ -329,7 +354,7 @@ export class BookSide {
this.now = maxTimestamp;
}
static getPriceFromKey(key: BN) {
static getPriceFromKey(key: BN): BN {
return key.ushrn(64);
}
@ -456,7 +481,7 @@ export class LeafNode {
) {}
}
export class InnerNode {
static from(obj: { children: [number] }) {
static from(obj: { children: [number] }): InnerNode {
return new InnerNode(obj.children);
}
@ -477,7 +502,11 @@ export class PerpOrderType {
}
export class PerpOrder {
static from(perpMarket: PerpMarket, leafNode: LeafNode, type: BookSideType) {
static from(
perpMarket: PerpMarket,
leafNode: LeafNode,
type: BookSideType,
): PerpOrder {
const side =
type == BookSideType.bids ? PerpOrderSide.bid : PerpOrderSide.ask;
const price = BookSide.getPriceFromKey(leafNode.key);
@ -555,7 +584,7 @@ export class PerpEventQueue {
),
);
}
throw new Error(`Unknown event with eventType ${event.eventType}`);
throw new Error(`Unknown event with eventType ${event.eventType}!`);
});
}

View File

@ -4,9 +4,13 @@ import { Cluster, PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
import { MangoClient } from '../client';
import { SERUM3_PROGRAM_ID } from '../constants';
import { As } from '../utils';
import { TokenIndex } from './bank';
import { Group } from './group';
import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from './I80F48';
export type MarketIndex = number & As<'market-index'>;
export class Serum3Market {
public name: string;
static from(
@ -26,12 +30,12 @@ export class Serum3Market {
return new Serum3Market(
publicKey,
obj.group,
obj.baseTokenIndex,
obj.quoteTokenIndex,
obj.baseTokenIndex as TokenIndex,
obj.quoteTokenIndex as TokenIndex,
obj.name,
obj.serumProgram,
obj.serumMarketExternal,
obj.marketIndex,
obj.marketIndex as MarketIndex,
obj.registrationTime,
);
}
@ -39,12 +43,12 @@ export class Serum3Market {
constructor(
public publicKey: PublicKey,
public group: PublicKey,
public baseTokenIndex: number,
public quoteTokenIndex: number,
public baseTokenIndex: TokenIndex,
public quoteTokenIndex: TokenIndex,
name: number[],
public serumProgram: PublicKey,
public serumMarketExternal: PublicKey,
public marketIndex: number,
public marketIndex: MarketIndex,
public registrationTime: BN,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
@ -58,19 +62,7 @@ export class Serum3Market {
*/
maxBidLeverage(group: Group): number {
const baseBank = group.getFirstBankByTokenIndex(this.baseTokenIndex);
if (!baseBank) {
throw new Error(
`bank for base token with index ${this.baseTokenIndex} not found`,
);
}
const quoteBank = group.getFirstBankByTokenIndex(this.quoteTokenIndex);
if (!quoteBank) {
throw new Error(
`bank for quote token with index ${this.quoteTokenIndex} not found`,
);
}
if (
quoteBank.initLiabWeight.sub(baseBank.initAssetWeight).lte(ZERO_I80F48())
) {
@ -90,18 +82,7 @@ export class Serum3Market {
*/
maxAskLeverage(group: Group): number {
const baseBank = group.getFirstBankByTokenIndex(this.baseTokenIndex);
if (!baseBank) {
throw new Error(
`bank for base token with index ${this.baseTokenIndex} not found`,
);
}
const quoteBank = group.getFirstBankByTokenIndex(this.quoteTokenIndex);
if (!quoteBank) {
throw new Error(
`bank for quote token with index ${this.quoteTokenIndex} not found`,
);
}
if (
baseBank.initLiabWeight.sub(quoteBank.initAssetWeight).lte(ZERO_I80F48())
@ -115,28 +96,18 @@ export class Serum3Market {
}
public async loadBids(client: MangoClient, group: Group): Promise<Orderbook> {
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
this.serumMarketExternal.toBase58(),
const serum3MarketExternal = group.getSerum3ExternalMarket(
this.serumMarketExternal,
);
if (!serum3MarketExternal) {
throw new Error(
`Unable to find serum3MarketExternal for ${this.serumMarketExternal.toBase58()}`,
);
}
return await serum3MarketExternal.loadBids(
client.program.provider.connection,
);
}
public async loadAsks(client: MangoClient, group: Group): Promise<Orderbook> {
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
this.serumMarketExternal.toBase58(),
const serum3MarketExternal = group.getSerum3ExternalMarket(
this.serumMarketExternal,
);
if (!serum3MarketExternal) {
throw new Error(
`Unable to find serum3MarketExternal for ${this.serumMarketExternal.toBase58()}`,
);
}
return await serum3MarketExternal.loadAsks(
client.program.provider.connection,
);

View File

@ -24,14 +24,13 @@ import {
TransactionSignature,
} from '@solana/web3.js';
import bs58 from 'bs58';
import { Bank, MintInfo } from './accounts/bank';
import { Bank, MintInfo, TokenIndex } from './accounts/bank';
import { Group } from './accounts/group';
import { I80F48 } from './accounts/I80F48';
import {
MangoAccount,
MangoAccountData,
TokenPosition,
PerpPosition,
TokenPosition,
} from './accounts/mangoAccount';
import { StubOracle } from './accounts/oracle';
import {
@ -39,6 +38,7 @@ import {
OutEvent,
PerpEventQueue,
PerpMarket,
PerpMarketIndex,
PerpOrderSide,
PerpOrderType,
} from './accounts/perp';
@ -59,7 +59,6 @@ import {
I64_MAX_BN,
toNativeDecimals,
} from './utils';
import { simulate } from './utils/anchor';
import { sendTransaction } from './utils/rpc';
enum AccountRetriever {
@ -70,7 +69,6 @@ enum AccountRetriever {
export type IdsSource = 'api' | 'static' | 'get-program-accounts';
// TODO: replace ui values with native as input wherever possible
// TODO: replace token/market names with token or market indices
export class MangoClient {
private postSendTxCallback?: ({ txid }) => void;
private prioritizationFee: number;
@ -405,7 +403,7 @@ export class MangoClient {
public async getMintInfoForTokenIndex(
group: Group,
tokenIndex: number,
tokenIndex: TokenIndex,
): Promise<MintInfo[]> {
const tokenIndexBuf = Buffer.alloc(2);
tokenIndexBuf.writeUInt16LE(tokenIndex);
@ -649,21 +647,27 @@ export class MangoClient {
);
}
public async getMangoAccount(mangoAccount: MangoAccount) {
public async getMangoAccount(
mangoAccount: MangoAccount,
): Promise<MangoAccount> {
return MangoAccount.from(
mangoAccount.publicKey,
await this.program.account.mangoAccount.fetch(mangoAccount.publicKey),
);
}
public async getMangoAccountForPublicKey(mangoAccountPk: PublicKey) {
public async getMangoAccountForPublicKey(
mangoAccountPk: PublicKey,
): Promise<MangoAccount> {
return MangoAccount.from(
mangoAccountPk,
await this.program.account.mangoAccount.fetch(mangoAccountPk),
);
}
public async getMangoAccountWithSlot(mangoAccountPk: PublicKey) {
public async getMangoAccountWithSlot(
mangoAccountPk: PublicKey,
): Promise<{ slot: number; value: MangoAccount } | undefined> {
const resp =
await this.program.provider.connection.getAccountInfoAndContext(
mangoAccountPk,
@ -753,52 +757,6 @@ export class MangoClient {
);
}
public async computeAccountData(
group: Group,
mangoAccount: MangoAccount,
): Promise<MangoAccountData | undefined> {
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group,
[mangoAccount],
[],
[],
);
// Use our custom simulate fn in utils/anchor.ts so signing the tx is not required
this.program.provider.simulate = simulate;
const res = await this.program.methods
.computeAccountData()
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
})
.remainingAccounts(
healthRemainingAccounts.map(
(pk) =>
({
pubkey: pk,
isWritable: false,
isSigner: false,
} as AccountMeta),
),
)
.simulate();
if (res.events) {
const accountDataEvent = res?.events.find(
(event) => (event.name = 'MangoAccountData'),
);
return accountDataEvent
? MangoAccountData.from(accountDataEvent.data as any)
: undefined;
} else {
return undefined;
}
}
public async tokenDeposit(
group: Group,
mangoAccount: MangoAccount,
@ -820,7 +778,7 @@ export class MangoClient {
mangoAccount: MangoAccount,
mintPk: PublicKey,
nativeAmount: number,
) {
): Promise<TransactionSignature> {
const bank = group.getFirstBankByMint(mintPk);
const tokenAccountPk = await getAssociatedTokenAddress(
@ -1148,19 +1106,19 @@ export class MangoClient {
orderType: Serum3OrderType,
clientOrderId: number,
limit: number,
) {
): Promise<TransactionSignature> {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
if (!mangoAccount.findSerum3Account(serum3Market.marketIndex)) {
if (!mangoAccount.getSerum3Account(serum3Market.marketIndex)) {
await this.serum3CreateOpenOrders(
group,
mangoAccount,
serum3Market.serumMarketExternal,
);
await mangoAccount.reload(this, group);
await mangoAccount.reload(this);
}
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
const serum3MarketExternal = group.serum3ExternalMarketsMap.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternalVaultSigner =
@ -1182,13 +1140,17 @@ export class MangoClient {
const limitPrice = serum3MarketExternal.priceNumberToLots(price);
const maxBaseQuantity = serum3MarketExternal.baseSizeNumberToLots(size);
const maxQuoteQuantity = serum3MarketExternal.decoded.quoteLotSize
.mul(new BN(1 + group.getFeeRate(orderType === Serum3OrderType.postOnly)))
.mul(
new BN(
1 + group.getSerum3FeeRates(orderType === Serum3OrderType.postOnly),
),
)
.mul(
serum3MarketExternal
.baseSizeNumberToLots(size)
.mul(serum3MarketExternal.priceNumberToLots(price)),
);
const payerTokenIndex = (() => {
const payerTokenIndex = ((): TokenIndex => {
if (side == Serum3Side.bid) {
return serum3Market.quoteTokenIndex;
} else {
@ -1211,7 +1173,7 @@ export class MangoClient {
group: group.publicKey,
account: mangoAccount.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
openOrders: mangoAccount.findSerum3Account(serum3Market.marketIndex)
openOrders: mangoAccount.getSerum3Account(serum3Market.marketIndex)
?.openOrders,
serumMarket: serum3Market.publicKey,
serumProgram: SERUM3_PROGRAM_ID[this.cluster],
@ -1249,12 +1211,12 @@ export class MangoClient {
mangoAccount: MangoAccount,
externalMarketPk: PublicKey,
limit: number,
) {
): Promise<TransactionSignature> {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
const serum3MarketExternal = group.serum3ExternalMarketsMap.get(
externalMarketPk.toBase58(),
)!;
@ -1264,7 +1226,7 @@ export class MangoClient {
group: group.publicKey,
account: mangoAccount.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
openOrders: mangoAccount.findSerum3Account(serum3Market.marketIndex)
openOrders: mangoAccount.getSerum3Account(serum3Market.marketIndex)
?.openOrders,
serumMarket: serum3Market.publicKey,
serumProgram: SERUM3_PROGRAM_ID[this.cluster],
@ -1293,7 +1255,7 @@ export class MangoClient {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
const serum3MarketExternal = group.serum3ExternalMarketsMap.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternalVaultSigner =
@ -1309,7 +1271,7 @@ export class MangoClient {
group: group.publicKey,
account: mangoAccount.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
openOrders: mangoAccount.findSerum3Account(serum3Market.marketIndex)
openOrders: mangoAccount.getSerum3Account(serum3Market.marketIndex)
?.openOrders,
serumMarket: serum3Market.publicKey,
serumProgram: SERUM3_PROGRAM_ID[this.cluster],
@ -1349,7 +1311,7 @@ export class MangoClient {
externalMarketPk.toBase58(),
)!;
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
const serum3MarketExternal = group.serum3ExternalMarketsMap.get(
externalMarketPk.toBase58(),
)!;
@ -1358,7 +1320,7 @@ export class MangoClient {
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
openOrders: mangoAccount.findSerum3Account(serum3Market.marketIndex)
openOrders: mangoAccount.getSerum3Account(serum3Market.marketIndex)
?.openOrders,
serumMarket: serum3Market.publicKey,
serumProgram: SERUM3_PROGRAM_ID[this.cluster],
@ -1495,7 +1457,7 @@ export class MangoClient {
async perpEditMarket(
group: Group,
perpMarketName: string,
perpMarketIndex: PerpMarketIndex,
oracle: PublicKey,
oracleConfFilter: number,
baseDecimals: number,
@ -1516,7 +1478,7 @@ export class MangoClient {
settleFeeAmountThreshold: number,
settleFeeFractionLowHealth: number,
): Promise<TransactionSignature> {
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
return await this.program.methods
.perpEditMarket(
@ -1554,9 +1516,9 @@ export class MangoClient {
async perpCloseMarket(
group: Group,
perpMarketName: string,
perpMarketIndex: PerpMarketIndex,
): Promise<TransactionSignature> {
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
return await this.program.methods
.perpCloseMarket()
@ -1594,9 +1556,9 @@ export class MangoClient {
async perpDeactivatePosition(
group: Group,
mangoAccount: MangoAccount,
perpMarketName: string,
perpMarketIndex: PerpMarketIndex,
): Promise<TransactionSignature> {
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
@ -1625,7 +1587,7 @@ export class MangoClient {
async perpPlaceOrder(
group: Group,
mangoAccount: MangoAccount,
perpMarketName: string,
perpMarketIndex: PerpMarketIndex,
side: PerpOrderSide,
price: number,
quantity: number,
@ -1635,14 +1597,14 @@ export class MangoClient {
expiryTimestamp: number,
limit: number,
): Promise<TransactionSignature> {
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group,
[mangoAccount],
// Settlement token bank, because a position for it may be created
[group.getFirstBankByTokenIndex(0)],
[group.getFirstBankByTokenIndex(0 as TokenIndex)],
[perpMarket],
);
const ix = await this.program.methods
@ -1689,10 +1651,10 @@ export class MangoClient {
async perpCancelAllOrders(
group: Group,
mangoAccount: MangoAccount,
perpMarketName: string,
perpMarketIndex: PerpMarketIndex,
limit: number,
): Promise<TransactionSignature> {
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const ix = await this.program.methods
.perpCancelAllOrders(limit)
.accounts({
@ -1717,11 +1679,11 @@ export class MangoClient {
async perpConsumeEvents(
group: Group,
perpMarketName: string,
perpMarketIndex: PerpMarketIndex,
accounts: PublicKey[],
limit: number,
): Promise<TransactionSignature> {
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
return await this.program.methods
.perpConsumeEvents(new BN(limit))
.accounts({
@ -1740,10 +1702,10 @@ export class MangoClient {
async perpConsumeAllEvents(
group: Group,
perpMarketName: string,
perpMarketIndex: PerpMarketIndex,
): Promise<void> {
const limit = 8;
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const eventQueue = await perpMarket.loadEventQueue(this);
const unconsumedEvents = eventQueue.getUnconsumedEvents();
while (unconsumedEvents.length > 0) {
@ -1762,12 +1724,12 @@ export class MangoClient {
case PerpEventQueue.LIQUIDATE_EVENT_TYPE:
return [];
default:
throw new Error(`Unknown event with eventType ${ev.eventType}`);
throw new Error(`Unknown event with eventType ${ev.eventType}!`);
}
})
.flat();
await this.perpConsumeEvents(group, perpMarketName, accounts, limit);
await this.perpConsumeEvents(group, perpMarketIndex, accounts, limit);
}
}
@ -1793,8 +1755,6 @@ export class MangoClient {
const inputBank: Bank = group.getFirstBankByMint(inputMintPk);
const outputBank: Bank = group.getFirstBankByMint(outputMintPk);
if (!inputBank || !outputBank) throw new Error('Invalid token');
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
@ -1944,12 +1904,15 @@ export class MangoClient {
);
}
async updateIndexAndRate(group: Group, mintPk: PublicKey) {
async updateIndexAndRate(
group: Group,
mintPk: PublicKey,
): Promise<TransactionSignature> {
// TODO: handle updating multiple banks
const bank = group.getFirstBankByMint(mintPk);
const mintInfo = group.mintInfosMapByMint.get(mintPk.toString())!;
await this.program.methods
return await this.program.methods
.tokenUpdateIndexAndRate()
.accounts({
group: group.publicKey,
@ -1976,7 +1939,7 @@ export class MangoClient {
assetMintPk: PublicKey,
liabMintPk: PublicKey,
maxLiabTransfer: number,
) {
): Promise<TransactionSignature> {
const assetBank: Bank = group.getFirstBankByMint(assetMintPk);
const liabBank: Bank = group.getFirstBankByMint(liabMintPk);
@ -2024,7 +1987,11 @@ export class MangoClient {
);
}
async altSet(group: Group, addressLookupTable: PublicKey, index: number) {
async altSet(
group: Group,
addressLookupTable: PublicKey,
index: number,
): Promise<TransactionSignature> {
const ix = await this.program.methods
.altSet(index)
.accounts({
@ -2049,7 +2016,7 @@ export class MangoClient {
addressLookupTable: PublicKey,
index: number,
pks: PublicKey[],
) {
): Promise<TransactionSignature> {
return await this.program.methods
.altExtend(index, pks)
.accounts({
@ -2070,9 +2037,6 @@ export class MangoClient {
opts: any = {},
getIdsFromApi: IdsSource = 'api',
): MangoClient {
// TODO: use IDL on chain or in repository? decide...
// Alternatively we could fetch IDL from chain.
// const idl = await Program.fetchIdl(MANGO_V4_ID, provider);
const idl = IDL;
return new MangoClient(
@ -2108,8 +2072,7 @@ export class MangoClient {
/// private
// todo make private
public buildHealthRemainingAccounts(
private buildHealthRemainingAccounts(
retriever: AccountRetriever,
group: Group,
mangoAccounts: MangoAccount[],
@ -2133,8 +2096,7 @@ export class MangoClient {
}
}
// todo make private
public buildFixedAccountRetrieverHealthAccounts(
private buildFixedAccountRetrieverHealthAccounts(
group: Group,
mangoAccount: MangoAccount,
// Banks and perpMarkets for whom positions don't exist on mango account,
@ -2207,8 +2169,7 @@ export class MangoClient {
return healthRemainingAccounts;
}
// todo make private
public buildScanningAccountRetrieverHealthAccounts(
private buildScanningAccountRetrieverHealthAccounts(
group: Group,
mangoAccounts: MangoAccount[],
banks: Bank[],
@ -2216,7 +2177,7 @@ export class MangoClient {
): PublicKey[] {
const healthRemainingAccounts: PublicKey[] = [];
let tokenIndices: number[] = [];
let tokenIndices: TokenIndex[] = [];
for (const mangoAccount of mangoAccounts) {
tokenIndices.push(
...mangoAccount.tokens
@ -2241,7 +2202,7 @@ export class MangoClient {
...mintInfos.map((mintInfo) => mintInfo.oracle),
);
const perpIndices: number[] = [];
const perpIndices: PerpMarketIndex[] = [];
for (const mangoAccount of mangoAccounts) {
perpIndices.push(
...mangoAccount.perps

View File

@ -2,6 +2,7 @@ import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Cluster, Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { Group } from '../accounts/group';
import { HealthCache } from '../accounts/healthCache';
import { HealthType, MangoAccount } from '../accounts/mangoAccount';
import { PerpMarket } from '../accounts/perp';
import { Serum3Market } from '../accounts/serum3';
@ -26,7 +27,7 @@ async function debugUser(
) {
console.log(mangoAccount.toString(group));
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
console.log(
'buildFixedAccountRetrieverHealthAccounts ' +
@ -45,42 +46,52 @@ async function debugUser(
);
console.log(
'mangoAccount.getEquity() ' +
toUiDecimalsForQuote(mangoAccount.getEquity()!.toNumber()),
toUiDecimalsForQuote(mangoAccount.getEquity(group)!.toNumber()),
);
console.log(
'mangoAccount.getHealth(HealthType.init) ' +
toUiDecimalsForQuote(mangoAccount.getHealth(HealthType.init)!.toNumber()),
toUiDecimalsForQuote(
mangoAccount.getHealth(group, HealthType.init)!.toNumber(),
),
);
console.log(
'HealthCache.fromMangoAccount(group,mangoAccount).health(HealthType.init) ' +
toUiDecimalsForQuote(
HealthCache.fromMangoAccount(group, mangoAccount)
.health(HealthType.init)
.toNumber(),
),
);
console.log(
'mangoAccount.getHealthRatio(HealthType.init) ' +
mangoAccount.getHealthRatio(HealthType.init)!.toNumber(),
mangoAccount.getHealthRatio(group, HealthType.init)!.toNumber(),
);
console.log(
'mangoAccount.getHealthRatioUi(HealthType.init) ' +
mangoAccount.getHealthRatioUi(HealthType.init),
mangoAccount.getHealthRatioUi(group, HealthType.init),
);
console.log(
'mangoAccount.getHealthRatio(HealthType.maint) ' +
mangoAccount.getHealthRatio(HealthType.maint)!.toNumber(),
mangoAccount.getHealthRatio(group, HealthType.maint)!.toNumber(),
);
console.log(
'mangoAccount.getHealthRatioUi(HealthType.maint) ' +
mangoAccount.getHealthRatioUi(HealthType.maint),
mangoAccount.getHealthRatioUi(group, HealthType.maint),
);
console.log(
'mangoAccount.getCollateralValue() ' +
toUiDecimalsForQuote(mangoAccount.getCollateralValue()!.toNumber()),
toUiDecimalsForQuote(mangoAccount.getCollateralValue(group)!.toNumber()),
);
console.log(
'mangoAccount.getAssetsValue() ' +
toUiDecimalsForQuote(
mangoAccount.getAssetsValue(HealthType.init)!.toNumber(),
mangoAccount.getAssetsValue(group, HealthType.init)!.toNumber(),
),
);
console.log(
'mangoAccount.getLiabsValue() ' +
toUiDecimalsForQuote(
mangoAccount.getLiabsValue(HealthType.init)!.toNumber(),
mangoAccount.getLiabsValue(group, HealthType.init)!.toNumber(),
),
);
@ -223,9 +234,9 @@ async function main() {
for (const mangoAccount of mangoAccounts) {
console.log(`MangoAccount ${mangoAccount.publicKey}`);
// if (mangoAccount.name === 'PnL Test') {
await debugUser(client, group, mangoAccount);
// }
if (mangoAccount.name === 'PnL Test') {
await debugUser(client, group, mangoAccount);
}
}
}

View File

@ -51,7 +51,7 @@ export class Id {
static fromIdsByName(name: string): Id {
const groupConfig = ids.groups.find((id) => id['name'] === name);
if (!groupConfig) throw new Error(`Unable to find group config ${name}`);
if (!groupConfig) throw new Error(`No group config ${name} found in Ids!`);
return new Id(
groupConfig.cluster as Cluster,
groupConfig.name,
@ -71,7 +71,7 @@ export class Id {
(id) => id['publicKey'] === groupPk.toString(),
);
if (!groupConfig)
throw new Error(`Unable to find group config ${groupPk.toString()}`);
throw new Error(`No group config ${groupPk.toString()} found in Ids!`);
return new Id(
groupConfig.cluster as Cluster,
groupConfig.name,

View File

@ -567,7 +567,6 @@ async function main() {
// );
// console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// TODO decide on what keys should go in
console.log(`ALT: extending manually with bank publick keys and oracles`);
const extendIx = AddressLookupTableProgram.extendLookupTable({
lookupTable: createIx[1],

View File

@ -78,10 +78,10 @@ async function main() {
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString(group));
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
// set delegate, and change name
if (false) {
if (true) {
console.log(`...changing mango account name, and setting a delegate`);
const randomKey = new PublicKey(
'4ZkS7ZZkxfsC3GtvvsHP3DFcUeByU9zzZELS4r8HCELo',
@ -93,7 +93,7 @@ async function main() {
'my_changed_name',
randomKey,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
console.log(mangoAccount.toString());
console.log(`...resetting mango account name, and re-setting a delegate`);
@ -103,7 +103,7 @@ async function main() {
'my_mango_account',
PublicKey.default,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
console.log(mangoAccount.toString());
}
@ -113,7 +113,7 @@ async function main() {
`...expanding mango account to have serum3 and perp position slots`,
);
await client.expandMangoAccount(group, mangoAccount, 8, 8, 8, 8);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
}
// deposit and withdraw
@ -126,7 +126,7 @@ async function main() {
new PublicKey(DEVNET_MINTS.get('USDC')!),
50,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
await client.tokenDeposit(
group,
@ -134,7 +134,7 @@ async function main() {
new PublicKey(DEVNET_MINTS.get('SOL')!),
1,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
await client.tokenDeposit(
group,
@ -142,7 +142,7 @@ async function main() {
new PublicKey(DEVNET_MINTS.get('MNGO')!),
1,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
console.log(`...withdrawing 1 USDC`);
await client.tokenWithdraw(
@ -152,7 +152,7 @@ async function main() {
1,
true,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
console.log(`...depositing 0.0005 BTC`);
await client.tokenDeposit(
@ -161,7 +161,7 @@ async function main() {
new PublicKey(DEVNET_MINTS.get('BTC')!),
0.0005,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
console.log(mangoAccount.toString(group));
} catch (error) {
@ -171,12 +171,6 @@ async function main() {
if (true) {
// serum3
const serum3Market = group.serum3MarketsMapByExternal.get(
DEVNET_SERUM3_MARKETS.get('BTC/USDC')?.toBase58()!,
);
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
DEVNET_SERUM3_MARKETS.get('BTC/USDC')?.toBase58()!,
);
const asks = await group.loadSerum3AsksForMarket(
client,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
@ -205,7 +199,7 @@ async function main() {
Date.now(),
10,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
price = lowestAsk.price + lowestAsk.price / 2;
qty = 0.0001;
@ -224,7 +218,7 @@ async function main() {
Date.now(),
10,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
price = highestBid.price - highestBid.price / 2;
qty = 0.0001;
@ -291,33 +285,27 @@ async function main() {
}
if (true) {
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
console.log(
'...mangoAccount.getEquity() ' +
toUiDecimalsForQuote(mangoAccount.getEquity()!.toNumber()),
toUiDecimalsForQuote(mangoAccount.getEquity(group)!.toNumber()),
);
console.log(
'...mangoAccount.getCollateralValue() ' +
toUiDecimalsForQuote(mangoAccount.getCollateralValue()!.toNumber()),
);
console.log(
'...mangoAccount.accountData["healthCache"].health(HealthType.init) ' +
toUiDecimalsForQuote(
mangoAccount
.accountData!['healthCache'].health(HealthType.init)
.toNumber(),
mangoAccount.getCollateralValue(group)!.toNumber(),
),
);
console.log(
'...mangoAccount.getAssetsVal() ' +
toUiDecimalsForQuote(
mangoAccount.getAssetsValue(HealthType.init)!.toNumber(),
mangoAccount.getAssetsValue(group, HealthType.init)!.toNumber(),
),
);
console.log(
'...mangoAccount.getLiabsVal() ' +
toUiDecimalsForQuote(
mangoAccount.getLiabsValue(HealthType.init)!.toNumber(),
mangoAccount.getLiabsValue(group, HealthType.init)!.toNumber(),
),
);
console.log(
@ -400,10 +388,11 @@ async function main() {
// perps
if (true) {
let sig;
const perpMarket = group.getPerpMarketByName('BTC-PERP');
const orders = await mangoAccount.loadPerpOpenOrdersForMarket(
client,
group,
'BTC-PERP',
perpMarket.perpMarketIndex,
);
for (const order of orders) {
console.log(
@ -411,7 +400,12 @@ async function main() {
);
}
console.log(`...cancelling all perp orders`);
sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
sig = await client.perpCancelAllOrders(
group,
mangoAccount,
perpMarket.perpMarketIndex,
10,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// scenario 1
@ -423,7 +417,7 @@ async function main() {
Math.floor(Math.random() * 100);
const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi(
group,
'BTC-PERP',
perpMarket.perpMarketIndex,
1,
);
const baseQty = quoteQty / price;
@ -433,7 +427,7 @@ async function main() {
const sig = await client.perpPlaceOrder(
group,
mangoAccount,
'BTC-PERP',
perpMarket.perpMarketIndex,
PerpOrderSide.bid,
price,
baseQty,
@ -448,7 +442,12 @@ async function main() {
console.log(error);
}
console.log(`...cancelling all perp orders`);
sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
sig = await client.perpCancelAllOrders(
group,
mangoAccount,
perpMarket.perpMarketIndex,
10,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// bid max perp + some
@ -458,7 +457,11 @@ async function main() {
group.banksMapByName.get('BTC')![0].uiPrice! -
Math.floor(Math.random() * 100);
const quoteQty =
mangoAccount.getMaxQuoteForPerpBidUi(group, 'BTC-PERP', 1) * 1.02;
mangoAccount.getMaxQuoteForPerpBidUi(
group,
perpMarket.perpMarketIndex,
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}`,
@ -466,7 +469,7 @@ async function main() {
const sig = await client.perpPlaceOrder(
group,
mangoAccount,
'BTC-PERP',
perpMarket.perpMarketIndex,
PerpOrderSide.bid,
price,
baseQty,
@ -487,7 +490,11 @@ async function main() {
const price =
group.banksMapByName.get('BTC')![0].uiPrice! +
Math.floor(Math.random() * 100);
const baseQty = mangoAccount.getMaxBaseForPerpAskUi(group, 'BTC-PERP', 1);
const baseQty = mangoAccount.getMaxBaseForPerpAskUi(
group,
perpMarket.perpMarketIndex,
1,
);
const quoteQty = baseQty * price;
console.log(
`...placing max qty perp ask clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
@ -495,7 +502,7 @@ async function main() {
const sig = await client.perpPlaceOrder(
group,
mangoAccount,
'BTC-PERP',
perpMarket.perpMarketIndex,
PerpOrderSide.ask,
price,
baseQty,
@ -517,7 +524,11 @@ async function main() {
group.banksMapByName.get('BTC')![0].uiPrice! +
Math.floor(Math.random() * 100);
const baseQty =
mangoAccount.getMaxBaseForPerpAskUi(group, 'BTC-PERP', 1) * 1.02;
mangoAccount.getMaxBaseForPerpAskUi(
group,
perpMarket.perpMarketIndex,
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}`,
@ -525,7 +536,7 @@ async function main() {
const sig = await client.perpPlaceOrder(
group,
mangoAccount,
'BTC-PERP',
perpMarket.perpMarketIndex,
PerpOrderSide.ask,
price,
baseQty,
@ -541,7 +552,12 @@ async function main() {
}
console.log(`...cancelling all perp orders`);
sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
sig = await client.perpCancelAllOrders(
group,
mangoAccount,
perpMarket.perpMarketIndex,
10,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// // scenario 2
@ -553,8 +569,8 @@ async function main() {
// const sig = await client.perpPlaceOrder(
// group,
// mangoAccount,
// 'BTC-PERP',
// PerpOrderSide.bid,
// perpMarket.perpMarketIndex,
// PerpOrderSide.bid,
// price,
// 0.01,
// price * 0.01,
@ -574,8 +590,8 @@ async function main() {
// const sig = await client.perpPlaceOrder(
// group,
// mangoAccount,
// 'BTC-PERP',
// PerpOrderSide.ask,
// perpMarket.perpMarketIndex,
// PerpOrderSide.ask,
// price,
// 0.01,
// price * 0.011,
@ -590,11 +606,9 @@ async function main() {
// }
// // // should be able to cancel them : know bug
// // console.log(`...cancelling all perp orders`);
// // sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
// // sig = await client.perpCancelAllOrders(group, mangoAccount, perpMarket.perpMarketIndex, 10);
// // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
const perpMarket = group.perpMarketsMap.get('BTC-PERP')!;
const bids: BookSide = await perpMarket?.loadBids(client)!;
console.log(`bids - ${Array.from(bids.items())}`);
const asks: BookSide = await perpMarket?.loadAsks(client)!;
@ -615,7 +629,7 @@ async function main() {
// make+take orders should have cancelled each other, and if keeper has already cranked, then should not appear in position
await group.reloadAll(client);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
console.log(`${mangoAccount.toString(group)}`);
}

View File

@ -23,7 +23,13 @@ import { PerpMarket } from './accounts/perp';
export const U64_MAX_BN = new BN('18446744073709551615');
export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64);
export function debugAccountMetas(ams: AccountMeta[]) {
// https://stackoverflow.com/questions/70261755/user-defined-type-guard-function-and-type-narrowing-to-more-specific-type/70262876#70262876
export declare abstract class As<Tag extends keyof never> {
private static readonly $as$: unique symbol;
private [As.$as$]: Record<Tag, true>;
}
export function debugAccountMetas(ams: AccountMeta[]): void {
for (const am of ams) {
console.log(
`${am.pubkey.toBase58()}, isSigner: ${am.isSigner
@ -39,7 +45,7 @@ export function debugHealthAccounts(
group: Group,
mangoAccount: MangoAccount,
publicKeys: PublicKey[],
) {
): void {
const banks = new Map(
Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [
banks[0].publicKey.toBase58(),
@ -66,10 +72,12 @@ export function debugHealthAccounts(
}),
);
const perps = new Map(
Array.from(group.perpMarketsMap.values()).map((perpMarket: PerpMarket) => [
perpMarket.publicKey.toBase58(),
`${perpMarket.name} perp market`,
]),
Array.from(group.perpMarketsMapByName.values()).map(
(perpMarket: PerpMarket) => [
perpMarket.publicKey.toBase58(),
`${perpMarket.name} perp market`,
],
),
);
publicKeys.map((pk) => {
@ -126,7 +134,7 @@ export async function getAssociatedTokenAddress(
associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID,
): Promise<PublicKey> {
if (!allowOwnerOffCurve && !PublicKey.isOnCurve(owner.toBuffer()))
throw new Error('TokenOwnerOffCurve');
throw new Error('TokenOwnerOffCurve!');
const [address] = await PublicKey.findProgramAddress(
[owner.toBuffer(), programId.toBuffer(), mint.toBuffer()],

View File

@ -1,42 +0,0 @@
import {
simulateTransaction,
SuccessfulTxSimulationResponse,
} from '@project-serum/anchor/dist/cjs/utils/rpc';
import {
Signer,
PublicKey,
Transaction,
Commitment,
SimulatedTransactionResponse,
} from '@solana/web3.js';
class SimulateError extends Error {
constructor(
readonly simulationResponse: SimulatedTransactionResponse,
message?: string,
) {
super(message);
}
}
export async function simulate(
tx: Transaction,
signers?: Signer[],
commitment?: Commitment,
includeAccounts?: boolean | PublicKey[],
): Promise<SuccessfulTxSimulationResponse> {
tx.feePayer = this.wallet.publicKey;
tx.recentBlockhash = (
await this.connection.getLatestBlockhash(
commitment ?? this.connection.commitment,
)
).blockhash;
const result = await simulateTransaction(this.connection, tx);
if (result.value.err) {
throw new SimulateError(result.value);
}
return result.value;
}

View File

@ -10,7 +10,7 @@ export async function sendTransaction(
ixs: TransactionInstruction[],
alts: AddressLookupTableAccount[],
opts: any = {},
) {
): Promise<string> {
const connection = provider.connection;
const latestBlockhash = await connection.getLatestBlockhash(
opts.preflightCommitment,