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, "ecmaVersion": 12,
"sourceType": "module" "sourceType": "module"
}, },
"plugins": ["@typescript-eslint"], "plugins": [
"@typescript-eslint"
],
"rules": { "rules": {
"linebreak-style": ["error", "unix"], "linebreak-style": [
"semi": ["error", "always"], "error",
"unix"
],
"semi": [
"error",
"always"
],
"@typescript-eslint/no-non-null-assertion": 0, "@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/ban-ts-comment": 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 { BN } from '@project-serum/anchor';
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes'; import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { PublicKey } from '@solana/web3.js'; import { PublicKey } from '@solana/web3.js';
import { nativeI80F48ToUi } from '../utils'; import { As, nativeI80F48ToUi } from '../utils';
import { I80F48, I80F48Dto, ZERO_I80F48 } from './I80F48'; import { I80F48, I80F48Dto, ZERO_I80F48 } from './I80F48';
export const QUOTE_DECIMALS = 6; export const QUOTE_DECIMALS = 6;
export type TokenIndex = number & As<'token-index'>;
export type OracleConfig = { export type OracleConfig = {
confFilter: I80F48Dto; confFilter: I80F48Dto;
}; };
export interface BankForHealth { export interface BankForHealth {
tokenIndex: number; tokenIndex: TokenIndex;
maintAssetWeight: I80F48; maintAssetWeight: I80F48;
initAssetWeight: I80F48; initAssetWeight: I80F48;
maintLiabWeight: I80F48; maintLiabWeight: I80F48;
@ -85,7 +87,7 @@ export class Bank implements BankForHealth {
mintDecimals: number; mintDecimals: number;
bankNum: number; bankNum: number;
}, },
) { ): Bank {
return new Bank( return new Bank(
publicKey, publicKey,
obj.name, obj.name,
@ -120,7 +122,7 @@ export class Bank implements BankForHealth {
obj.dust, obj.dust,
obj.flashLoanTokenAccountInitial, obj.flashLoanTokenAccountInitial,
obj.flashLoanApprovedAmount, obj.flashLoanApprovedAmount,
obj.tokenIndex, obj.tokenIndex as TokenIndex,
obj.mintDecimals, obj.mintDecimals,
obj.bankNum, obj.bankNum,
); );
@ -160,7 +162,7 @@ export class Bank implements BankForHealth {
dust: I80F48Dto, dust: I80F48Dto,
flashLoanTokenAccountInitial: BN, flashLoanTokenAccountInitial: BN,
flashLoanApprovedAmount: BN, flashLoanApprovedAmount: BN,
public tokenIndex: number, public tokenIndex: TokenIndex,
public mintDecimals: number, public mintDecimals: number,
public bankNum: number, public bankNum: number,
) { ) {
@ -207,9 +209,9 @@ export class Bank implements BankForHealth {
'\n oracle - ' + '\n oracle - ' +
this.oracle.toBase58() + this.oracle.toBase58() +
'\n price - ' + '\n price - ' +
this.price?.toNumber() + this._price?.toNumber() +
'\n uiPrice - ' + '\n uiPrice - ' +
this.uiPrice + this._uiPrice +
'\n deposit index - ' + '\n deposit index - ' +
this.depositIndex.toNumber() + this.depositIndex.toNumber() +
'\n borrow index - ' + '\n borrow index - ' +
@ -268,7 +270,7 @@ export class Bank implements BankForHealth {
get price(): I80F48 { get price(): I80F48 {
if (!this._price) { if (!this._price) {
throw new Error( 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; return this._price;
@ -277,7 +279,7 @@ export class Bank implements BankForHealth {
get uiPrice(): number { get uiPrice(): number {
if (!this._uiPrice) { if (!this._uiPrice) {
throw new Error( 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; return this._uiPrice;
@ -388,11 +390,11 @@ export class MintInfo {
registrationTime: BN; registrationTime: BN;
groupInsuranceFund: number; groupInsuranceFund: number;
}, },
) { ): MintInfo {
return new MintInfo( return new MintInfo(
publicKey, publicKey,
obj.group, obj.group,
obj.tokenIndex, obj.tokenIndex as TokenIndex,
obj.mint, obj.mint,
obj.banks, obj.banks,
obj.vaults, obj.vaults,
@ -405,7 +407,7 @@ export class MintInfo {
constructor( constructor(
public publicKey: PublicKey, public publicKey: PublicKey,
public group: PublicKey, public group: PublicKey,
public tokenIndex: number, public tokenIndex: TokenIndex,
public mint: PublicKey, public mint: PublicKey,
public banks: PublicKey[], public banks: PublicKey[],
public vaults: PublicKey[], public vaults: PublicKey[],

View File

@ -6,7 +6,7 @@ import {
Market, Market,
Orderbook, Orderbook,
} from '@project-serum/serum'; } from '@project-serum/serum';
import { parsePriceData, PriceData } from '@pythnetwork/client'; import { parsePriceData } from '@pythnetwork/client';
import { import {
AccountInfo, AccountInfo,
AddressLookupTableAccount, AddressLookupTableAccount,
@ -17,15 +17,15 @@ import { MangoClient } from '../client';
import { SERUM3_PROGRAM_ID } from '../constants'; import { SERUM3_PROGRAM_ID } from '../constants';
import { Id } from '../ids'; import { Id } from '../ids';
import { toNativeDecimals, toUiDecimals } from '../utils'; import { toNativeDecimals, toUiDecimals } from '../utils';
import { Bank, MintInfo } from './bank'; import { Bank, MintInfo, TokenIndex } from './bank';
import { I80F48, ONE_I80F48 } from './I80F48'; import { I80F48, ONE_I80F48 } from './I80F48';
import { import {
isPythOracle, isPythOracle,
isSwitchboardOracle, isSwitchboardOracle,
parseSwitchboardOracle, parseSwitchboardOracle,
} from './oracle'; } from './oracle';
import { BookSide, PerpMarket } from './perp'; import { BookSide, PerpMarket, PerpMarketIndex } from './perp';
import { Serum3Market } from './serum3'; import { MarketIndex, Serum3Market } from './serum3';
export class Group { export class Group {
static from( static from(
@ -57,12 +57,14 @@ export class Group {
new Map(), // banksMapByName new Map(), // banksMapByName
new Map(), // banksMapByMint new Map(), // banksMapByMint
new Map(), // banksMapByTokenIndex new Map(), // banksMapByTokenIndex
new Map(), // serum3MarketsMap new Map(), // serum3MarketsMapByExternal
new Map(), // serum3MarketsMapByMarketIndex
new Map(), // serum3MarketExternalsMap new Map(), // serum3MarketExternalsMap
new Map(), // perpMarketsMap new Map(), // perpMarketsMapByOracle
new Map(), // perpMarketsMapByMarketIndex
new Map(), // perpMarketsMapByName
new Map(), // mintInfosMapByTokenIndex new Map(), // mintInfosMapByTokenIndex
new Map(), // mintInfosMapByMint new Map(), // mintInfosMapByMint
new Map(), // oraclesMap
new Map(), // vaultAmountsMap new Map(), // vaultAmountsMap
); );
} }
@ -81,18 +83,19 @@ export class Group {
public addressLookupTablesList: AddressLookupTableAccount[], public addressLookupTablesList: AddressLookupTableAccount[],
public banksMapByName: Map<string, Bank[]>, public banksMapByName: Map<string, Bank[]>,
public banksMapByMint: Map<string, Bank[]>, public banksMapByMint: Map<string, Bank[]>,
public banksMapByTokenIndex: Map<number, Bank[]>, public banksMapByTokenIndex: Map<TokenIndex, Bank[]>,
public serum3MarketsMapByExternal: Map<string, Serum3Market>, public serum3MarketsMapByExternal: Map<string, Serum3Market>,
public serum3MarketExternalsMap: Map<string, Market>, public serum3MarketsMapByMarketIndex: Map<MarketIndex, Serum3Market>,
// TODO rethink key public serum3ExternalMarketsMap: Map<string, Market>,
public perpMarketsMap: Map<string, PerpMarket>, public perpMarketsMapByOracle: Map<string, PerpMarket>,
public mintInfosMapByTokenIndex: Map<number, MintInfo>, public perpMarketsMapByMarketIndex: Map<PerpMarketIndex, PerpMarket>,
public perpMarketsMapByName: Map<string, PerpMarket>,
public mintInfosMapByTokenIndex: Map<TokenIndex, MintInfo>,
public mintInfosMapByMint: Map<string, MintInfo>, public mintInfosMapByMint: Map<string, MintInfo>,
private oraclesMap: Map<string, PriceData>, // UNUSED
public vaultAmountsMap: Map<string, number>, public vaultAmountsMap: Map<string, number>,
) {} ) {}
public async reloadAll(client: MangoClient) { public async reloadAll(client: MangoClient): Promise<void> {
let ids: Id | undefined = undefined; let ids: Id | undefined = undefined;
if (client.idsSource === 'api') { if (client.idsSource === 'api') {
@ -109,12 +112,12 @@ export class Group {
this.reloadBanks(client, ids).then(() => this.reloadBanks(client, ids).then(() =>
Promise.all([ Promise.all([
this.reloadBankOraclePrices(client), this.reloadBankOraclePrices(client),
this.reloadVaults(client, ids), this.reloadVaults(client),
]), ]),
), ),
this.reloadMintInfos(client, ids), this.reloadMintInfos(client, ids),
this.reloadSerum3Markets(client, ids).then(() => this.reloadSerum3Markets(client, ids).then(() =>
this.reloadSerum3ExternalMarkets(client, ids), this.reloadSerum3ExternalMarkets(client),
), ),
this.reloadPerpMarkets(client, ids).then(() => this.reloadPerpMarkets(client, ids).then(() =>
this.reloadPerpMarketOraclePrices(client), this.reloadPerpMarketOraclePrices(client),
@ -123,7 +126,7 @@ export class Group {
// console.timeEnd('group.reload'); // console.timeEnd('group.reload');
} }
public async reloadAlts(client: MangoClient) { public async reloadAlts(client: MangoClient): Promise<void> {
const alts = await Promise.all( const alts = await Promise.all(
this.addressLookupTables this.addressLookupTables
.filter((alt) => !alt.equals(PublicKey.default)) .filter((alt) => !alt.equals(PublicKey.default))
@ -133,13 +136,13 @@ export class Group {
); );
this.addressLookupTablesList = alts.map((res, i) => { this.addressLookupTablesList = alts.map((res, i) => {
if (!res || !res.value) { 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; return res.value;
}); });
} }
public async reloadBanks(client: MangoClient, ids?: Id) { public async reloadBanks(client: MangoClient, ids?: Id): Promise<void> {
let banks: Bank[]; let banks: Bank[];
if (ids && ids.getBanks().length) { 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[]; let mintInfos: MintInfo[];
if (ids && ids.getMintInfos().length) { if (ids && ids.getMintInfos().length) {
mintInfos = ( 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[]; let serum3Markets: Serum3Market[];
if (ids && ids.getSerum3Markets().length) { if (ids && ids.getSerum3Markets().length) {
serum3Markets = ( serum3Markets = (
@ -214,9 +220,15 @@ export class Group {
serum3Market, 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( const externalMarkets = await Promise.all(
Array.from(this.serum3MarketsMapByExternal.values()).map((serum3Market) => Array.from(this.serum3MarketsMapByExternal.values()).map((serum3Market) =>
Market.load( Market.load(
@ -228,7 +240,7 @@ export class Group {
), ),
); );
this.serum3MarketExternalsMap = new Map( this.serum3ExternalMarketsMap = new Map(
Array.from(this.serum3MarketsMapByExternal.values()).map( Array.from(this.serum3MarketsMapByExternal.values()).map(
(serum3Market, index) => [ (serum3Market, index) => [
serum3Market.serumMarketExternal.toBase58(), 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[]; let perpMarkets: PerpMarket[];
if (ids && ids.getPerpMarkets().length) { if (ids && ids.getPerpMarkets().length) {
perpMarkets = ( perpMarkets = (
@ -252,9 +264,18 @@ export class Group {
perpMarkets = await client.perpGetMarkets(this); perpMarkets = await client.perpGetMarkets(this);
} }
this.perpMarketsMap = new Map( this.perpMarketsMapByName = new Map(
perpMarkets.map((perpMarket) => [perpMarket.name, perpMarket]), 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> { public async reloadBankOraclePrices(client: MangoClient): Promise<void> {
@ -293,7 +314,9 @@ export class Group {
public async reloadPerpMarketOraclePrices( public async reloadPerpMarketOraclePrices(
client: MangoClient, client: MangoClient,
): Promise<void> { ): 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 oracles = perpMarkets.map((b) => b.oracle);
const ais = const ais =
await client.program.provider.connection.getMultipleAccountsInfo(oracles); await client.program.provider.connection.getMultipleAccountsInfo(oracles);
@ -302,15 +325,17 @@ export class Group {
ais.forEach(async (ai, i) => { ais.forEach(async (ai, i) => {
const perpMarket = perpMarkets[i]; const perpMarket = perpMarkets[i];
if (!ai) 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( const { price, uiPrice } = await this.decodePriceFromOracleAi(
coder, coder,
perpMarket.oracle, perpMarket.oracle,
ai, ai,
perpMarket.baseDecimals, perpMarket.baseDecimals,
); );
perpMarket.price = price; perpMarket._price = price;
perpMarket.uiPrice = uiPrice; perpMarket._uiPrice = uiPrice;
}); });
} }
@ -319,7 +344,7 @@ export class Group {
oracle: PublicKey, oracle: PublicKey,
ai: AccountInfo<Buffer>, ai: AccountInfo<Buffer>,
baseDecimals: number, baseDecimals: number,
) { ): Promise<{ price: I80F48; uiPrice: number }> {
let price, uiPrice; let price, uiPrice;
if ( if (
!BorshAccountsCoder.accountDiscriminator('stubOracle').compare( !BorshAccountsCoder.accountDiscriminator('stubOracle').compare(
@ -337,13 +362,13 @@ export class Group {
price = this?.toNativePrice(uiPrice, baseDecimals); price = this?.toNativePrice(uiPrice, baseDecimals);
} else { } else {
throw new Error( throw new Error(
`Unknown oracle provider for oracle ${oracle}, with owner ${ai.owner}`, `Unknown oracle provider (parsing not implemented) for oracle ${oracle}, with owner ${ai.owner}!`,
); );
} }
return { price, uiPrice }; 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()) const vaultPks = Array.from(this.banksMapByMint.values())
.flat() .flat()
.map((bank) => bank.vault); .map((bank) => bank.vault);
@ -354,7 +379,9 @@ export class Group {
this.vaultAmountsMap = new Map( this.vaultAmountsMap = new Map(
vaultAccounts.map((vaultAi, i) => { 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() const vaultAmount = coder()
.accounts.decode('token', vaultAi.data) .accounts.decode('token', vaultAi.data)
.amount.toNumber(); .amount.toNumber();
@ -365,8 +392,7 @@ export class Group {
public getMintDecimals(mintPk: PublicKey): number { public getMintDecimals(mintPk: PublicKey): number {
const banks = this.banksMapByMint.get(mintPk.toString()); const banks = this.banksMapByMint.get(mintPk.toString());
if (!banks) if (!banks) throw new Error(`No bank found for mint ${mintPk}!`);
throw new Error(`Unable to find mint decimals for ${mintPk.toString()}`);
return banks[0].mintDecimals; return banks[0].mintDecimals;
} }
@ -376,14 +402,13 @@ export class Group {
public getFirstBankByMint(mintPk: PublicKey): Bank { public getFirstBankByMint(mintPk: PublicKey): Bank {
const banks = this.banksMapByMint.get(mintPk.toString()); const banks = this.banksMapByMint.get(mintPk.toString());
if (!banks) throw new Error(`Unable to find bank for ${mintPk.toString()}`); if (!banks) throw new Error(`No bank found for mint ${mintPk}!`);
return banks[0]; return banks[0];
} }
public getFirstBankByTokenIndex(tokenIndex: number): Bank { public getFirstBankByTokenIndex(tokenIndex: TokenIndex): Bank {
const banks = this.banksMapByTokenIndex.get(tokenIndex); const banks = this.banksMapByTokenIndex.get(tokenIndex);
if (!banks) if (!banks) throw new Error(`No bank found for tokenIndex ${tokenIndex}!`);
throw new Error(`Unable to find banks for tokenIndex ${tokenIndex}`);
return banks[0]; return banks[0];
} }
@ -394,10 +419,7 @@ export class Group {
*/ */
public getTokenVaultBalanceByMint(mintPk: PublicKey): I80F48 { public getTokenVaultBalanceByMint(mintPk: PublicKey): I80F48 {
const banks = this.banksMapByMint.get(mintPk.toBase58()); const banks = this.banksMapByMint.get(mintPk.toBase58());
if (!banks) if (!banks) throw new Error(`No bank found for mint ${mintPk}!`);
throw new Error(
`Mint does not exist in getTokenVaultBalanceByMint ${mintPk.toString()}`,
);
let totalAmount = 0; let totalAmount = 0;
for (const bank of banks) { for (const bank of banks) {
const amount = this.vaultAmountsMap.get(bank.vault.toBase58()); const amount = this.vaultAmountsMap.get(bank.vault.toBase58());
@ -408,83 +430,6 @@ export class Group {
return I80F48.fromNumber(totalAmount); 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 * @param mintPk
@ -497,7 +442,131 @@ export class Group {
return toUiDecimals(vaultBalance, mintDecimals); 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 mintBanks of this.banksMapByMint.values()) {
for (const bank of mintBanks) { for (const bank of mintBanks) {
console.log(bank.toString()); 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 { expect } from 'chai';
import { toUiDecimalsForQuote } from '../utils'; import { toUiDecimalsForQuote } from '../utils';
import { BankForHealth } from './bank'; import { BankForHealth, TokenIndex } from './bank';
import { HealthCache, TokenInfo } from './healthCache'; import { HealthCache, PerpInfo, Serum3Info, TokenInfo } from './healthCache';
import { I80F48, ZERO_I80F48 } from './I80F48'; 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', () => { 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', () => { it('max swap tokens for min ratio', () => {
// USDC like // USDC like
const sourceBank: BankForHealth = { const sourceBank: BankForHealth = {
tokenIndex: 0, tokenIndex: 0 as TokenIndex,
maintAssetWeight: I80F48.fromNumber(1), maintAssetWeight: I80F48.fromNumber(1),
initAssetWeight: I80F48.fromNumber(1), initAssetWeight: I80F48.fromNumber(1),
maintLiabWeight: I80F48.fromNumber(1), maintLiabWeight: I80F48.fromNumber(1),
@ -17,7 +374,7 @@ describe('Health Cache', () => {
}; };
// BTC like // BTC like
const targetBank: BankForHealth = { const targetBank: BankForHealth = {
tokenIndex: 1, tokenIndex: 1 as TokenIndex,
maintAssetWeight: I80F48.fromNumber(0.9), maintAssetWeight: I80F48.fromNumber(0.9),
initAssetWeight: I80F48.fromNumber(0.8), initAssetWeight: I80F48.fromNumber(0.8),
maintLiabWeight: I80F48.fromNumber(1.1), maintLiabWeight: I80F48.fromNumber(1.1),
@ -28,7 +385,7 @@ describe('Health Cache', () => {
const hc = new HealthCache( const hc = new HealthCache(
[ [
new TokenInfo( new TokenInfo(
0, 0 as TokenIndex,
sourceBank.maintAssetWeight, sourceBank.maintAssetWeight,
sourceBank.initAssetWeight, sourceBank.initAssetWeight,
sourceBank.maintLiabWeight, sourceBank.maintLiabWeight,
@ -39,7 +396,7 @@ describe('Health Cache', () => {
), ),
new TokenInfo( new TokenInfo(
1, 1 as TokenIndex,
targetBank.maintAssetWeight, targetBank.maintAssetWeight,
targetBank.initAssetWeight, targetBank.initAssetWeight,
targetBank.maintLiabWeight, 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 { PublicKey } from '@solana/web3.js';
import _ from 'lodash'; import _ from 'lodash';
import { Bank, BankForHealth } from './bank'; import { Bank, BankForHealth, TokenIndex } from './bank';
import { Group } from './group'; import { Group } from './group';
import { import {
HUNDRED_I80F48, HUNDRED_I80F48,
I80F48, I80F48,
I80F48Dto, I80F48Dto,
MAX_I80F48, MAX_I80F48,
ONE_I80F48,
ZERO_I80F48, ZERO_I80F48,
} from './I80F48'; } from './I80F48';
import { HealthType } from './mangoAccount';
import { HealthType, MangoAccount, PerpPosition } from './mangoAccount';
import { PerpMarket, PerpOrderSide } from './perp'; 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[], 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( return new HealthCache(
dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)), dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)),
dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)), dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)),
@ -57,6 +115,7 @@ export class HealthCache {
const health = ZERO_I80F48(); const health = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) { for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType); const contrib = tokenInfo.healthContribution(healthType);
// console.log(` - ti ${contrib}`);
health.iadd(contrib); health.iadd(contrib);
} }
for (const serum3Info of this.serum3Infos) { for (const serum3Info of this.serum3Infos) {
@ -64,10 +123,12 @@ export class HealthCache {
healthType, healthType,
this.tokenInfos, this.tokenInfos,
); );
// console.log(` - si ${contrib}`);
health.iadd(contrib); health.iadd(contrib);
} }
for (const perpInfo of this.perpInfos) { for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType); const contrib = perpInfo.healthContribution(healthType);
// console.log(` - pi ${contrib}`);
health.iadd(contrib); health.iadd(contrib);
} }
return health; return health;
@ -164,34 +225,32 @@ export class HealthCache {
} }
} }
findTokenInfoIndex(tokenIndex: number): number { findTokenInfoIndex(tokenIndex: TokenIndex): number {
return this.tokenInfos.findIndex( return this.tokenInfos.findIndex(
(tokenInfo) => tokenInfo.tokenIndex == tokenIndex, (tokenInfo) => tokenInfo.tokenIndex === tokenIndex,
); );
} }
getOrCreateTokenInfoIndex(bank: BankForHealth): number { getOrCreateTokenInfoIndex(bank: BankForHealth): number {
const index = this.findTokenInfoIndex(bank.tokenIndex); const index = this.findTokenInfoIndex(bank.tokenIndex);
if (index == -1) { if (index == -1) {
this.tokenInfos.push(TokenInfo.emptyFromBank(bank)); this.tokenInfos.push(TokenInfo.fromBank(bank));
} }
return this.findTokenInfoIndex(bank.tokenIndex); return this.findTokenInfoIndex(bank.tokenIndex);
} }
findSerum3InfoIndex(marketIndex: number): number { findSerum3InfoIndex(marketIndex: MarketIndex): number {
return this.serum3Infos.findIndex( return this.serum3Infos.findIndex(
(serum3Info) => serum3Info.marketIndex === marketIndex, (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 index = this.findSerum3InfoIndex(serum3Market.marketIndex);
const baseBank = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
);
const quoteBank = group.getFirstBankByTokenIndex(
serum3Market.quoteTokenIndex,
);
const baseEntryIndex = this.getOrCreateTokenInfoIndex(baseBank); const baseEntryIndex = this.getOrCreateTokenInfoIndex(baseBank);
const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank); const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank);
if (index == -1) { if (index == -1) {
@ -208,20 +267,14 @@ export class HealthCache {
adjustSerum3Reserved( adjustSerum3Reserved(
// todo change indices to types from numbers // todo change indices to types from numbers
group: Group, baseBank: BankForHealth,
quoteBank: BankForHealth,
serum3Market: Serum3Market, serum3Market: Serum3Market,
reservedBaseChange: I80F48, reservedBaseChange: I80F48,
freeBaseChange: I80F48, freeBaseChange: I80F48,
reservedQuoteChange: I80F48, reservedQuoteChange: I80F48,
freeQuoteChange: I80F48, freeQuoteChange: I80F48,
) { ): void {
const baseBank = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
);
const quoteBank = group.getFirstBankByTokenIndex(
serum3Market.quoteTokenIndex,
);
const baseEntryIndex = this.getOrCreateTokenInfoIndex(baseBank); const baseEntryIndex = this.getOrCreateTokenInfoIndex(baseBank);
const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank); const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank);
@ -238,7 +291,11 @@ export class HealthCache {
quoteEntry.balance.iadd(freeQuoteChange.mul(quoteEntry.oraclePrice)); quoteEntry.balance.iadd(freeQuoteChange.mul(quoteEntry.oraclePrice));
// Apply it to the serum3 info // Apply it to the serum3 info
const index = this.getOrCreateSerum3InfoIndex(group, serum3Market); const index = this.getOrCreateSerum3InfoIndex(
baseBank,
quoteBank,
serum3Market,
);
const serum3Info = this.serum3Infos[index]; const serum3Info = this.serum3Infos[index];
serum3Info.reserved = serum3Info.reserved.add(reservedAmount); serum3Info.reserved = serum3Info.reserved.add(reservedAmount);
} }
@ -257,7 +314,7 @@ export class HealthCache {
return this.findPerpInfoIndex(perpMarket.perpMarketIndex); 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); if (debug) console.log(debug);
for (const token of healthCache.tokenInfos) { for (const token of healthCache.tokenInfos) {
console.log(` ${token.toString()}`); console.log(` ${token.toString()}`);
@ -293,10 +350,6 @@ export class HealthCache {
for (const change of nativeTokenChanges) { for (const change of nativeTokenChanges) {
const bank: Bank = group.getFirstBankByMint(change.mintPk); const bank: Bank = group.getFirstBankByMint(change.mintPk);
const changeIndex = adjustedCache.getOrCreateTokenInfoIndex(bank); const changeIndex = adjustedCache.getOrCreateTokenInfoIndex(bank);
if (!bank.price)
throw new Error(
`Oracle price not loaded for ${change.mintPk.toString()}`,
);
adjustedCache.tokenInfos[changeIndex].balance.iadd( adjustedCache.tokenInfos[changeIndex].balance.iadd(
change.nativeTokenAmount.mul(bank.price), change.nativeTokenAmount.mul(bank.price),
); );
@ -306,18 +359,13 @@ export class HealthCache {
} }
simHealthRatioWithSerum3BidChanges( simHealthRatioWithSerum3BidChanges(
group: Group, baseBank: BankForHealth,
quoteBank: BankForHealth,
bidNativeQuoteAmount: I80F48, bidNativeQuoteAmount: I80F48,
serum3Market: Serum3Market, serum3Market: Serum3Market,
healthType: HealthType = HealthType.init, healthType: HealthType = HealthType.init,
): I80F48 { ): I80F48 {
const adjustedCache: HealthCache = _.cloneDeep(this); 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 quoteIndex = adjustedCache.getOrCreateTokenInfoIndex(quoteBank);
const quote = adjustedCache.tokenInfos[quoteIndex]; const quote = adjustedCache.tokenInfos[quoteIndex];
@ -331,7 +379,8 @@ export class HealthCache {
// Increase reserved in Serum3Info for quote // Increase reserved in Serum3Info for quote
adjustedCache.adjustSerum3Reserved( adjustedCache.adjustSerum3Reserved(
group, baseBank,
quoteBank,
serum3Market, serum3Market,
ZERO_I80F48(), ZERO_I80F48(),
ZERO_I80F48(), ZERO_I80F48(),
@ -342,18 +391,13 @@ export class HealthCache {
} }
simHealthRatioWithSerum3AskChanges( simHealthRatioWithSerum3AskChanges(
group: Group, baseBank: BankForHealth,
quoteBank: BankForHealth,
askNativeBaseAmount: I80F48, askNativeBaseAmount: I80F48,
serum3Market: Serum3Market, serum3Market: Serum3Market,
healthType: HealthType = HealthType.init, healthType: HealthType = HealthType.init,
): I80F48 { ): I80F48 {
const adjustedCache: HealthCache = _.cloneDeep(this); 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 baseIndex = adjustedCache.getOrCreateTokenInfoIndex(baseBank);
const base = adjustedCache.tokenInfos[baseIndex]; const base = adjustedCache.tokenInfos[baseIndex];
@ -367,7 +411,8 @@ export class HealthCache {
// Increase reserved in Serum3Info for base // Increase reserved in Serum3Info for base
adjustedCache.adjustSerum3Reserved( adjustedCache.adjustSerum3Reserved(
group, baseBank,
quoteBank,
serum3Market, serum3Market,
askNativeBaseAmount, askNativeBaseAmount,
ZERO_I80F48(), ZERO_I80F48(),
@ -384,7 +429,7 @@ export class HealthCache {
rightRatio: I80F48, rightRatio: I80F48,
targetRatio: I80F48, targetRatio: I80F48,
healthRatioAfterActionFn: (I80F48) => I80F48, healthRatioAfterActionFn: (I80F48) => I80F48,
) { ): I80F48 {
const maxIterations = 40; const maxIterations = 40;
// TODO: make relative to health ratio decimals? Might be over engineering // TODO: make relative to health ratio decimals? Might be over engineering
const targetError = I80F48.fromNumber(0.001); const targetError = I80F48.fromNumber(0.001);
@ -396,11 +441,12 @@ export class HealthCache {
rightRatio.sub(targetRatio).isNeg()) rightRatio.sub(targetRatio).isNeg())
) { ) {
throw new Error( 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; let newAmount;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const key of Array(maxIterations).fill(0).keys()) { for (const key of Array(maxIterations).fill(0).keys()) {
newAmount = left.add(right).mul(I80F48.fromNumber(0.5)); newAmount = left.add(right).mul(I80F48.fromNumber(0.5));
const newAmountRatio = healthRatioAfterActionFn(newAmount); const newAmountRatio = healthRatioAfterActionFn(newAmount);
@ -427,15 +473,6 @@ export class HealthCache {
minRatio: I80F48, minRatio: I80F48,
priceFactor: I80F48, priceFactor: I80F48,
): I80F48 { ): I80F48 {
if (
!sourceBank.price ||
sourceBank.price.lte(ZERO_I80F48()) ||
!targetBank.price ||
targetBank.price.lte(ZERO_I80F48())
) {
return ZERO_I80F48();
}
if ( if (
sourceBank.initLiabWeight sourceBank.initLiabWeight
.sub(targetBank.initAssetWeight) .sub(targetBank.initAssetWeight)
@ -454,6 +491,7 @@ export class HealthCache {
// - be careful about finding the minRatio point: the function isn't convex // - be careful about finding the minRatio point: the function isn't convex
const initialRatio = this.healthRatio(HealthType.init); const initialRatio = this.healthRatio(HealthType.init);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const initialHealth = this.health(HealthType.init); const initialHealth = this.health(HealthType.init);
if (initialRatio.lte(ZERO_I80F48())) { if (initialRatio.lte(ZERO_I80F48())) {
return ZERO_I80F48(); return ZERO_I80F48();
@ -481,7 +519,7 @@ export class HealthCache {
// negative. // negative.
// The maximum will be at one of these points (ignoring serum3 effects). // 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); const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
// HealthCache.logHealthCache('beforeSwap', adjustedCache); // HealthCache.logHealthCache('beforeSwap', adjustedCache);
adjustedCache.tokenInfos[sourceIndex].balance.isub(amount); adjustedCache.tokenInfos[sourceIndex].balance.isub(amount);
@ -578,24 +616,12 @@ export class HealthCache {
} }
getMaxSerum3OrderForHealthRatio( getMaxSerum3OrderForHealthRatio(
group: Group, baseBank: BankForHealth,
quoteBank: BankForHealth,
serum3Market: Serum3Market, serum3Market: Serum3Market,
side: Serum3Side, side: Serum3Side,
minRatio: I80F48, minRatio: I80F48,
) { ): 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}`);
}
const healthCacheClone: HealthCache = _.cloneDeep(this); const healthCacheClone: HealthCache = _.cloneDeep(this);
const baseIndex = healthCacheClone.getOrCreateTokenInfoIndex(baseBank); const baseIndex = healthCacheClone.getOrCreateTokenInfoIndex(baseBank);
@ -652,10 +678,11 @@ export class HealthCache {
} }
const cache = cacheAfterPlacingOrder(zeroAmount); const cache = cacheAfterPlacingOrder(zeroAmount);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const zeroAmountHealth = cache.health(HealthType.init); const zeroAmountHealth = cache.health(HealthType.init);
const zeroAmountRatio = cache.healthRatio(HealthType.init); const zeroAmountRatio = cache.healthRatio(HealthType.init);
function cacheAfterPlacingOrder(amount: I80F48) { function cacheAfterPlacingOrder(amount: I80F48): HealthCache {
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone); const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
side === Serum3Side.ask side === Serum3Side.ask
@ -663,7 +690,8 @@ export class HealthCache {
: adjustedCache.tokenInfos[quoteIndex].balance.isub(amount); : adjustedCache.tokenInfos[quoteIndex].balance.isub(amount);
adjustedCache.adjustSerum3Reserved( adjustedCache.adjustSerum3Reserved(
group, baseBank,
quoteBank,
serum3Market, serum3Market,
side === Serum3Side.ask ? amount.div(base.oraclePrice) : ZERO_I80F48(), side === Serum3Side.ask ? amount.div(base.oraclePrice) : ZERO_I80F48(),
ZERO_I80F48(), ZERO_I80F48(),
@ -687,18 +715,7 @@ export class HealthCache {
healthRatioAfterPlacingOrder, healthRatioAfterPlacingOrder,
); );
// If its a bid then the reserved fund and potential loan is in quote, return amount;
// 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))));
} }
getMaxPerpForHealthRatio( getMaxPerpForHealthRatio(
@ -813,7 +830,7 @@ export class HealthCache {
export class TokenInfo { export class TokenInfo {
constructor( constructor(
public tokenIndex: number, public tokenIndex: TokenIndex,
public maintAssetWeight: I80F48, public maintAssetWeight: I80F48,
public initAssetWeight: I80F48, public initAssetWeight: I80F48,
public maintLiabWeight: I80F48, public maintLiabWeight: I80F48,
@ -828,7 +845,7 @@ export class TokenInfo {
static fromDto(dto: TokenInfoDto): TokenInfo { static fromDto(dto: TokenInfoDto): TokenInfo {
return new TokenInfo( return new TokenInfo(
dto.tokenIndex, dto.tokenIndex as TokenIndex,
I80F48.from(dto.maintAssetWeight), I80F48.from(dto.maintAssetWeight),
I80F48.from(dto.initAssetWeight), I80F48.from(dto.initAssetWeight),
I80F48.from(dto.maintLiabWeight), I80F48.from(dto.maintLiabWeight),
@ -839,11 +856,11 @@ export class TokenInfo {
); );
} }
static emptyFromBank(bank: BankForHealth): TokenInfo { static fromBank(
if (!bank.price) bank: BankForHealth,
throw new Error( nativeBalance?: I80F48,
`Failed to create TokenInfo. Bank price unavailable for bank with tokenIndex ${bank.tokenIndex}`, serum3MaxReserved?: I80F48,
); ): TokenInfo {
return new TokenInfo( return new TokenInfo(
bank.tokenIndex, bank.tokenIndex,
bank.maintAssetWeight, bank.maintAssetWeight,
@ -851,8 +868,8 @@ export class TokenInfo {
bank.maintLiabWeight, bank.maintLiabWeight,
bank.initLiabWeight, bank.initLiabWeight,
bank.price, bank.price,
ZERO_I80F48(), nativeBalance ? nativeBalance.mul(bank.price) : ZERO_I80F48(),
ZERO_I80F48(), serum3MaxReserved ? serum3MaxReserved : ZERO_I80F48(),
); );
} }
@ -876,7 +893,7 @@ export class TokenInfo {
).mul(this.balance); ).mul(this.balance);
} }
toString() { toString(): string {
return ` tokenIndex: ${this.tokenIndex}, balance: ${ return ` tokenIndex: ${this.tokenIndex}, balance: ${
this.balance this.balance
}, serum3MaxReserved: ${ }, serum3MaxReserved: ${
@ -890,15 +907,15 @@ export class Serum3Info {
public reserved: I80F48, public reserved: I80F48,
public baseIndex: number, public baseIndex: number,
public quoteIndex: number, public quoteIndex: number,
public marketIndex: number, public marketIndex: MarketIndex,
) {} ) {}
static fromDto(dto: Serum3InfoDto) { static fromDto(dto: Serum3InfoDto): Serum3Info {
return new Serum3Info( return new Serum3Info(
I80F48.from(dto.reserved), I80F48.from(dto.reserved),
dto.baseIndex, dto.baseIndex,
dto.quoteIndex, dto.quoteIndex,
dto.marketIndex, dto.marketIndex as MarketIndex,
); );
} }
@ -906,7 +923,7 @@ export class Serum3Info {
serum3Market: Serum3Market, serum3Market: Serum3Market,
baseEntryIndex: number, baseEntryIndex: number,
quoteEntryIndex: number, quoteEntryIndex: number,
) { ): Serum3Info {
return new Serum3Info( return new Serum3Info(
ZERO_I80F48(), ZERO_I80F48(),
baseEntryIndex, 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 { healthContribution(healthType: HealthType, tokenInfos: TokenInfo[]): I80F48 {
const baseInfo = tokenInfos[this.baseIndex]; const baseInfo = tokenInfos[this.baseIndex];
const quoteInfo = tokenInfos[this.quoteIndex]; const quoteInfo = tokenInfos[this.quoteIndex];
const reserved = this.reserved; const reserved = this.reserved;
// console.log(` - reserved ${reserved}`);
// console.log(` - this.baseIndex ${this.baseIndex}`);
// console.log(` - this.quoteIndex ${this.quoteIndex}`);
if (reserved.isZero()) { if (reserved.isZero()) {
return ZERO_I80F48(); 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 // How much the health would increase if the reserved balance were applied to the passed
// token info? // 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 // 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`. // token, including this market itself: `reserved` is already included in `max_balance`.
const maxBalance = tokenInfo.balance.add(tokenInfo.serum3MaxReserved); const maxBalance = tokenInfo.balance.add(tokenInfo.serum3MaxReserved);
@ -946,15 +1000,25 @@ export class Serum3Info {
} }
const assetWeight = tokenInfo.assetWeight(healthType); const assetWeight = tokenInfo.assetWeight(healthType);
const liabWeight = tokenInfo.liabWeight(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)); return assetWeight.mul(assetPart).add(liabWeight.mul(liabPart));
}; };
const reservedAsBase = computeHealthEffect(baseInfo); const reservedAsBase = computeHealthEffect(baseInfo);
const reservedAsQuote = computeHealthEffect(quoteInfo); const reservedAsQuote = computeHealthEffect(quoteInfo);
// console.log(` - reservedAsBase ${reservedAsBase}`);
// console.log(` - reservedAsQuote ${reservedAsQuote}`);
return reservedAsBase.min(reservedAsQuote); return reservedAsBase.min(reservedAsQuote);
} }
toString(tokenInfos: TokenInfo[]) { toString(tokenInfos: TokenInfo[]): string {
return ` marketIndex: ${this.marketIndex}, baseIndex: ${ return ` marketIndex: ${this.marketIndex}, baseIndex: ${
this.baseIndex this.baseIndex
}, quoteIndex: ${this.quoteIndex}, reserved: ${ }, quoteIndex: ${this.quoteIndex}, reserved: ${
@ -970,15 +1034,14 @@ export class PerpInfo {
public initAssetWeight: I80F48, public initAssetWeight: I80F48,
public maintLiabWeight: I80F48, public maintLiabWeight: I80F48,
public initLiabWeight: I80F48, public initLiabWeight: I80F48,
// in health-reference-token native units, needs scaling by asset/liab
public base: I80F48, public base: I80F48,
// in health-reference-token native units, no asset/liab factor needed
public quote: I80F48, public quote: I80F48,
public oraclePrice: I80F48, public oraclePrice: I80F48,
public hasOpenOrders: boolean, public hasOpenOrders: boolean,
public trustedMarket: boolean,
) {} ) {}
static fromDto(dto: PerpInfoDto) { static fromDto(dto: PerpInfoDto): PerpInfo {
return new PerpInfo( return new PerpInfo(
dto.perpMarketIndex, dto.perpMarketIndex,
I80F48.from(dto.maintAssetWeight), I80F48.from(dto.maintAssetWeight),
@ -989,6 +1052,114 @@ export class PerpInfo {
I80F48.from(dto.quote), I80F48.from(dto.quote),
I80F48.from(dto.oraclePrice), I80F48.from(dto.oraclePrice),
dto.hasOpenOrders, 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; weight = this.maintAssetWeight;
} }
// FUTURE: Allow v3-style "reliable" markets where we can return // console.log(`initLiabWeight ${this.initLiabWeight}`);
// `self.quote + weight * self.base` here // console.log(`initAssetWeight ${this.initAssetWeight}`);
return this.quote.add(weight.mul(this.base)).min(ZERO_I80F48()); // 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 { static emptyFromPerpMarket(perpMarket: PerpMarket): PerpInfo {
if (!perpMarket.price)
throw new Error(
`Failed to create PerpInfo. Oracle price unavailable. ${perpMarket.oracle.toString()}`,
);
return new PerpInfo( return new PerpInfo(
perpMarket.perpMarketIndex, perpMarket.perpMarketIndex,
perpMarket.maintAssetWeight, perpMarket.maintAssetWeight,
@ -1024,10 +1200,19 @@ export class PerpInfo {
perpMarket.initLiabWeight, perpMarket.initLiabWeight,
ZERO_I80F48(), ZERO_I80F48(),
ZERO_I80F48(), ZERO_I80F48(),
I80F48.fromNumber(perpMarket.price), perpMarket.price,
false, 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 { export class HealthCacheDto {
@ -1087,10 +1272,9 @@ export class PerpInfoDto {
initAssetWeight: I80F48Dto; initAssetWeight: I80F48Dto;
maintLiabWeight: I80F48Dto; maintLiabWeight: I80F48Dto;
initLiabWeight: I80F48Dto; initLiabWeight: I80F48Dto;
// in health-reference-token native units, needs scaling by asset/liab
base: I80F48Dto; base: I80F48Dto;
// in health-reference-token native units, no asset/liab factor needed
quote: I80F48Dto; quote: I80F48Dto;
oraclePrice: I80F48Dto; oraclePrice: I80F48Dto;
hasOpenOrders: boolean; hasOpenOrders: boolean;
trustedMarket: boolean;
} }

View File

@ -1,20 +1,21 @@
import { BN } from '@project-serum/anchor'; import { BN } from '@project-serum/anchor';
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes'; 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 { PublicKey } from '@solana/web3.js';
import { MangoClient } from '../client'; import { MangoClient } from '../client';
import { SERUM3_PROGRAM_ID } from '../constants';
import { import {
nativeI80F48ToUi, nativeI80F48ToUi,
toNative, toNative,
toUiDecimals, toUiDecimals,
toUiDecimalsForQuote, toUiDecimalsForQuote,
} from '../utils'; } from '../utils';
import { Bank } from './bank'; import { Bank, TokenIndex } from './bank';
import { Group } from './group'; import { Group } from './group';
import { HealthCache, HealthCacheDto } from './healthCache'; import { HealthCache } from './healthCache';
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48'; import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48';
import { PerpOrder, PerpOrderSide } from './perp'; import { PerpMarket, PerpMarketIndex, PerpOrder, PerpOrderSide } from './perp';
import { Serum3Side } from './serum3'; import { MarketIndex, Serum3Side } from './serum3';
export class MangoAccount { export class MangoAccount {
public tokens: TokenPosition[]; public tokens: TokenPosition[];
public serum3: Serum3Orders[]; public serum3: Serum3Orders[];
@ -58,7 +59,7 @@ export class MangoAccount {
obj.serum3 as Serum3PositionDto[], obj.serum3 as Serum3PositionDto[],
obj.perps as PerpPositionDto[], obj.perps as PerpPositionDto[],
obj.perpOpenOrders as PerpOoDto[], obj.perpOpenOrders as PerpOoDto[],
{} as any, new Map(), // serum3OosMapByMarketIndex
); );
} }
@ -78,39 +79,56 @@ export class MangoAccount {
serum3: Serum3PositionDto[], serum3: Serum3PositionDto[],
perps: PerpPositionDto[], perps: PerpPositionDto[],
perpOpenOrders: PerpOoDto[], perpOpenOrders: PerpOoDto[],
public accountData: undefined | MangoAccountData, public serum3OosMapByMarketIndex: Map<number, OpenOrders>,
) { ) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
this.tokens = tokens.map((dto) => TokenPosition.from(dto)); this.tokens = tokens.map((dto) => TokenPosition.from(dto));
this.serum3 = serum3.map((dto) => Serum3Orders.from(dto)); this.serum3 = serum3.map((dto) => Serum3Orders.from(dto));
this.perps = perps.map((dto) => PerpPosition.from(dto)); this.perps = perps.map((dto) => PerpPosition.from(dto));
this.perpOpenOrders = perpOpenOrders.map((dto) => PerpOo.from(dto)); this.perpOpenOrders = perpOpenOrders.map((dto) => PerpOo.from(dto));
this.accountData = undefined;
this.netDeposits = netDeposits; this.netDeposits = netDeposits;
} }
async reload(client: MangoClient, group: Group): Promise<MangoAccount> { async reload(client: MangoClient): Promise<MangoAccount> {
const mangoAccount = await client.getMangoAccount(this); const mangoAccount = await client.getMangoAccount(this);
await mangoAccount.reloadAccountData(client, group); await mangoAccount.reloadAccountData(client);
Object.assign(this, mangoAccount); Object.assign(this, mangoAccount);
return mangoAccount; return mangoAccount;
} }
async reloadWithSlot( async reloadWithSlot(
client: MangoClient, client: MangoClient,
group: Group,
): Promise<{ value: MangoAccount; slot: number }> { ): Promise<{ value: MangoAccount; slot: number }> {
const resp = await client.getMangoAccountWithSlot(this.publicKey); const resp = await client.getMangoAccountWithSlot(this.publicKey);
await resp?.value.reloadAccountData(client, group); await resp?.value.reloadAccountData(client);
Object.assign(this, resp?.value); Object.assign(this, resp?.value);
return { value: resp!.value, slot: resp!.slot }; return { value: resp!.value, slot: resp!.slot };
} }
async reloadAccountData( async reloadAccountData(client: MangoClient): Promise<MangoAccount> {
client: MangoClient, const serum3Active = this.serum3Active();
group: Group, const ais =
): Promise<MangoAccount> { await client.program.provider.connection.getMultipleAccountsInfo(
this.accountData = await client.computeAccountData(group, this); 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; 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); 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); 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 // 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 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 // * 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 * @returns native balance for a token, is signed
*/ */
getTokenBalance(bank: Bank): I80F48 { getTokenBalance(bank: Bank): I80F48 {
const tp = this.findToken(bank.tokenIndex); const tp = this.getToken(bank.tokenIndex);
return tp ? tp.balance(bank) : ZERO_I80F48(); 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 * @returns native deposits for a token, 0 if position has borrows
*/ */
getTokenDeposits(bank: Bank): I80F48 { getTokenDeposits(bank: Bank): I80F48 {
const tp = this.findToken(bank.tokenIndex); const tp = this.getToken(bank.tokenIndex);
return tp ? tp.deposits(bank) : ZERO_I80F48(); 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 * @returns native borrows for a token, 0 if position has deposits
*/ */
getTokenBorrows(bank: Bank): I80F48 { getTokenBorrows(bank: Bank): I80F48 {
const tp = this.findToken(bank.tokenIndex); const tp = this.getToken(bank.tokenIndex);
return tp ? tp.borrows(bank) : ZERO_I80F48(); return tp ? tp.borrows(bank) : ZERO_I80F48();
} }
@ -182,7 +212,7 @@ export class MangoAccount {
* @returns UI balance for a token, is signed * @returns UI balance for a token, is signed
*/ */
getTokenBalanceUi(bank: Bank): number { getTokenBalanceUi(bank: Bank): number {
const tp = this.findToken(bank.tokenIndex); const tp = this.getToken(bank.tokenIndex);
return tp ? tp.balanceUi(bank) : 0; return tp ? tp.balanceUi(bank) : 0;
} }
@ -192,7 +222,7 @@ export class MangoAccount {
* @returns UI deposits for a token, 0 or more * @returns UI deposits for a token, 0 or more
*/ */
getTokenDepositsUi(bank: Bank): number { getTokenDepositsUi(bank: Bank): number {
const ta = this.findToken(bank.tokenIndex); const ta = this.getToken(bank.tokenIndex);
return ta ? ta.depositsUi(bank) : 0; return ta ? ta.depositsUi(bank) : 0;
} }
@ -202,7 +232,7 @@ export class MangoAccount {
* @returns UI borrows for a token, 0 or less * @returns UI borrows for a token, 0 or less
*/ */
getTokenBorrowsUi(bank: Bank): number { getTokenBorrowsUi(bank: Bank): number {
const ta = this.findToken(bank.tokenIndex); const ta = this.getToken(bank.tokenIndex);
return ta ? ta.borrowsUi(bank) : 0; return ta ? ta.borrowsUi(bank) : 0;
} }
@ -211,10 +241,9 @@ export class MangoAccount {
* @param healthType * @param healthType
* @returns raw health number, in native quote * @returns raw health number, in native quote
*/ */
getHealth(healthType: HealthType): I80F48 | undefined { getHealth(group: Group, healthType: HealthType): I80F48 {
return healthType == HealthType.init const hc = HealthCache.fromMangoAccount(group, this);
? this.accountData?.initHealth return hc.health(healthType);
: this.accountData?.maintHealth;
} }
/** /**
@ -223,8 +252,9 @@ export class MangoAccount {
* @param healthType * @param healthType
* @returns health ratio, in percentage form * @returns health ratio, in percentage form
*/ */
getHealthRatio(healthType: HealthType): I80F48 | undefined { getHealthRatio(group: Group, healthType: HealthType): I80F48 {
return this.accountData?.healthCache.healthRatio(healthType); const hc = HealthCache.fromMangoAccount(group, this);
return hc.healthRatio(healthType);
} }
/** /**
@ -232,8 +262,8 @@ export class MangoAccount {
* @param healthType * @param healthType
* @returns health ratio, in percentage form, capped to 100 * @returns health ratio, in percentage form, capped to 100
*/ */
getHealthRatioUi(healthType: HealthType): number | undefined { getHealthRatioUi(group: Group, healthType: HealthType): number | undefined {
const ratio = this.getHealthRatio(healthType)?.toNumber(); const ratio = this.getHealthRatio(group, healthType).toNumber();
if (ratio) { if (ratio) {
return ratio > 100 ? 100 : Math.trunc(ratio); return ratio > 100 ? 100 : Math.trunc(ratio);
} else { } 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. * 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 * @returns equity, in native quote
*/ */
getEquity(): I80F48 | undefined { getEquity(group: Group): I80F48 {
if (this.accountData) { const tokensMap = new Map<number, I80F48>();
const equity = this.accountData.equity; for (const tp of this.tokensActive()) {
const total_equity = equity.tokens.reduce( const bank = group.getFirstBankByTokenIndex(tp.tokenIndex);
(a, b) => a.add(b.value), tokensMap.set(tp.tokenIndex, tp.balance(bank).mul(bank.price));
ZERO_I80F48(),
);
return total_equity;
} }
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. * The amount of native quote you could withdraw against your existing assets.
* @returns collateral value, in native quote * @returns collateral value, in native quote
*/ */
getCollateralValue(): I80F48 | undefined { getCollateralValue(group: Group): I80F48 {
return this.getHealth(HealthType.init); return this.getHealth(group, HealthType.init);
} }
/** /**
* Sum of all positive assets. * Sum of all positive assets.
* @returns assets, in native quote * @returns assets, in native quote
*/ */
getAssetsValue(healthType: HealthType): I80F48 | undefined { getAssetsValue(group: Group, healthType: HealthType): I80F48 {
return this.accountData?.healthCache.assets(healthType); const hc = HealthCache.fromMangoAccount(group, this);
return hc.assets(healthType);
} }
/** /**
* Sum of all negative assets. * Sum of all negative assets.
* @returns liabs, in native quote * @returns liabs, in native quote
*/ */
getLiabsValue(healthType: HealthType): I80F48 | undefined { getLiabsValue(group: Group, healthType: HealthType): I80F48 {
return this.accountData?.healthCache.liabs(healthType); 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) * 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) * 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 { getPnl(group: Group): I80F48 {
return this.getEquity()?.add( return this.getEquity(group)?.add(
I80F48.fromI64(this.netDeposits).mul(I80F48.fromNumber(-1)), I80F48.fromI64(this.netDeposits).mul(I80F48.fromNumber(-1)),
); );
} }
@ -301,7 +364,7 @@ export class MangoAccount {
mintPk: PublicKey, mintPk: PublicKey,
): I80F48 | undefined { ): I80F48 | undefined {
const tokenBank: Bank = group.getFirstBankByMint(mintPk); const tokenBank: Bank = group.getFirstBankByMint(mintPk);
const initHealth = this.accountData?.initHealth; const initHealth = this.getHealth(group, HealthType.init);
if (!initHealth) return undefined; if (!initHealth) return undefined;
@ -314,8 +377,7 @@ export class MangoAccount {
// Deposits need special treatment since they would neither count towards liabilities // Deposits need special treatment since they would neither count towards liabilities
// nor would be charged loanOriginationFeeRate when withdrawn // nor would be charged loanOriginationFeeRate when withdrawn
const tp = this.findToken(tokenBank.tokenIndex); const tp = this.getToken(tokenBank.tokenIndex);
if (!tokenBank.price) return undefined;
const existingTokenDeposits = tp ? tp.deposits(tokenBank) : ZERO_I80F48(); const existingTokenDeposits = tp ? tp.deposits(tokenBank) : ZERO_I80F48();
let existingPositionHealthContrib = ZERO_I80F48(); let existingPositionHealthContrib = ZERO_I80F48();
if (existingTokenDeposits.gt(ZERO_I80F48())) { if (existingTokenDeposits.gt(ZERO_I80F48())) {
@ -377,18 +439,14 @@ export class MangoAccount {
targetMintPk: PublicKey, targetMintPk: PublicKey,
priceFactor: number, priceFactor: number,
): number | undefined { ): number | undefined {
if (!this.accountData) {
throw new Error(
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
);
}
if (sourceMintPk.equals(targetMintPk)) { if (sourceMintPk.equals(targetMintPk)) {
return 0; return 0;
} }
const maxSource = this.accountData.healthCache.getMaxSourceForTokenSwap( const hc = HealthCache.fromMangoAccount(group, this);
const maxSource = hc.getMaxSourceForTokenSwap(
group.getFirstBankByMint(sourceMintPk), group.getFirstBankByMint(sourceMintPk),
group.getFirstBankByMint(targetMintPk), group.getFirstBankByMint(targetMintPk),
ONE_I80F48(), // target 1% health I80F48.fromNumber(2), // target 2% health
I80F48.fromNumber(priceFactor), I80F48.fromNumber(priceFactor),
); );
maxSource.idiv( maxSource.idiv(
@ -426,7 +484,8 @@ export class MangoAccount {
mintPk: tokenChange.mintPk, mintPk: tokenChange.mintPk,
}; };
}); });
return this.accountData?.healthCache const hc = HealthCache.fromMangoAccount(group, this);
return hc
.simHealthRatioWithTokenPositionChanges( .simHealthRatioWithTokenPositionChanges(
group, group,
nativeTokenChanges, nativeTokenChanges,
@ -440,14 +499,8 @@ export class MangoAccount {
group: Group, group: Group,
externalMarketPk: PublicKey, externalMarketPk: PublicKey,
): Promise<Order[]> { ): Promise<Order[]> {
const serum3Market = group.serum3MarketsMapByExternal.get( const serum3Market =
externalMarketPk.toBase58(), group.getSerum3MarketByExternalMarket(externalMarketPk);
);
if (!serum3Market) {
throw new Error(
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
const serum3OO = this.serum3Active().find( const serum3OO = this.serum3Active().find(
(s) => s.marketIndex === serum3Market.marketIndex, (s) => s.marketIndex === serum3Market.marketIndex,
); );
@ -455,7 +508,7 @@ export class MangoAccount {
throw new Error(`No open orders account found for ${externalMarketPk}`); throw new Error(`No open orders account found for ${externalMarketPk}`);
} }
const serum3MarketExternal = group.serum3MarketExternalsMap.get( const serum3MarketExternal = group.serum3ExternalMarketsMap.get(
externalMarketPk.toBase58(), externalMarketPk.toBase58(),
)!; )!;
const [bidsInfo, asksInfo] = const [bidsInfo, asksInfo] =
@ -463,9 +516,14 @@ export class MangoAccount {
serum3MarketExternal.bidsAddress, serum3MarketExternal.bidsAddress,
serum3MarketExternal.asksAddress, serum3MarketExternal.asksAddress,
]); ]);
if (!bidsInfo || !asksInfo) { if (!bidsInfo) {
throw new Error( 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); const bids = Orderbook.decode(serum3MarketExternal, bidsInfo.data);
@ -476,7 +534,6 @@ export class MangoAccount {
} }
/** /**
* TODO priceFactor
* @param group * @param group
* @param externalMarketPk * @param externalMarketPk
* @returns maximum ui quote which can be traded for base token given current health * @returns maximum ui quote which can be traded for base token given current health
@ -485,26 +542,28 @@ export class MangoAccount {
group: Group, group: Group,
externalMarketPk: PublicKey, externalMarketPk: PublicKey,
): number { ): number {
const serum3Market = group.serum3MarketsMapByExternal.get( const serum3Market =
externalMarketPk.toBase58(), group.getSerum3MarketByExternalMarket(externalMarketPk);
const baseBank = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
); );
if (!serum3Market) { const quoteBank = group.getFirstBankByTokenIndex(
throw new Error( serum3Market.quoteTokenIndex,
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`, );
); const hc = HealthCache.fromMangoAccount(group, this);
} let nativeAmount = hc.getMaxSerum3OrderForHealthRatio(
if (!this.accountData) { baseBank,
throw new Error( quoteBank,
`accountData not loaded on MangoAccount, try reloading MangoAccount`, serum3Market,
); Serum3Side.bid,
} I80F48.fromNumber(2),
const nativeAmount = );
this.accountData.healthCache.getMaxSerum3OrderForHealthRatio( // If its a bid then the reserved fund and potential loan is in base
group, // also keep some buffer for fees, use taker fees for worst case simulation.
serum3Market, nativeAmount = nativeAmount
Serum3Side.bid, .div(quoteBank.price)
I80F48.fromNumber(1), .div(ONE_I80F48().add(baseBank.loanOriginationFeeRate))
); .div(ONE_I80F48().add(I80F48.fromNumber(group.getSerum3FeeRates(false))));
return toUiDecimals( return toUiDecimals(
nativeAmount, nativeAmount,
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex).mintDecimals, group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex).mintDecimals,
@ -512,7 +571,6 @@ export class MangoAccount {
} }
/** /**
* TODO priceFactor
* @param group * @param group
* @param externalMarketPk * @param externalMarketPk
* @returns maximum ui base which can be traded for quote token given current health * @returns maximum ui base which can be traded for quote token given current health
@ -521,26 +579,28 @@ export class MangoAccount {
group: Group, group: Group,
externalMarketPk: PublicKey, externalMarketPk: PublicKey,
): number { ): number {
const serum3Market = group.serum3MarketsMapByExternal.get( const serum3Market =
externalMarketPk.toBase58(), group.getSerum3MarketByExternalMarket(externalMarketPk);
const baseBank = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
); );
if (!serum3Market) { const quoteBank = group.getFirstBankByTokenIndex(
throw new Error( serum3Market.quoteTokenIndex,
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`, );
); const hc = HealthCache.fromMangoAccount(group, this);
} let nativeAmount = hc.getMaxSerum3OrderForHealthRatio(
if (!this.accountData) { baseBank,
throw new Error( quoteBank,
`accountData not loaded on MangoAccount, try reloading MangoAccount`, serum3Market,
); Serum3Side.ask,
} I80F48.fromNumber(2),
const nativeAmount = );
this.accountData.healthCache.getMaxSerum3OrderForHealthRatio( // If its a ask then the reserved fund and potential loan is in base
group, // also keep some buffer for fees, use taker fees for worst case simulation.
serum3Market, nativeAmount = nativeAmount
Serum3Side.ask, .div(baseBank.price)
I80F48.fromNumber(1), .div(ONE_I80F48().add(baseBank.loanOriginationFeeRate))
); .div(ONE_I80F48().add(I80F48.fromNumber(group.getSerum3FeeRates(false))));
return toUiDecimals( return toUiDecimals(
nativeAmount, nativeAmount,
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex).mintDecimals, group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex).mintDecimals,
@ -561,22 +621,19 @@ export class MangoAccount {
externalMarketPk: PublicKey, externalMarketPk: PublicKey,
healthType: HealthType = HealthType.init, healthType: HealthType = HealthType.init,
): number { ): number {
const serum3Market = group.serum3MarketsMapByExternal.get( const serum3Market =
externalMarketPk.toBase58(), group.getSerum3MarketByExternalMarket(externalMarketPk);
const baseBank = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
); );
if (!serum3Market) { const quoteBank = group.getFirstBankByTokenIndex(
throw new Error( serum3Market.quoteTokenIndex,
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`, );
); const hc = HealthCache.fromMangoAccount(group, this);
} return hc
if (!this.accountData) {
throw new Error(
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
);
}
return this.accountData.healthCache
.simHealthRatioWithSerum3BidChanges( .simHealthRatioWithSerum3BidChanges(
group, baseBank,
quoteBank,
toNative( toNative(
uiQuoteAmount, uiQuoteAmount,
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex) group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex)
@ -602,22 +659,19 @@ export class MangoAccount {
externalMarketPk: PublicKey, externalMarketPk: PublicKey,
healthType: HealthType = HealthType.init, healthType: HealthType = HealthType.init,
): number { ): number {
const serum3Market = group.serum3MarketsMapByExternal.get( const serum3Market =
externalMarketPk.toBase58(), group.getSerum3MarketByExternalMarket(externalMarketPk);
const baseBank = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
); );
if (!serum3Market) { const quoteBank = group.getFirstBankByTokenIndex(
throw new Error( serum3Market.quoteTokenIndex,
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`, );
); const hc = HealthCache.fromMangoAccount(group, this);
} return hc
if (!this.accountData) {
throw new Error(
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
);
}
return this.accountData.healthCache
.simHealthRatioWithSerum3AskChanges( .simHealthRatioWithSerum3AskChanges(
group, baseBank,
quoteBank,
toNative( toNative(
uiBaseAmount, uiBaseAmount,
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex) group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
@ -638,28 +692,21 @@ export class MangoAccount {
*/ */
public getMaxQuoteForPerpBidUi( public getMaxQuoteForPerpBidUi(
group: Group, group: Group,
perpMarketName: string, perpMarketIndex: PerpMarketIndex,
uiPrice: number, uiPrice: number,
): number { ): number {
const perpMarket = group.perpMarketsMap.get(perpMarketName); const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
if (!perpMarket) { const hc = HealthCache.fromMangoAccount(group, this);
throw new Error(`PerpMarket for ${perpMarketName} not found!`); const baseLots = hc.getMaxPerpForHealthRatio(
}
if (!this.accountData) {
throw new Error(
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
);
}
const baseLots = this.accountData.healthCache.getMaxPerpForHealthRatio(
perpMarket, perpMarket,
PerpOrderSide.bid, PerpOrderSide.bid,
I80F48.fromNumber(1), I80F48.fromNumber(2),
group.toNativePrice(uiPrice, perpMarket.baseDecimals), group.toNativePrice(uiPrice, perpMarket.baseDecimals),
); );
const nativeBase = baseLots.mul( const nativeBase = baseLots.mul(
I80F48.fromString(perpMarket.baseLotSize.toString()), I80F48.fromString(perpMarket.baseLotSize.toString()),
); );
const nativeQuote = nativeBase.mul(I80F48.fromNumber(perpMarket.price)); const nativeQuote = nativeBase.mul(perpMarket.price);
return toUiDecimalsForQuote(nativeQuote.toNumber()); return toUiDecimalsForQuote(nativeQuote.toNumber());
} }
@ -672,22 +719,15 @@ export class MangoAccount {
*/ */
public getMaxBaseForPerpAskUi( public getMaxBaseForPerpAskUi(
group: Group, group: Group,
perpMarketName: string, perpMarketIndex: PerpMarketIndex,
uiPrice: number, uiPrice: number,
): number { ): number {
const perpMarket = group.perpMarketsMap.get(perpMarketName); const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
if (!perpMarket) { const hc = HealthCache.fromMangoAccount(group, this);
throw new Error(`PerpMarket for ${perpMarketName} not found!`); const baseLots = hc.getMaxPerpForHealthRatio(
}
if (!this.accountData) {
throw new Error(
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
);
}
const baseLots = this.accountData.healthCache.getMaxPerpForHealthRatio(
perpMarket, perpMarket,
PerpOrderSide.ask, PerpOrderSide.ask,
I80F48.fromNumber(1), I80F48.fromNumber(2),
group.toNativePrice(uiPrice, perpMarket.baseDecimals), group.toNativePrice(uiPrice, perpMarket.baseDecimals),
); );
return perpMarket.baseLotsToUi(new BN(baseLots.toString())); return perpMarket.baseLotsToUi(new BN(baseLots.toString()));
@ -696,12 +736,9 @@ export class MangoAccount {
public async loadPerpOpenOrdersForMarket( public async loadPerpOpenOrdersForMarket(
client: MangoClient, client: MangoClient,
group: Group, group: Group,
perpMarketName: string, perpMarketIndex: PerpMarketIndex,
): Promise<PerpOrder[]> { ): Promise<PerpOrder[]> {
const perpMarket = group.perpMarketsMap.get(perpMarketName); const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
if (!perpMarket) {
throw new Error(`Perp Market ${perpMarketName} not found!`);
}
const [bids, asks] = await Promise.all([ const [bids, asks] = await Promise.all([
perpMarket.loadBids(client), perpMarket.loadBids(client),
perpMarket.loadAsks(client), perpMarket.loadAsks(client),
@ -759,17 +796,17 @@ export class MangoAccount {
export class TokenPosition { export class TokenPosition {
static TokenIndexUnset = 65535; static TokenIndexUnset = 65535;
static from(dto: TokenPositionDto) { static from(dto: TokenPositionDto): TokenPosition {
return new TokenPosition( return new TokenPosition(
I80F48.from(dto.indexedPosition), I80F48.from(dto.indexedPosition),
dto.tokenIndex, dto.tokenIndex as TokenIndex,
dto.inUseCount, dto.inUseCount,
); );
} }
constructor( constructor(
public indexedPosition: I80F48, public indexedPosition: I80F48,
public tokenIndex: number, public tokenIndex: TokenIndex,
public inUseCount: number, public inUseCount: number,
) {} ) {}
@ -877,17 +914,17 @@ export class Serum3Orders {
static from(dto: Serum3PositionDto): Serum3Orders { static from(dto: Serum3PositionDto): Serum3Orders {
return new Serum3Orders( return new Serum3Orders(
dto.openOrders, dto.openOrders,
dto.marketIndex, dto.marketIndex as MarketIndex,
dto.baseTokenIndex, dto.baseTokenIndex as TokenIndex,
dto.quoteTokenIndex, dto.quoteTokenIndex as TokenIndex,
); );
} }
constructor( constructor(
public openOrders: PublicKey, public openOrders: PublicKey,
public marketIndex: number, public marketIndex: MarketIndex,
public baseTokenIndex: number, public baseTokenIndex: TokenIndex,
public quoteTokenIndex: number, public quoteTokenIndex: TokenIndex,
) {} ) {}
public isActive(): boolean { public isActive(): boolean {
@ -907,31 +944,77 @@ export class Serum3PositionDto {
export class PerpPosition { export class PerpPosition {
static PerpMarketIndexUnset = 65535; static PerpMarketIndexUnset = 65535;
static from(dto: PerpPositionDto) { static from(dto: PerpPositionDto): PerpPosition {
return new PerpPosition( return new PerpPosition(
dto.marketIndex, dto.marketIndex as PerpMarketIndex,
dto.basePositionLots.toNumber(), dto.basePositionLots.toNumber(),
dto.quotePositionNative.val, I80F48.from(dto.quotePositionNative),
dto.bidsBaseLots.toNumber(), dto.bidsBaseLots.toNumber(),
dto.asksBaseLots.toNumber(), dto.asksBaseLots.toNumber(),
dto.takerBaseLots.toNumber(), dto.takerBaseLots.toNumber(),
dto.takerQuoteLots.toNumber(), dto.takerQuoteLots.toNumber(),
I80F48.from(dto.longSettledFunding),
I80F48.from(dto.shortSettledFunding),
); );
} }
constructor( constructor(
public marketIndex: number, public marketIndex: PerpMarketIndex,
public basePositionLots: number, public basePositionLots: number,
public quotePositionNative: BN, public quotePositionNative: I80F48,
public bidsBaseLots: number, public bidsBaseLots: number,
public asksBaseLots: number, public asksBaseLots: number,
public takerBaseLots: number, public takerBaseLots: number,
public takerQuoteLots: number, public takerQuoteLots: number,
public longSettledFunding: I80F48,
public shortSettledFunding: I80F48,
) {} ) {}
isActive(): boolean { isActive(): boolean {
return this.marketIndex != PerpPosition.PerpMarketIndexUnset; 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 { export class PerpPositionDto {
@ -944,12 +1027,14 @@ export class PerpPositionDto {
public asksBaseLots: BN, public asksBaseLots: BN,
public takerBaseLots: BN, public takerBaseLots: BN,
public takerQuoteLots: BN, public takerQuoteLots: BN,
public longSettledFunding: I80F48Dto,
public shortSettledFunding: I80F48Dto,
) {} ) {}
} }
export class PerpOo { export class PerpOo {
static OrderMarketUnset = 65535; static OrderMarketUnset = 65535;
static from(dto: PerpOoDto) { static from(dto: PerpOoDto): PerpOo {
return new PerpOo( return new PerpOo(
dto.orderSide, dto.orderSide,
dto.orderMarket, dto.orderMarket,
@ -978,66 +1063,3 @@ export class HealthType {
static maint = { maint: {} }; static maint = { maint: {} };
static init = { init: {} }; 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); 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 { 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 { PublicKey } from '@solana/web3.js';
import Big from 'big.js'; import Big from 'big.js';
import { MangoClient } from '../client'; import { MangoClient } from '../client';
import { U64_MAX_BN } from '../utils'; import { As, U64_MAX_BN } from '../utils';
import { OracleConfig, QUOTE_DECIMALS } from './bank'; import { OracleConfig, QUOTE_DECIMALS } from './bank';
import { I80F48, I80F48Dto } from './I80F48'; import { I80F48, I80F48Dto } from './I80F48';
export type PerpMarketIndex = number & As<'perp-market-index'>;
export class PerpMarket { export class PerpMarket {
public name: string; public name: string;
public maintAssetWeight: I80F48; public maintAssetWeight: I80F48;
@ -18,21 +20,23 @@ export class PerpMarket {
public takerFee: I80F48; public takerFee: I80F48;
public minFunding: I80F48; public minFunding: I80F48;
public maxFunding: I80F48; public maxFunding: I80F48;
public longFunding: I80F48;
public shortFunding: I80F48;
public openInterest: number; public openInterest: number;
public seqNum: number; public seqNum: number;
public feesAccrued: I80F48; public feesAccrued: I80F48;
priceLotsToUiConverter: number; priceLotsToUiConverter: number;
baseLotsToUiConverter: number; baseLotsToUiConverter: number;
quoteLotsToUiConverter: number; quoteLotsToUiConverter: number;
public price: number; public _price: I80F48;
public uiPrice: number; public _uiPrice: number;
static from( static from(
publicKey: PublicKey, publicKey: PublicKey,
obj: { obj: {
group: PublicKey; group: PublicKey;
quoteTokenIndex: number;
perpMarketIndex: number; perpMarketIndex: number;
trustedMarket: number;
name: number[]; name: number[];
oracle: PublicKey; oracle: PublicKey;
oracleConfig: OracleConfig; oracleConfig: OracleConfig;
@ -55,7 +59,7 @@ export class PerpMarket {
shortFunding: I80F48Dto; shortFunding: I80F48Dto;
fundingLastUpdated: BN; fundingLastUpdated: BN;
openInterest: BN; openInterest: BN;
seqNum: any; // TODO: ts complains that this is unknown for whatever reason seqNum: BN;
feesAccrued: I80F48Dto; feesAccrued: I80F48Dto;
bump: number; bump: number;
baseDecimals: number; baseDecimals: number;
@ -65,8 +69,8 @@ export class PerpMarket {
return new PerpMarket( return new PerpMarket(
publicKey, publicKey,
obj.group, obj.group,
obj.quoteTokenIndex, obj.perpMarketIndex as PerpMarketIndex,
obj.perpMarketIndex, obj.trustedMarket == 1,
obj.name, obj.name,
obj.oracle, obj.oracle,
obj.oracleConfig, obj.oracleConfig,
@ -100,8 +104,8 @@ export class PerpMarket {
constructor( constructor(
public publicKey: PublicKey, public publicKey: PublicKey,
public group: PublicKey, public group: PublicKey,
public quoteTokenIndex: number, public perpMarketIndex: PerpMarketIndex, // TODO rename to marketIndex?
public perpMarketIndex: number, public trustedMarket: boolean,
name: number[], name: number[],
public oracle: PublicKey, public oracle: PublicKey,
oracleConfig: OracleConfig, oracleConfig: OracleConfig,
@ -140,6 +144,8 @@ export class PerpMarket {
this.takerFee = I80F48.from(takerFee); this.takerFee = I80F48.from(takerFee);
this.minFunding = I80F48.from(minFunding); this.minFunding = I80F48.from(minFunding);
this.maxFunding = I80F48.from(maxFunding); this.maxFunding = I80F48.from(maxFunding);
this.longFunding = I80F48.from(longFunding);
this.shortFunding = I80F48.from(shortFunding);
this.openInterest = openInterest.toNumber(); this.openInterest = openInterest.toNumber();
this.seqNum = seqNum.toNumber(); this.seqNum = seqNum.toNumber();
this.feesAccrued = I80F48.from(feesAccrued); this.feesAccrued = I80F48.from(feesAccrued);
@ -159,6 +165,23 @@ export class PerpMarket {
.toNumber(); .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> { public async loadAsks(client: MangoClient): Promise<BookSide> {
const asks = await client.program.account.bookSide.fetch(this.asks); const asks = await client.program.account.bookSide.fetch(this.asks);
return BookSide.from(client, this, BookSideType.asks, asks); return BookSide.from(client, this, BookSideType.asks, asks);
@ -176,7 +199,10 @@ export class PerpMarket {
return new PerpEventQueue(client, eventQueue.header, eventQueue.buf); 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); const eventQueue = await this.loadEventQueue(client);
return eventQueue return eventQueue
.eventsSince(lastSeqNum) .eventsSince(lastSeqNum)
@ -189,13 +215,13 @@ export class PerpMarket {
* @param asks * @param asks
* @returns returns funding rate per hour * @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 MIN_FUNDING = this.minFunding.toNumber();
const MAX_FUNDING = this.maxFunding.toNumber(); const MAX_FUNDING = this.maxFunding.toNumber();
const bid = bids.getImpactPriceUi(new BN(this.impactQuantity)); const bid = bids.getImpactPriceUi(new BN(this.impactQuantity));
const ask = asks.getImpactPriceUi(new BN(this.impactQuantity)); const ask = asks.getImpactPriceUi(new BN(this.impactQuantity));
const indexPrice = this.uiPrice; const indexPrice = this._uiPrice;
let funding; let funding;
if (bid !== undefined && ask !== undefined) { if (bid !== undefined && ask !== undefined) {
@ -284,7 +310,7 @@ export class BookSide {
leafCount: number; leafCount: number;
nodes: unknown; nodes: unknown;
}, },
) { ): BookSide {
return new BookSide( return new BookSide(
client, client,
perpMarket, perpMarket,
@ -311,7 +337,6 @@ export class BookSide {
public includeExpired = false, public includeExpired = false,
maxBookDelay?: number, maxBookDelay?: number,
) { ) {
// TODO why? Ask Daffy
// Determine the maxTimestamp found on the book to use for tif // Determine the maxTimestamp found on the book to use for tif
// If maxBookDelay is not provided, use 3600 as a very large number // If maxBookDelay is not provided, use 3600 as a very large number
maxBookDelay = maxBookDelay === undefined ? 3600 : maxBookDelay; maxBookDelay = maxBookDelay === undefined ? 3600 : maxBookDelay;
@ -329,7 +354,7 @@ export class BookSide {
this.now = maxTimestamp; this.now = maxTimestamp;
} }
static getPriceFromKey(key: BN) { static getPriceFromKey(key: BN): BN {
return key.ushrn(64); return key.ushrn(64);
} }
@ -456,7 +481,7 @@ export class LeafNode {
) {} ) {}
} }
export class InnerNode { export class InnerNode {
static from(obj: { children: [number] }) { static from(obj: { children: [number] }): InnerNode {
return new InnerNode(obj.children); return new InnerNode(obj.children);
} }
@ -477,7 +502,11 @@ export class PerpOrderType {
} }
export class PerpOrder { export class PerpOrder {
static from(perpMarket: PerpMarket, leafNode: LeafNode, type: BookSideType) { static from(
perpMarket: PerpMarket,
leafNode: LeafNode,
type: BookSideType,
): PerpOrder {
const side = const side =
type == BookSideType.bids ? PerpOrderSide.bid : PerpOrderSide.ask; type == BookSideType.bids ? PerpOrderSide.bid : PerpOrderSide.ask;
const price = BookSide.getPriceFromKey(leafNode.key); 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 BN from 'bn.js';
import { MangoClient } from '../client'; import { MangoClient } from '../client';
import { SERUM3_PROGRAM_ID } from '../constants'; import { SERUM3_PROGRAM_ID } from '../constants';
import { As } from '../utils';
import { TokenIndex } from './bank';
import { Group } from './group'; import { Group } from './group';
import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from './I80F48'; import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from './I80F48';
export type MarketIndex = number & As<'market-index'>;
export class Serum3Market { export class Serum3Market {
public name: string; public name: string;
static from( static from(
@ -26,12 +30,12 @@ export class Serum3Market {
return new Serum3Market( return new Serum3Market(
publicKey, publicKey,
obj.group, obj.group,
obj.baseTokenIndex, obj.baseTokenIndex as TokenIndex,
obj.quoteTokenIndex, obj.quoteTokenIndex as TokenIndex,
obj.name, obj.name,
obj.serumProgram, obj.serumProgram,
obj.serumMarketExternal, obj.serumMarketExternal,
obj.marketIndex, obj.marketIndex as MarketIndex,
obj.registrationTime, obj.registrationTime,
); );
} }
@ -39,12 +43,12 @@ export class Serum3Market {
constructor( constructor(
public publicKey: PublicKey, public publicKey: PublicKey,
public group: PublicKey, public group: PublicKey,
public baseTokenIndex: number, public baseTokenIndex: TokenIndex,
public quoteTokenIndex: number, public quoteTokenIndex: TokenIndex,
name: number[], name: number[],
public serumProgram: PublicKey, public serumProgram: PublicKey,
public serumMarketExternal: PublicKey, public serumMarketExternal: PublicKey,
public marketIndex: number, public marketIndex: MarketIndex,
public registrationTime: BN, public registrationTime: BN,
) { ) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
@ -58,19 +62,7 @@ export class Serum3Market {
*/ */
maxBidLeverage(group: Group): number { maxBidLeverage(group: Group): number {
const baseBank = group.getFirstBankByTokenIndex(this.baseTokenIndex); 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); const quoteBank = group.getFirstBankByTokenIndex(this.quoteTokenIndex);
if (!quoteBank) {
throw new Error(
`bank for quote token with index ${this.quoteTokenIndex} not found`,
);
}
if ( if (
quoteBank.initLiabWeight.sub(baseBank.initAssetWeight).lte(ZERO_I80F48()) quoteBank.initLiabWeight.sub(baseBank.initAssetWeight).lte(ZERO_I80F48())
) { ) {
@ -90,18 +82,7 @@ export class Serum3Market {
*/ */
maxAskLeverage(group: Group): number { maxAskLeverage(group: Group): number {
const baseBank = group.getFirstBankByTokenIndex(this.baseTokenIndex); 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); const quoteBank = group.getFirstBankByTokenIndex(this.quoteTokenIndex);
if (!quoteBank) {
throw new Error(
`bank for quote token with index ${this.quoteTokenIndex} not found`,
);
}
if ( if (
baseBank.initLiabWeight.sub(quoteBank.initAssetWeight).lte(ZERO_I80F48()) baseBank.initLiabWeight.sub(quoteBank.initAssetWeight).lte(ZERO_I80F48())
@ -115,28 +96,18 @@ export class Serum3Market {
} }
public async loadBids(client: MangoClient, group: Group): Promise<Orderbook> { public async loadBids(client: MangoClient, group: Group): Promise<Orderbook> {
const serum3MarketExternal = group.serum3MarketExternalsMap.get( const serum3MarketExternal = group.getSerum3ExternalMarket(
this.serumMarketExternal.toBase58(), this.serumMarketExternal,
); );
if (!serum3MarketExternal) {
throw new Error(
`Unable to find serum3MarketExternal for ${this.serumMarketExternal.toBase58()}`,
);
}
return await serum3MarketExternal.loadBids( return await serum3MarketExternal.loadBids(
client.program.provider.connection, client.program.provider.connection,
); );
} }
public async loadAsks(client: MangoClient, group: Group): Promise<Orderbook> { public async loadAsks(client: MangoClient, group: Group): Promise<Orderbook> {
const serum3MarketExternal = group.serum3MarketExternalsMap.get( const serum3MarketExternal = group.getSerum3ExternalMarket(
this.serumMarketExternal.toBase58(), this.serumMarketExternal,
); );
if (!serum3MarketExternal) {
throw new Error(
`Unable to find serum3MarketExternal for ${this.serumMarketExternal.toBase58()}`,
);
}
return await serum3MarketExternal.loadAsks( return await serum3MarketExternal.loadAsks(
client.program.provider.connection, client.program.provider.connection,
); );

View File

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

View File

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

View File

@ -51,7 +51,7 @@ export class Id {
static fromIdsByName(name: string): Id { static fromIdsByName(name: string): Id {
const groupConfig = ids.groups.find((id) => id['name'] === name); 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( return new Id(
groupConfig.cluster as Cluster, groupConfig.cluster as Cluster,
groupConfig.name, groupConfig.name,
@ -71,7 +71,7 @@ export class Id {
(id) => id['publicKey'] === groupPk.toString(), (id) => id['publicKey'] === groupPk.toString(),
); );
if (!groupConfig) 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( return new Id(
groupConfig.cluster as Cluster, groupConfig.cluster as Cluster,
groupConfig.name, groupConfig.name,

View File

@ -567,7 +567,6 @@ async function main() {
// ); // );
// console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`); // 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`); console.log(`ALT: extending manually with bank publick keys and oracles`);
const extendIx = AddressLookupTableProgram.extendLookupTable({ const extendIx = AddressLookupTableProgram.extendLookupTable({
lookupTable: createIx[1], lookupTable: createIx[1],

View File

@ -78,10 +78,10 @@ async function main() {
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString(group)); console.log(mangoAccount.toString(group));
await mangoAccount.reload(client, group); await mangoAccount.reload(client);
// set delegate, and change name // set delegate, and change name
if (false) { if (true) {
console.log(`...changing mango account name, and setting a delegate`); console.log(`...changing mango account name, and setting a delegate`);
const randomKey = new PublicKey( const randomKey = new PublicKey(
'4ZkS7ZZkxfsC3GtvvsHP3DFcUeByU9zzZELS4r8HCELo', '4ZkS7ZZkxfsC3GtvvsHP3DFcUeByU9zzZELS4r8HCELo',
@ -93,7 +93,7 @@ async function main() {
'my_changed_name', 'my_changed_name',
randomKey, randomKey,
); );
await mangoAccount.reload(client, group); await mangoAccount.reload(client);
console.log(mangoAccount.toString()); console.log(mangoAccount.toString());
console.log(`...resetting mango account name, and re-setting a delegate`); console.log(`...resetting mango account name, and re-setting a delegate`);
@ -103,7 +103,7 @@ async function main() {
'my_mango_account', 'my_mango_account',
PublicKey.default, PublicKey.default,
); );
await mangoAccount.reload(client, group); await mangoAccount.reload(client);
console.log(mangoAccount.toString()); console.log(mangoAccount.toString());
} }
@ -113,7 +113,7 @@ async function main() {
`...expanding mango account to have serum3 and perp position slots`, `...expanding mango account to have serum3 and perp position slots`,
); );
await client.expandMangoAccount(group, mangoAccount, 8, 8, 8, 8); await client.expandMangoAccount(group, mangoAccount, 8, 8, 8, 8);
await mangoAccount.reload(client, group); await mangoAccount.reload(client);
} }
// deposit and withdraw // deposit and withdraw
@ -126,7 +126,7 @@ async function main() {
new PublicKey(DEVNET_MINTS.get('USDC')!), new PublicKey(DEVNET_MINTS.get('USDC')!),
50, 50,
); );
await mangoAccount.reload(client, group); await mangoAccount.reload(client);
await client.tokenDeposit( await client.tokenDeposit(
group, group,
@ -134,7 +134,7 @@ async function main() {
new PublicKey(DEVNET_MINTS.get('SOL')!), new PublicKey(DEVNET_MINTS.get('SOL')!),
1, 1,
); );
await mangoAccount.reload(client, group); await mangoAccount.reload(client);
await client.tokenDeposit( await client.tokenDeposit(
group, group,
@ -142,7 +142,7 @@ async function main() {
new PublicKey(DEVNET_MINTS.get('MNGO')!), new PublicKey(DEVNET_MINTS.get('MNGO')!),
1, 1,
); );
await mangoAccount.reload(client, group); await mangoAccount.reload(client);
console.log(`...withdrawing 1 USDC`); console.log(`...withdrawing 1 USDC`);
await client.tokenWithdraw( await client.tokenWithdraw(
@ -152,7 +152,7 @@ async function main() {
1, 1,
true, true,
); );
await mangoAccount.reload(client, group); await mangoAccount.reload(client);
console.log(`...depositing 0.0005 BTC`); console.log(`...depositing 0.0005 BTC`);
await client.tokenDeposit( await client.tokenDeposit(
@ -161,7 +161,7 @@ async function main() {
new PublicKey(DEVNET_MINTS.get('BTC')!), new PublicKey(DEVNET_MINTS.get('BTC')!),
0.0005, 0.0005,
); );
await mangoAccount.reload(client, group); await mangoAccount.reload(client);
console.log(mangoAccount.toString(group)); console.log(mangoAccount.toString(group));
} catch (error) { } catch (error) {
@ -171,12 +171,6 @@ async function main() {
if (true) { if (true) {
// serum3 // 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( const asks = await group.loadSerum3AsksForMarket(
client, client,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
@ -205,7 +199,7 @@ async function main() {
Date.now(), Date.now(),
10, 10,
); );
await mangoAccount.reload(client, group); await mangoAccount.reload(client);
price = lowestAsk.price + lowestAsk.price / 2; price = lowestAsk.price + lowestAsk.price / 2;
qty = 0.0001; qty = 0.0001;
@ -224,7 +218,7 @@ async function main() {
Date.now(), Date.now(),
10, 10,
); );
await mangoAccount.reload(client, group); await mangoAccount.reload(client);
price = highestBid.price - highestBid.price / 2; price = highestBid.price - highestBid.price / 2;
qty = 0.0001; qty = 0.0001;
@ -291,33 +285,27 @@ async function main() {
} }
if (true) { if (true) {
await mangoAccount.reload(client, group); await mangoAccount.reload(client);
console.log( console.log(
'...mangoAccount.getEquity() ' + '...mangoAccount.getEquity() ' +
toUiDecimalsForQuote(mangoAccount.getEquity()!.toNumber()), toUiDecimalsForQuote(mangoAccount.getEquity(group)!.toNumber()),
); );
console.log( console.log(
'...mangoAccount.getCollateralValue() ' + '...mangoAccount.getCollateralValue() ' +
toUiDecimalsForQuote(mangoAccount.getCollateralValue()!.toNumber()),
);
console.log(
'...mangoAccount.accountData["healthCache"].health(HealthType.init) ' +
toUiDecimalsForQuote( toUiDecimalsForQuote(
mangoAccount mangoAccount.getCollateralValue(group)!.toNumber(),
.accountData!['healthCache'].health(HealthType.init)
.toNumber(),
), ),
); );
console.log( console.log(
'...mangoAccount.getAssetsVal() ' + '...mangoAccount.getAssetsVal() ' +
toUiDecimalsForQuote( toUiDecimalsForQuote(
mangoAccount.getAssetsValue(HealthType.init)!.toNumber(), mangoAccount.getAssetsValue(group, HealthType.init)!.toNumber(),
), ),
); );
console.log( console.log(
'...mangoAccount.getLiabsVal() ' + '...mangoAccount.getLiabsVal() ' +
toUiDecimalsForQuote( toUiDecimalsForQuote(
mangoAccount.getLiabsValue(HealthType.init)!.toNumber(), mangoAccount.getLiabsValue(group, HealthType.init)!.toNumber(),
), ),
); );
console.log( console.log(
@ -400,10 +388,11 @@ async function main() {
// perps // perps
if (true) { if (true) {
let sig; let sig;
const perpMarket = group.getPerpMarketByName('BTC-PERP');
const orders = await mangoAccount.loadPerpOpenOrdersForMarket( const orders = await mangoAccount.loadPerpOpenOrdersForMarket(
client, client,
group, group,
'BTC-PERP', perpMarket.perpMarketIndex,
); );
for (const order of orders) { for (const order of orders) {
console.log( console.log(
@ -411,7 +400,12 @@ async function main() {
); );
} }
console.log(`...cancelling all perp orders`); console.log(`...cancelling all perp orders`);
sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10); sig = await client.perpCancelAllOrders(
group,
mangoAccount,
perpMarket.perpMarketIndex,
10,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// scenario 1 // scenario 1
@ -423,7 +417,7 @@ async function main() {
Math.floor(Math.random() * 100); Math.floor(Math.random() * 100);
const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi( const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi(
group, group,
'BTC-PERP', perpMarket.perpMarketIndex,
1, 1,
); );
const baseQty = quoteQty / price; const baseQty = quoteQty / price;
@ -433,7 +427,7 @@ async function main() {
const sig = await client.perpPlaceOrder( const sig = await client.perpPlaceOrder(
group, group,
mangoAccount, mangoAccount,
'BTC-PERP', perpMarket.perpMarketIndex,
PerpOrderSide.bid, PerpOrderSide.bid,
price, price,
baseQty, baseQty,
@ -448,7 +442,12 @@ async function main() {
console.log(error); console.log(error);
} }
console.log(`...cancelling all perp orders`); console.log(`...cancelling all perp orders`);
sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10); sig = await client.perpCancelAllOrders(
group,
mangoAccount,
perpMarket.perpMarketIndex,
10,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// bid max perp + some // bid max perp + some
@ -458,7 +457,11 @@ async function main() {
group.banksMapByName.get('BTC')![0].uiPrice! - group.banksMapByName.get('BTC')![0].uiPrice! -
Math.floor(Math.random() * 100); Math.floor(Math.random() * 100);
const quoteQty = const quoteQty =
mangoAccount.getMaxQuoteForPerpBidUi(group, 'BTC-PERP', 1) * 1.02; mangoAccount.getMaxQuoteForPerpBidUi(
group,
perpMarket.perpMarketIndex,
1,
) * 1.02;
const baseQty = quoteQty / price; const baseQty = quoteQty / price;
console.log( console.log(
`...placing max qty * 1.02 perp bid clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, `...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( const sig = await client.perpPlaceOrder(
group, group,
mangoAccount, mangoAccount,
'BTC-PERP', perpMarket.perpMarketIndex,
PerpOrderSide.bid, PerpOrderSide.bid,
price, price,
baseQty, baseQty,
@ -487,7 +490,11 @@ async function main() {
const price = const price =
group.banksMapByName.get('BTC')![0].uiPrice! + group.banksMapByName.get('BTC')![0].uiPrice! +
Math.floor(Math.random() * 100); Math.floor(Math.random() * 100);
const baseQty = mangoAccount.getMaxBaseForPerpAskUi(group, 'BTC-PERP', 1); const baseQty = mangoAccount.getMaxBaseForPerpAskUi(
group,
perpMarket.perpMarketIndex,
1,
);
const quoteQty = baseQty * price; const quoteQty = baseQty * price;
console.log( console.log(
`...placing max qty perp ask clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, `...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( const sig = await client.perpPlaceOrder(
group, group,
mangoAccount, mangoAccount,
'BTC-PERP', perpMarket.perpMarketIndex,
PerpOrderSide.ask, PerpOrderSide.ask,
price, price,
baseQty, baseQty,
@ -517,7 +524,11 @@ async function main() {
group.banksMapByName.get('BTC')![0].uiPrice! + group.banksMapByName.get('BTC')![0].uiPrice! +
Math.floor(Math.random() * 100); Math.floor(Math.random() * 100);
const baseQty = const baseQty =
mangoAccount.getMaxBaseForPerpAskUi(group, 'BTC-PERP', 1) * 1.02; mangoAccount.getMaxBaseForPerpAskUi(
group,
perpMarket.perpMarketIndex,
1,
) * 1.02;
const quoteQty = baseQty * price; const quoteQty = baseQty * price;
console.log( console.log(
`...placing max qty perp ask * 1.02 clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, `...placing max qty perp ask * 1.02 clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
@ -525,7 +536,7 @@ async function main() {
const sig = await client.perpPlaceOrder( const sig = await client.perpPlaceOrder(
group, group,
mangoAccount, mangoAccount,
'BTC-PERP', perpMarket.perpMarketIndex,
PerpOrderSide.ask, PerpOrderSide.ask,
price, price,
baseQty, baseQty,
@ -541,7 +552,12 @@ async function main() {
} }
console.log(`...cancelling all perp orders`); console.log(`...cancelling all perp orders`);
sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10); sig = await client.perpCancelAllOrders(
group,
mangoAccount,
perpMarket.perpMarketIndex,
10,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// // scenario 2 // // scenario 2
@ -553,8 +569,8 @@ async function main() {
// const sig = await client.perpPlaceOrder( // const sig = await client.perpPlaceOrder(
// group, // group,
// mangoAccount, // mangoAccount,
// 'BTC-PERP', // perpMarket.perpMarketIndex,
// PerpOrderSide.bid, // PerpOrderSide.bid,
// price, // price,
// 0.01, // 0.01,
// price * 0.01, // price * 0.01,
@ -574,8 +590,8 @@ async function main() {
// const sig = await client.perpPlaceOrder( // const sig = await client.perpPlaceOrder(
// group, // group,
// mangoAccount, // mangoAccount,
// 'BTC-PERP', // perpMarket.perpMarketIndex,
// PerpOrderSide.ask, // PerpOrderSide.ask,
// price, // price,
// 0.01, // 0.01,
// price * 0.011, // price * 0.011,
@ -590,11 +606,9 @@ async function main() {
// } // }
// // // should be able to cancel them : know bug // // // should be able to cancel them : know bug
// // console.log(`...cancelling all perp orders`); // // console.log(`...cancelling all perp orders`);
// // sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10); // // sig = await client.perpCancelAllOrders(group, mangoAccount, perpMarket.perpMarketIndex, 10);
// // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); // // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
const perpMarket = group.perpMarketsMap.get('BTC-PERP')!;
const bids: BookSide = await perpMarket?.loadBids(client)!; const bids: BookSide = await perpMarket?.loadBids(client)!;
console.log(`bids - ${Array.from(bids.items())}`); console.log(`bids - ${Array.from(bids.items())}`);
const asks: BookSide = await perpMarket?.loadAsks(client)!; 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 // 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 group.reloadAll(client);
await mangoAccount.reload(client, group); await mangoAccount.reload(client);
console.log(`${mangoAccount.toString(group)}`); 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 U64_MAX_BN = new BN('18446744073709551615');
export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64); 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) { for (const am of ams) {
console.log( console.log(
`${am.pubkey.toBase58()}, isSigner: ${am.isSigner `${am.pubkey.toBase58()}, isSigner: ${am.isSigner
@ -39,7 +45,7 @@ export function debugHealthAccounts(
group: Group, group: Group,
mangoAccount: MangoAccount, mangoAccount: MangoAccount,
publicKeys: PublicKey[], publicKeys: PublicKey[],
) { ): void {
const banks = new Map( const banks = new Map(
Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [ Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [
banks[0].publicKey.toBase58(), banks[0].publicKey.toBase58(),
@ -66,10 +72,12 @@ export function debugHealthAccounts(
}), }),
); );
const perps = new Map( const perps = new Map(
Array.from(group.perpMarketsMap.values()).map((perpMarket: PerpMarket) => [ Array.from(group.perpMarketsMapByName.values()).map(
perpMarket.publicKey.toBase58(), (perpMarket: PerpMarket) => [
`${perpMarket.name} perp market`, perpMarket.publicKey.toBase58(),
]), `${perpMarket.name} perp market`,
],
),
); );
publicKeys.map((pk) => { publicKeys.map((pk) => {
@ -126,7 +134,7 @@ export async function getAssociatedTokenAddress(
associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID, associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID,
): Promise<PublicKey> { ): Promise<PublicKey> {
if (!allowOwnerOffCurve && !PublicKey.isOnCurve(owner.toBuffer())) if (!allowOwnerOffCurve && !PublicKey.isOnCurve(owner.toBuffer()))
throw new Error('TokenOwnerOffCurve'); throw new Error('TokenOwnerOffCurve!');
const [address] = await PublicKey.findProgramAddress( const [address] = await PublicKey.findProgramAddress(
[owner.toBuffer(), programId.toBuffer(), mint.toBuffer()], [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[], ixs: TransactionInstruction[],
alts: AddressLookupTableAccount[], alts: AddressLookupTableAccount[],
opts: any = {}, opts: any = {},
) { ): Promise<string> {
const connection = provider.connection; const connection = provider.connection;
const latestBlockhash = await connection.getLatestBlockhash( const latestBlockhash = await connection.getLatestBlockhash(
opts.preflightCommitment, opts.preflightCommitment,