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:
parent
7e180c7b3a
commit
c22302a1da
|
@ -14,12 +14,21 @@
|
|||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"semi": ["error", "always"],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"@typescript-eslint/no-non-null-assertion": 0,
|
||||
"@typescript-eslint/ban-ts-comment": 0,
|
||||
"@typescript-eslint/no-explicit-any": 0
|
||||
"@typescript-eslint/no-explicit-any": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": "warn"
|
||||
}
|
||||
}
|
|
@ -1,17 +1,19 @@
|
|||
import { BN } from '@project-serum/anchor';
|
||||
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import { nativeI80F48ToUi } from '../utils';
|
||||
import { As, nativeI80F48ToUi } from '../utils';
|
||||
import { I80F48, I80F48Dto, ZERO_I80F48 } from './I80F48';
|
||||
|
||||
export const QUOTE_DECIMALS = 6;
|
||||
|
||||
export type TokenIndex = number & As<'token-index'>;
|
||||
|
||||
export type OracleConfig = {
|
||||
confFilter: I80F48Dto;
|
||||
};
|
||||
|
||||
export interface BankForHealth {
|
||||
tokenIndex: number;
|
||||
tokenIndex: TokenIndex;
|
||||
maintAssetWeight: I80F48;
|
||||
initAssetWeight: I80F48;
|
||||
maintLiabWeight: I80F48;
|
||||
|
@ -85,7 +87,7 @@ export class Bank implements BankForHealth {
|
|||
mintDecimals: number;
|
||||
bankNum: number;
|
||||
},
|
||||
) {
|
||||
): Bank {
|
||||
return new Bank(
|
||||
publicKey,
|
||||
obj.name,
|
||||
|
@ -120,7 +122,7 @@ export class Bank implements BankForHealth {
|
|||
obj.dust,
|
||||
obj.flashLoanTokenAccountInitial,
|
||||
obj.flashLoanApprovedAmount,
|
||||
obj.tokenIndex,
|
||||
obj.tokenIndex as TokenIndex,
|
||||
obj.mintDecimals,
|
||||
obj.bankNum,
|
||||
);
|
||||
|
@ -160,7 +162,7 @@ export class Bank implements BankForHealth {
|
|||
dust: I80F48Dto,
|
||||
flashLoanTokenAccountInitial: BN,
|
||||
flashLoanApprovedAmount: BN,
|
||||
public tokenIndex: number,
|
||||
public tokenIndex: TokenIndex,
|
||||
public mintDecimals: number,
|
||||
public bankNum: number,
|
||||
) {
|
||||
|
@ -207,9 +209,9 @@ export class Bank implements BankForHealth {
|
|||
'\n oracle - ' +
|
||||
this.oracle.toBase58() +
|
||||
'\n price - ' +
|
||||
this.price?.toNumber() +
|
||||
this._price?.toNumber() +
|
||||
'\n uiPrice - ' +
|
||||
this.uiPrice +
|
||||
this._uiPrice +
|
||||
'\n deposit index - ' +
|
||||
this.depositIndex.toNumber() +
|
||||
'\n borrow index - ' +
|
||||
|
@ -268,7 +270,7 @@ export class Bank implements BankForHealth {
|
|||
get price(): I80F48 {
|
||||
if (!this._price) {
|
||||
throw new Error(
|
||||
`Undefined price for bank ${this.publicKey}, tokenIndex ${this.tokenIndex}`,
|
||||
`Undefined price for bank ${this.publicKey} with tokenIndex ${this.tokenIndex}!`,
|
||||
);
|
||||
}
|
||||
return this._price;
|
||||
|
@ -277,7 +279,7 @@ export class Bank implements BankForHealth {
|
|||
get uiPrice(): number {
|
||||
if (!this._uiPrice) {
|
||||
throw new Error(
|
||||
`Undefined uiPrice for bank ${this.publicKey}, tokenIndex ${this.tokenIndex}`,
|
||||
`Undefined uiPrice for bank ${this.publicKey} with tokenIndex ${this.tokenIndex}!`,
|
||||
);
|
||||
}
|
||||
return this._uiPrice;
|
||||
|
@ -388,11 +390,11 @@ export class MintInfo {
|
|||
registrationTime: BN;
|
||||
groupInsuranceFund: number;
|
||||
},
|
||||
) {
|
||||
): MintInfo {
|
||||
return new MintInfo(
|
||||
publicKey,
|
||||
obj.group,
|
||||
obj.tokenIndex,
|
||||
obj.tokenIndex as TokenIndex,
|
||||
obj.mint,
|
||||
obj.banks,
|
||||
obj.vaults,
|
||||
|
@ -405,7 +407,7 @@ export class MintInfo {
|
|||
constructor(
|
||||
public publicKey: PublicKey,
|
||||
public group: PublicKey,
|
||||
public tokenIndex: number,
|
||||
public tokenIndex: TokenIndex,
|
||||
public mint: PublicKey,
|
||||
public banks: PublicKey[],
|
||||
public vaults: PublicKey[],
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
Market,
|
||||
Orderbook,
|
||||
} from '@project-serum/serum';
|
||||
import { parsePriceData, PriceData } from '@pythnetwork/client';
|
||||
import { parsePriceData } from '@pythnetwork/client';
|
||||
import {
|
||||
AccountInfo,
|
||||
AddressLookupTableAccount,
|
||||
|
@ -17,15 +17,15 @@ import { MangoClient } from '../client';
|
|||
import { SERUM3_PROGRAM_ID } from '../constants';
|
||||
import { Id } from '../ids';
|
||||
import { toNativeDecimals, toUiDecimals } from '../utils';
|
||||
import { Bank, MintInfo } from './bank';
|
||||
import { Bank, MintInfo, TokenIndex } from './bank';
|
||||
import { I80F48, ONE_I80F48 } from './I80F48';
|
||||
import {
|
||||
isPythOracle,
|
||||
isSwitchboardOracle,
|
||||
parseSwitchboardOracle,
|
||||
} from './oracle';
|
||||
import { BookSide, PerpMarket } from './perp';
|
||||
import { Serum3Market } from './serum3';
|
||||
import { BookSide, PerpMarket, PerpMarketIndex } from './perp';
|
||||
import { MarketIndex, Serum3Market } from './serum3';
|
||||
|
||||
export class Group {
|
||||
static from(
|
||||
|
@ -57,12 +57,14 @@ export class Group {
|
|||
new Map(), // banksMapByName
|
||||
new Map(), // banksMapByMint
|
||||
new Map(), // banksMapByTokenIndex
|
||||
new Map(), // serum3MarketsMap
|
||||
new Map(), // serum3MarketsMapByExternal
|
||||
new Map(), // serum3MarketsMapByMarketIndex
|
||||
new Map(), // serum3MarketExternalsMap
|
||||
new Map(), // perpMarketsMap
|
||||
new Map(), // perpMarketsMapByOracle
|
||||
new Map(), // perpMarketsMapByMarketIndex
|
||||
new Map(), // perpMarketsMapByName
|
||||
new Map(), // mintInfosMapByTokenIndex
|
||||
new Map(), // mintInfosMapByMint
|
||||
new Map(), // oraclesMap
|
||||
new Map(), // vaultAmountsMap
|
||||
);
|
||||
}
|
||||
|
@ -81,18 +83,19 @@ export class Group {
|
|||
public addressLookupTablesList: AddressLookupTableAccount[],
|
||||
public banksMapByName: Map<string, Bank[]>,
|
||||
public banksMapByMint: Map<string, Bank[]>,
|
||||
public banksMapByTokenIndex: Map<number, Bank[]>,
|
||||
public banksMapByTokenIndex: Map<TokenIndex, Bank[]>,
|
||||
public serum3MarketsMapByExternal: Map<string, Serum3Market>,
|
||||
public serum3MarketExternalsMap: Map<string, Market>,
|
||||
// TODO rethink key
|
||||
public perpMarketsMap: Map<string, PerpMarket>,
|
||||
public mintInfosMapByTokenIndex: Map<number, MintInfo>,
|
||||
public serum3MarketsMapByMarketIndex: Map<MarketIndex, Serum3Market>,
|
||||
public serum3ExternalMarketsMap: Map<string, Market>,
|
||||
public perpMarketsMapByOracle: Map<string, PerpMarket>,
|
||||
public perpMarketsMapByMarketIndex: Map<PerpMarketIndex, PerpMarket>,
|
||||
public perpMarketsMapByName: Map<string, PerpMarket>,
|
||||
public mintInfosMapByTokenIndex: Map<TokenIndex, MintInfo>,
|
||||
public mintInfosMapByMint: Map<string, MintInfo>,
|
||||
private oraclesMap: Map<string, PriceData>, // UNUSED
|
||||
public vaultAmountsMap: Map<string, number>,
|
||||
) {}
|
||||
|
||||
public async reloadAll(client: MangoClient) {
|
||||
public async reloadAll(client: MangoClient): Promise<void> {
|
||||
let ids: Id | undefined = undefined;
|
||||
|
||||
if (client.idsSource === 'api') {
|
||||
|
@ -109,12 +112,12 @@ export class Group {
|
|||
this.reloadBanks(client, ids).then(() =>
|
||||
Promise.all([
|
||||
this.reloadBankOraclePrices(client),
|
||||
this.reloadVaults(client, ids),
|
||||
this.reloadVaults(client),
|
||||
]),
|
||||
),
|
||||
this.reloadMintInfos(client, ids),
|
||||
this.reloadSerum3Markets(client, ids).then(() =>
|
||||
this.reloadSerum3ExternalMarkets(client, ids),
|
||||
this.reloadSerum3ExternalMarkets(client),
|
||||
),
|
||||
this.reloadPerpMarkets(client, ids).then(() =>
|
||||
this.reloadPerpMarketOraclePrices(client),
|
||||
|
@ -123,7 +126,7 @@ export class Group {
|
|||
// console.timeEnd('group.reload');
|
||||
}
|
||||
|
||||
public async reloadAlts(client: MangoClient) {
|
||||
public async reloadAlts(client: MangoClient): Promise<void> {
|
||||
const alts = await Promise.all(
|
||||
this.addressLookupTables
|
||||
.filter((alt) => !alt.equals(PublicKey.default))
|
||||
|
@ -133,13 +136,13 @@ export class Group {
|
|||
);
|
||||
this.addressLookupTablesList = alts.map((res, i) => {
|
||||
if (!res || !res.value) {
|
||||
throw new Error(`Error in getting ALT ${this.addressLookupTables[i]}`);
|
||||
throw new Error(`Undefined ALT ${this.addressLookupTables[i]}!`);
|
||||
}
|
||||
return res.value;
|
||||
});
|
||||
}
|
||||
|
||||
public async reloadBanks(client: MangoClient, ids?: Id) {
|
||||
public async reloadBanks(client: MangoClient, ids?: Id): Promise<void> {
|
||||
let banks: Bank[];
|
||||
|
||||
if (ids && ids.getBanks().length) {
|
||||
|
@ -169,7 +172,7 @@ export class Group {
|
|||
}
|
||||
}
|
||||
|
||||
public async reloadMintInfos(client: MangoClient, ids?: Id) {
|
||||
public async reloadMintInfos(client: MangoClient, ids?: Id): Promise<void> {
|
||||
let mintInfos: MintInfo[];
|
||||
if (ids && ids.getMintInfos().length) {
|
||||
mintInfos = (
|
||||
|
@ -194,7 +197,10 @@ export class Group {
|
|||
);
|
||||
}
|
||||
|
||||
public async reloadSerum3Markets(client: MangoClient, ids?: Id) {
|
||||
public async reloadSerum3Markets(
|
||||
client: MangoClient,
|
||||
ids?: Id,
|
||||
): Promise<void> {
|
||||
let serum3Markets: Serum3Market[];
|
||||
if (ids && ids.getSerum3Markets().length) {
|
||||
serum3Markets = (
|
||||
|
@ -214,9 +220,15 @@ export class Group {
|
|||
serum3Market,
|
||||
]),
|
||||
);
|
||||
this.serum3MarketsMapByMarketIndex = new Map(
|
||||
serum3Markets.map((serum3Market) => [
|
||||
serum3Market.marketIndex,
|
||||
serum3Market,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
public async reloadSerum3ExternalMarkets(client: MangoClient, ids?: Id) {
|
||||
public async reloadSerum3ExternalMarkets(client: MangoClient): Promise<void> {
|
||||
const externalMarkets = await Promise.all(
|
||||
Array.from(this.serum3MarketsMapByExternal.values()).map((serum3Market) =>
|
||||
Market.load(
|
||||
|
@ -228,7 +240,7 @@ export class Group {
|
|||
),
|
||||
);
|
||||
|
||||
this.serum3MarketExternalsMap = new Map(
|
||||
this.serum3ExternalMarketsMap = new Map(
|
||||
Array.from(this.serum3MarketsMapByExternal.values()).map(
|
||||
(serum3Market, index) => [
|
||||
serum3Market.serumMarketExternal.toBase58(),
|
||||
|
@ -238,7 +250,7 @@ export class Group {
|
|||
);
|
||||
}
|
||||
|
||||
public async reloadPerpMarkets(client: MangoClient, ids?: Id) {
|
||||
public async reloadPerpMarkets(client: MangoClient, ids?: Id): Promise<void> {
|
||||
let perpMarkets: PerpMarket[];
|
||||
if (ids && ids.getPerpMarkets().length) {
|
||||
perpMarkets = (
|
||||
|
@ -252,9 +264,18 @@ export class Group {
|
|||
perpMarkets = await client.perpGetMarkets(this);
|
||||
}
|
||||
|
||||
this.perpMarketsMap = new Map(
|
||||
this.perpMarketsMapByName = new Map(
|
||||
perpMarkets.map((perpMarket) => [perpMarket.name, perpMarket]),
|
||||
);
|
||||
this.perpMarketsMapByOracle = new Map(
|
||||
perpMarkets.map((perpMarket) => [
|
||||
perpMarket.oracle.toBase58(),
|
||||
perpMarket,
|
||||
]),
|
||||
);
|
||||
this.perpMarketsMapByMarketIndex = new Map(
|
||||
perpMarkets.map((perpMarket) => [perpMarket.perpMarketIndex, perpMarket]),
|
||||
);
|
||||
}
|
||||
|
||||
public async reloadBankOraclePrices(client: MangoClient): Promise<void> {
|
||||
|
@ -293,7 +314,9 @@ export class Group {
|
|||
public async reloadPerpMarketOraclePrices(
|
||||
client: MangoClient,
|
||||
): Promise<void> {
|
||||
const perpMarkets: PerpMarket[] = Array.from(this.perpMarketsMap.values());
|
||||
const perpMarkets: PerpMarket[] = Array.from(
|
||||
this.perpMarketsMapByName.values(),
|
||||
);
|
||||
const oracles = perpMarkets.map((b) => b.oracle);
|
||||
const ais =
|
||||
await client.program.provider.connection.getMultipleAccountsInfo(oracles);
|
||||
|
@ -302,15 +325,17 @@ export class Group {
|
|||
ais.forEach(async (ai, i) => {
|
||||
const perpMarket = perpMarkets[i];
|
||||
if (!ai)
|
||||
throw new Error('Undefined ai object in reloadPerpMarketOraclePrices!');
|
||||
throw new Error(
|
||||
`Undefined ai object in reloadPerpMarketOraclePrices for ${perpMarket.oracle}!`,
|
||||
);
|
||||
const { price, uiPrice } = await this.decodePriceFromOracleAi(
|
||||
coder,
|
||||
perpMarket.oracle,
|
||||
ai,
|
||||
perpMarket.baseDecimals,
|
||||
);
|
||||
perpMarket.price = price;
|
||||
perpMarket.uiPrice = uiPrice;
|
||||
perpMarket._price = price;
|
||||
perpMarket._uiPrice = uiPrice;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -319,7 +344,7 @@ export class Group {
|
|||
oracle: PublicKey,
|
||||
ai: AccountInfo<Buffer>,
|
||||
baseDecimals: number,
|
||||
) {
|
||||
): Promise<{ price: I80F48; uiPrice: number }> {
|
||||
let price, uiPrice;
|
||||
if (
|
||||
!BorshAccountsCoder.accountDiscriminator('stubOracle').compare(
|
||||
|
@ -337,13 +362,13 @@ export class Group {
|
|||
price = this?.toNativePrice(uiPrice, baseDecimals);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unknown oracle provider for oracle ${oracle}, with owner ${ai.owner}`,
|
||||
`Unknown oracle provider (parsing not implemented) for oracle ${oracle}, with owner ${ai.owner}!`,
|
||||
);
|
||||
}
|
||||
return { price, uiPrice };
|
||||
}
|
||||
|
||||
public async reloadVaults(client: MangoClient, ids?: Id): Promise<void> {
|
||||
public async reloadVaults(client: MangoClient): Promise<void> {
|
||||
const vaultPks = Array.from(this.banksMapByMint.values())
|
||||
.flat()
|
||||
.map((bank) => bank.vault);
|
||||
|
@ -354,7 +379,9 @@ export class Group {
|
|||
|
||||
this.vaultAmountsMap = new Map(
|
||||
vaultAccounts.map((vaultAi, i) => {
|
||||
if (!vaultAi) throw new Error('Missing vault account info');
|
||||
if (!vaultAi) {
|
||||
throw new Error(`Undefined vaultAi for ${vaultPks[i]}`!);
|
||||
}
|
||||
const vaultAmount = coder()
|
||||
.accounts.decode('token', vaultAi.data)
|
||||
.amount.toNumber();
|
||||
|
@ -365,8 +392,7 @@ export class Group {
|
|||
|
||||
public getMintDecimals(mintPk: PublicKey): number {
|
||||
const banks = this.banksMapByMint.get(mintPk.toString());
|
||||
if (!banks)
|
||||
throw new Error(`Unable to find mint decimals for ${mintPk.toString()}`);
|
||||
if (!banks) throw new Error(`No bank found for mint ${mintPk}!`);
|
||||
return banks[0].mintDecimals;
|
||||
}
|
||||
|
||||
|
@ -376,14 +402,13 @@ export class Group {
|
|||
|
||||
public getFirstBankByMint(mintPk: PublicKey): Bank {
|
||||
const banks = this.banksMapByMint.get(mintPk.toString());
|
||||
if (!banks) throw new Error(`Unable to find bank for ${mintPk.toString()}`);
|
||||
if (!banks) throw new Error(`No bank found for mint ${mintPk}!`);
|
||||
return banks[0];
|
||||
}
|
||||
|
||||
public getFirstBankByTokenIndex(tokenIndex: number): Bank {
|
||||
public getFirstBankByTokenIndex(tokenIndex: TokenIndex): Bank {
|
||||
const banks = this.banksMapByTokenIndex.get(tokenIndex);
|
||||
if (!banks)
|
||||
throw new Error(`Unable to find banks for tokenIndex ${tokenIndex}`);
|
||||
if (!banks) throw new Error(`No bank found for tokenIndex ${tokenIndex}!`);
|
||||
return banks[0];
|
||||
}
|
||||
|
||||
|
@ -394,10 +419,7 @@ export class Group {
|
|||
*/
|
||||
public getTokenVaultBalanceByMint(mintPk: PublicKey): I80F48 {
|
||||
const banks = this.banksMapByMint.get(mintPk.toBase58());
|
||||
if (!banks)
|
||||
throw new Error(
|
||||
`Mint does not exist in getTokenVaultBalanceByMint ${mintPk.toString()}`,
|
||||
);
|
||||
if (!banks) throw new Error(`No bank found for mint ${mintPk}!`);
|
||||
let totalAmount = 0;
|
||||
for (const bank of banks) {
|
||||
const amount = this.vaultAmountsMap.get(bank.vault.toBase58());
|
||||
|
@ -408,83 +430,6 @@ export class Group {
|
|||
return I80F48.fromNumber(totalAmount);
|
||||
}
|
||||
|
||||
public findPerpMarket(marketIndex: number): PerpMarket | undefined {
|
||||
return Array.from(this.perpMarketsMap.values()).find(
|
||||
(perpMarket) => perpMarket.perpMarketIndex === marketIndex,
|
||||
);
|
||||
}
|
||||
|
||||
public findSerum3Market(marketIndex: number): Serum3Market | undefined {
|
||||
return Array.from(this.serum3MarketsMapByExternal.values()).find(
|
||||
(serum3Market) => serum3Market.marketIndex === marketIndex,
|
||||
);
|
||||
}
|
||||
|
||||
public findSerum3MarketByName(name: string): Serum3Market | undefined {
|
||||
return Array.from(this.serum3MarketsMapByExternal.values()).find(
|
||||
(serum3Market) => serum3Market.name === name,
|
||||
);
|
||||
}
|
||||
|
||||
public async loadSerum3BidsForMarket(
|
||||
client: MangoClient,
|
||||
externalMarketPk: PublicKey,
|
||||
): Promise<Orderbook> {
|
||||
const serum3Market = this.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
);
|
||||
if (!serum3Market) {
|
||||
throw new Error(
|
||||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
||||
);
|
||||
}
|
||||
return await serum3Market.loadBids(client, this);
|
||||
}
|
||||
|
||||
public async loadSerum3AsksForMarket(
|
||||
client: MangoClient,
|
||||
externalMarketPk: PublicKey,
|
||||
): Promise<Orderbook> {
|
||||
const serum3Market = this.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
);
|
||||
if (!serum3Market) {
|
||||
throw new Error(
|
||||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
||||
);
|
||||
}
|
||||
return await serum3Market.loadAsks(client, this);
|
||||
}
|
||||
|
||||
public getFeeRate(maker = true) {
|
||||
// TODO: fetch msrm/srm vault balance
|
||||
const feeTier = getFeeTier(0, 0);
|
||||
const rates = getFeeRates(feeTier);
|
||||
return maker ? rates.maker : rates.taker;
|
||||
}
|
||||
|
||||
public async loadPerpBidsForMarket(
|
||||
client: MangoClient,
|
||||
marketName: string,
|
||||
): Promise<BookSide> {
|
||||
const perpMarket = this.perpMarketsMap.get(marketName);
|
||||
if (!perpMarket) {
|
||||
throw new Error(`Perp Market ${marketName} not found!`);
|
||||
}
|
||||
return await perpMarket.loadBids(client);
|
||||
}
|
||||
|
||||
public async loadPerpAsksForMarket(
|
||||
client: MangoClient,
|
||||
marketName: string,
|
||||
): Promise<BookSide> {
|
||||
const perpMarket = this.perpMarketsMap.get(marketName);
|
||||
if (!perpMarket) {
|
||||
throw new Error(`Perp Market ${marketName} not found!`);
|
||||
}
|
||||
return await perpMarket.loadAsks(client);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param mintPk
|
||||
|
@ -497,7 +442,131 @@ export class Group {
|
|||
return toUiDecimals(vaultBalance, mintDecimals);
|
||||
}
|
||||
|
||||
public consoleLogBanks() {
|
||||
public getSerum3MarketByMarketIndex(marketIndex: MarketIndex): Serum3Market {
|
||||
const serum3Market = this.serum3MarketsMapByMarketIndex.get(marketIndex);
|
||||
if (!serum3Market) {
|
||||
throw new Error(`No serum3Market found for marketIndex ${marketIndex}!`);
|
||||
}
|
||||
return serum3Market;
|
||||
}
|
||||
|
||||
public getSerum3MarketByName(name: string): Serum3Market {
|
||||
const serum3Market = Array.from(
|
||||
this.serum3MarketsMapByExternal.values(),
|
||||
).find((serum3Market) => serum3Market.name === name);
|
||||
if (!serum3Market) {
|
||||
throw new Error(`No serum3Market found by name ${name}!`);
|
||||
}
|
||||
return serum3Market;
|
||||
}
|
||||
|
||||
public getSerum3MarketByExternalMarket(
|
||||
externalMarketPk: PublicKey,
|
||||
): Serum3Market {
|
||||
const serum3Market = Array.from(
|
||||
this.serum3MarketsMapByExternal.values(),
|
||||
).find((serum3Market) =>
|
||||
serum3Market.serumMarketExternal.equals(externalMarketPk),
|
||||
);
|
||||
if (!serum3Market) {
|
||||
throw new Error(
|
||||
`No serum3Market found for external serum3 market ${externalMarketPk.toString()}!`,
|
||||
);
|
||||
}
|
||||
return serum3Market;
|
||||
}
|
||||
|
||||
public getSerum3ExternalMarket(externalMarketPk: PublicKey): Market {
|
||||
const market = this.serum3ExternalMarketsMap.get(
|
||||
externalMarketPk.toBase58(),
|
||||
);
|
||||
if (!market) {
|
||||
throw new Error(
|
||||
`No external market found for pk ${externalMarketPk.toString()}!`,
|
||||
);
|
||||
}
|
||||
return market;
|
||||
}
|
||||
|
||||
public async loadSerum3BidsForMarket(
|
||||
client: MangoClient,
|
||||
externalMarketPk: PublicKey,
|
||||
): Promise<Orderbook> {
|
||||
const serum3Market = this.getSerum3MarketByExternalMarket(externalMarketPk);
|
||||
return await serum3Market.loadBids(client, this);
|
||||
}
|
||||
|
||||
public async loadSerum3AsksForMarket(
|
||||
client: MangoClient,
|
||||
externalMarketPk: PublicKey,
|
||||
): Promise<Orderbook> {
|
||||
const serum3Market = this.getSerum3MarketByExternalMarket(externalMarketPk);
|
||||
return await serum3Market.loadAsks(client, this);
|
||||
}
|
||||
|
||||
public getSerum3FeeRates(maker = true): number {
|
||||
// TODO: fetch msrm/srm vault balance
|
||||
const feeTier = getFeeTier(0, 0);
|
||||
const rates = getFeeRates(feeTier);
|
||||
return maker ? rates.maker : rates.taker;
|
||||
}
|
||||
|
||||
public findPerpMarket(marketIndex: PerpMarketIndex): PerpMarket {
|
||||
const perpMarket = Array.from(this.perpMarketsMapByName.values()).find(
|
||||
(perpMarket) => perpMarket.perpMarketIndex === marketIndex,
|
||||
);
|
||||
if (!perpMarket) {
|
||||
throw new Error(
|
||||
`No perpMarket found for perpMarketIndex ${marketIndex}!`,
|
||||
);
|
||||
}
|
||||
return perpMarket;
|
||||
}
|
||||
|
||||
public getPerpMarketByOracle(oracle: PublicKey): PerpMarket {
|
||||
const perpMarket = this.perpMarketsMapByOracle.get(oracle.toBase58());
|
||||
if (!perpMarket) {
|
||||
throw new Error(`No PerpMarket found for oracle ${oracle}!`);
|
||||
}
|
||||
return perpMarket;
|
||||
}
|
||||
|
||||
public getPerpMarketByMarketIndex(marketIndex: PerpMarketIndex): PerpMarket {
|
||||
const perpMarket = this.perpMarketsMapByMarketIndex.get(marketIndex);
|
||||
if (!perpMarket) {
|
||||
throw new Error(`No PerpMarket found with marketIndex ${marketIndex}!`);
|
||||
}
|
||||
return perpMarket;
|
||||
}
|
||||
|
||||
public getPerpMarketByName(perpMarketName: string): PerpMarket {
|
||||
const perpMarket = Array.from(
|
||||
this.perpMarketsMapByMarketIndex.values(),
|
||||
).find((perpMarket) => perpMarket.name === perpMarketName);
|
||||
if (!perpMarket) {
|
||||
throw new Error(`No PerpMarket found by name ${perpMarketName}!`);
|
||||
}
|
||||
return perpMarket;
|
||||
}
|
||||
|
||||
public async loadPerpBidsForMarket(
|
||||
client: MangoClient,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
): Promise<BookSide> {
|
||||
const perpMarket = this.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
return await perpMarket.loadBids(client);
|
||||
}
|
||||
|
||||
public async loadPerpAsksForMarket(
|
||||
client: MangoClient,
|
||||
group: Group,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
): Promise<BookSide> {
|
||||
const perpMarket = this.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
return await perpMarket.loadAsks(client);
|
||||
}
|
||||
|
||||
public consoleLogBanks(): void {
|
||||
for (const mintBanks of this.banksMapByMint.values()) {
|
||||
for (const bank of mintBanks) {
|
||||
console.log(bank.toString());
|
||||
|
|
|
@ -1,14 +1,371 @@
|
|||
import { BN } from '@project-serum/anchor';
|
||||
import { OpenOrders } from '@project-serum/serum';
|
||||
import { expect } from 'chai';
|
||||
import { toUiDecimalsForQuote } from '../utils';
|
||||
import { BankForHealth } from './bank';
|
||||
import { HealthCache, TokenInfo } from './healthCache';
|
||||
import { BankForHealth, TokenIndex } from './bank';
|
||||
import { HealthCache, PerpInfo, Serum3Info, TokenInfo } from './healthCache';
|
||||
import { I80F48, ZERO_I80F48 } from './I80F48';
|
||||
import { HealthType, PerpPosition } from './mangoAccount';
|
||||
import { PerpMarket } from './perp';
|
||||
import { MarketIndex } from './serum3';
|
||||
|
||||
function mockBankAndOracle(
|
||||
tokenIndex: TokenIndex,
|
||||
maintWeight: number,
|
||||
initWeight: number,
|
||||
price: number,
|
||||
): BankForHealth {
|
||||
return {
|
||||
tokenIndex,
|
||||
maintAssetWeight: I80F48.fromNumber(1 - maintWeight),
|
||||
initAssetWeight: I80F48.fromNumber(1 - initWeight),
|
||||
maintLiabWeight: I80F48.fromNumber(1 + maintWeight),
|
||||
initLiabWeight: I80F48.fromNumber(1 + initWeight),
|
||||
price: I80F48.fromNumber(price),
|
||||
};
|
||||
}
|
||||
|
||||
function mockPerpMarket(
|
||||
perpMarketIndex: number,
|
||||
maintWeight: number,
|
||||
initWeight: number,
|
||||
price: I80F48,
|
||||
): PerpMarket {
|
||||
return {
|
||||
perpMarketIndex,
|
||||
maintAssetWeight: I80F48.fromNumber(1 - maintWeight),
|
||||
initAssetWeight: I80F48.fromNumber(1 - initWeight),
|
||||
maintLiabWeight: I80F48.fromNumber(1 + maintWeight),
|
||||
initLiabWeight: I80F48.fromNumber(1 + initWeight),
|
||||
price,
|
||||
quoteLotSize: new BN(100),
|
||||
baseLotSize: new BN(10),
|
||||
longFunding: ZERO_I80F48(),
|
||||
shortFunding: ZERO_I80F48(),
|
||||
} as unknown as PerpMarket;
|
||||
}
|
||||
|
||||
describe('Health Cache', () => {
|
||||
it('test_health0', () => {
|
||||
const sourceBank: BankForHealth = mockBankAndOracle(
|
||||
1 as TokenIndex,
|
||||
0.1,
|
||||
0.2,
|
||||
1,
|
||||
);
|
||||
const targetBank: BankForHealth = mockBankAndOracle(
|
||||
4 as TokenIndex,
|
||||
0.3,
|
||||
0.5,
|
||||
5,
|
||||
);
|
||||
|
||||
const ti1 = TokenInfo.fromBank(sourceBank, I80F48.fromNumber(100));
|
||||
const ti2 = TokenInfo.fromBank(targetBank, I80F48.fromNumber(-10));
|
||||
|
||||
const si1 = Serum3Info.fromOoModifyingTokenInfos(
|
||||
1,
|
||||
ti2,
|
||||
0,
|
||||
ti1,
|
||||
2 as MarketIndex,
|
||||
{
|
||||
quoteTokenTotal: new BN(21),
|
||||
baseTokenTotal: new BN(18),
|
||||
quoteTokenFree: new BN(1),
|
||||
baseTokenFree: new BN(3),
|
||||
referrerRebatesAccrued: new BN(2),
|
||||
} as any as OpenOrders,
|
||||
);
|
||||
|
||||
const pM = mockPerpMarket(9, 0.1, 0.2, targetBank.price);
|
||||
const pp = new PerpPosition(
|
||||
pM.perpMarketIndex,
|
||||
3,
|
||||
I80F48.fromNumber(-310),
|
||||
7,
|
||||
11,
|
||||
1,
|
||||
2,
|
||||
I80F48.fromNumber(0),
|
||||
I80F48.fromNumber(0),
|
||||
);
|
||||
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
|
||||
|
||||
const hc = new HealthCache([ti1, ti2], [si1], [pi1]);
|
||||
|
||||
// for bank1/oracle1, including open orders (scenario: bids execute)
|
||||
const health1 = (100.0 + 1.0 + 2.0 + (20.0 + 15.0 * 5.0)) * 0.8;
|
||||
// for bank2/oracle2
|
||||
const health2 = (-10.0 + 3.0) * 5.0 * 1.5;
|
||||
// for perp (scenario: bids execute)
|
||||
const health3 =
|
||||
(3.0 + 7.0 + 1.0) * 10.0 * 5.0 * 0.8 +
|
||||
(-310.0 + 2.0 * 100.0 - 7.0 * 10.0 * 5.0);
|
||||
|
||||
const health = hc.health(HealthType.init).toNumber();
|
||||
console.log(
|
||||
`health ${health
|
||||
.toFixed(3)
|
||||
.padStart(
|
||||
10,
|
||||
)}, case "test that includes all the side values (like referrer_rebates_accrued)"`,
|
||||
);
|
||||
|
||||
expect(health - (health1 + health2 + health3)).lessThan(0.0000001);
|
||||
});
|
||||
|
||||
it('test_health1', () => {
|
||||
function testFixture(fixture: {
|
||||
name: string;
|
||||
token1: number;
|
||||
token2: number;
|
||||
token3: number;
|
||||
oo12: [number, number];
|
||||
oo13: [number, number];
|
||||
perp1: [number, number, number, number];
|
||||
expectedHealth: number;
|
||||
}): void {
|
||||
const bank1: BankForHealth = mockBankAndOracle(
|
||||
1 as TokenIndex,
|
||||
0.1,
|
||||
0.2,
|
||||
1,
|
||||
);
|
||||
const bank2: BankForHealth = mockBankAndOracle(
|
||||
4 as TokenIndex,
|
||||
0.3,
|
||||
0.5,
|
||||
5,
|
||||
);
|
||||
const bank3: BankForHealth = mockBankAndOracle(
|
||||
5 as TokenIndex,
|
||||
0.3,
|
||||
0.5,
|
||||
10,
|
||||
);
|
||||
|
||||
const ti1 = TokenInfo.fromBank(bank1, I80F48.fromNumber(fixture.token1));
|
||||
const ti2 = TokenInfo.fromBank(bank2, I80F48.fromNumber(fixture.token2));
|
||||
const ti3 = TokenInfo.fromBank(bank3, I80F48.fromNumber(fixture.token3));
|
||||
|
||||
const si1 = Serum3Info.fromOoModifyingTokenInfos(
|
||||
1,
|
||||
ti2,
|
||||
0,
|
||||
ti1,
|
||||
2 as MarketIndex,
|
||||
{
|
||||
quoteTokenTotal: new BN(fixture.oo12[0]),
|
||||
baseTokenTotal: new BN(fixture.oo12[1]),
|
||||
quoteTokenFree: new BN(0),
|
||||
baseTokenFree: new BN(0),
|
||||
referrerRebatesAccrued: new BN(0),
|
||||
} as any as OpenOrders,
|
||||
);
|
||||
|
||||
const si2 = Serum3Info.fromOoModifyingTokenInfos(
|
||||
2,
|
||||
ti3,
|
||||
0,
|
||||
ti1,
|
||||
2 as MarketIndex,
|
||||
{
|
||||
quoteTokenTotal: new BN(fixture.oo13[0]),
|
||||
baseTokenTotal: new BN(fixture.oo13[1]),
|
||||
quoteTokenFree: new BN(0),
|
||||
baseTokenFree: new BN(0),
|
||||
referrerRebatesAccrued: new BN(0),
|
||||
} as any as OpenOrders,
|
||||
);
|
||||
|
||||
const pM = mockPerpMarket(9, 0.1, 0.2, bank2.price);
|
||||
const pp = new PerpPosition(
|
||||
pM.perpMarketIndex,
|
||||
fixture.perp1[0],
|
||||
I80F48.fromNumber(fixture.perp1[1]),
|
||||
fixture.perp1[2],
|
||||
fixture.perp1[3],
|
||||
0,
|
||||
0,
|
||||
I80F48.fromNumber(0),
|
||||
I80F48.fromNumber(0),
|
||||
);
|
||||
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
|
||||
|
||||
const hc = new HealthCache([ti1, ti2, ti3], [si1, si2], [pi1]);
|
||||
const health = hc.health(HealthType.init).toNumber();
|
||||
console.log(
|
||||
`health ${health.toFixed(3).padStart(10)}, case "${fixture.name}"`,
|
||||
);
|
||||
expect(health - fixture.expectedHealth).lessThan(0.0000001);
|
||||
}
|
||||
|
||||
const basePrice = 5;
|
||||
const baseLotsToQuote = 10.0 * basePrice;
|
||||
|
||||
testFixture({
|
||||
name: '0',
|
||||
token1: 100,
|
||||
token2: -10,
|
||||
token3: 0,
|
||||
oo12: [20, 15],
|
||||
oo13: [0, 0],
|
||||
perp1: [3, -131, 7, 11],
|
||||
expectedHealth:
|
||||
// for token1, including open orders (scenario: bids execute)
|
||||
(100.0 + (20.0 + 15.0 * basePrice)) * 0.8 -
|
||||
// for token2
|
||||
10.0 * basePrice * 1.5 +
|
||||
// for perp (scenario: bids execute)
|
||||
(3.0 + 7.0) * baseLotsToQuote * 0.8 +
|
||||
(-131.0 - 7.0 * baseLotsToQuote),
|
||||
});
|
||||
|
||||
testFixture({
|
||||
name: '1',
|
||||
token1: -100,
|
||||
token2: 10,
|
||||
token3: 0,
|
||||
oo12: [20, 15],
|
||||
oo13: [0, 0],
|
||||
perp1: [-10, -131, 7, 11],
|
||||
expectedHealth:
|
||||
// for token1
|
||||
-100.0 * 1.2 +
|
||||
// for token2, including open orders (scenario: asks execute)
|
||||
(10.0 * basePrice + (20.0 + 15.0 * basePrice)) * 0.5 +
|
||||
// for perp (scenario: asks execute)
|
||||
(-10.0 - 11.0) * baseLotsToQuote * 1.2 +
|
||||
(-131.0 + 11.0 * baseLotsToQuote),
|
||||
});
|
||||
|
||||
testFixture({
|
||||
name: '2',
|
||||
token1: 0,
|
||||
token2: 0,
|
||||
token3: 0,
|
||||
oo12: [0, 0],
|
||||
oo13: [0, 0],
|
||||
perp1: [-10, 100, 0, 0],
|
||||
expectedHealth: 0,
|
||||
});
|
||||
|
||||
testFixture({
|
||||
name: '3',
|
||||
token1: 0,
|
||||
token2: 0,
|
||||
token3: 0,
|
||||
oo12: [0, 0],
|
||||
oo13: [0, 0],
|
||||
perp1: [1, -100, 0, 0],
|
||||
expectedHealth: -100.0 + 0.8 * 1.0 * baseLotsToQuote,
|
||||
});
|
||||
|
||||
testFixture({
|
||||
name: '4',
|
||||
token1: 0,
|
||||
token2: 0,
|
||||
token3: 0,
|
||||
oo12: [0, 0],
|
||||
oo13: [0, 0],
|
||||
perp1: [10, 100, 0, 0],
|
||||
expectedHealth: 0,
|
||||
});
|
||||
|
||||
testFixture({
|
||||
name: '5',
|
||||
token1: 0,
|
||||
token2: 0,
|
||||
token3: 0,
|
||||
oo12: [0, 0],
|
||||
oo13: [0, 0],
|
||||
perp1: [30, -100, 0, 0],
|
||||
expectedHealth: 0,
|
||||
});
|
||||
|
||||
testFixture({
|
||||
name: '6, reserved oo funds',
|
||||
token1: -100,
|
||||
token2: -10,
|
||||
token3: -10,
|
||||
oo12: [1, 1],
|
||||
oo13: [1, 1],
|
||||
perp1: [30, -100, 0, 0],
|
||||
expectedHealth:
|
||||
// tokens
|
||||
-100.0 * 1.2 -
|
||||
10.0 * 5.0 * 1.5 -
|
||||
10.0 * 10.0 * 1.5 +
|
||||
// oo_1_2 (-> token1)
|
||||
(1.0 + 5.0) * 1.2 +
|
||||
// oo_1_3 (-> token1)
|
||||
(1.0 + 10.0) * 1.2,
|
||||
});
|
||||
|
||||
testFixture({
|
||||
name: '7, reserved oo funds cross the zero balance level',
|
||||
token1: -14,
|
||||
token2: -10,
|
||||
token3: -10,
|
||||
oo12: [1, 1],
|
||||
oo13: [1, 1],
|
||||
perp1: [0, 0, 0, 0],
|
||||
expectedHealth:
|
||||
-14.0 * 1.2 -
|
||||
10.0 * 5.0 * 1.5 -
|
||||
10.0 * 10.0 * 1.5 +
|
||||
// oo_1_2 (-> token1)
|
||||
3.0 * 1.2 +
|
||||
3.0 * 0.8 +
|
||||
// oo_1_3 (-> token1)
|
||||
8.0 * 1.2 +
|
||||
3.0 * 0.8,
|
||||
});
|
||||
|
||||
testFixture({
|
||||
name: '8, reserved oo funds in a non-quote currency',
|
||||
token1: -100,
|
||||
token2: -100,
|
||||
token3: -1,
|
||||
oo12: [0, 0],
|
||||
oo13: [10, 1],
|
||||
perp1: [0, 0, 0, 0],
|
||||
expectedHealth:
|
||||
// tokens
|
||||
-100.0 * 1.2 -
|
||||
100.0 * 5.0 * 1.5 -
|
||||
10.0 * 1.5 +
|
||||
// oo_1_3 (-> token3)
|
||||
10.0 * 1.5 +
|
||||
10.0 * 0.5,
|
||||
});
|
||||
|
||||
testFixture({
|
||||
name: '9, like 8 but oo_1_2 flips the oo_1_3 target',
|
||||
token1: -100,
|
||||
token2: -100,
|
||||
token3: -1,
|
||||
oo12: [100, 0],
|
||||
oo13: [10, 1],
|
||||
perp1: [0, 0, 0, 0],
|
||||
expectedHealth:
|
||||
// tokens
|
||||
-100.0 * 1.2 -
|
||||
100.0 * 5.0 * 1.5 -
|
||||
10.0 * 1.5 +
|
||||
// oo_1_2 (-> token1)
|
||||
80.0 * 1.2 +
|
||||
20.0 * 0.8 +
|
||||
// oo_1_3 (-> token1)
|
||||
20.0 * 0.8,
|
||||
});
|
||||
});
|
||||
|
||||
it('max swap tokens for min ratio', () => {
|
||||
// USDC like
|
||||
const sourceBank: BankForHealth = {
|
||||
tokenIndex: 0,
|
||||
tokenIndex: 0 as TokenIndex,
|
||||
maintAssetWeight: I80F48.fromNumber(1),
|
||||
initAssetWeight: I80F48.fromNumber(1),
|
||||
maintLiabWeight: I80F48.fromNumber(1),
|
||||
|
@ -17,7 +374,7 @@ describe('Health Cache', () => {
|
|||
};
|
||||
// BTC like
|
||||
const targetBank: BankForHealth = {
|
||||
tokenIndex: 1,
|
||||
tokenIndex: 1 as TokenIndex,
|
||||
maintAssetWeight: I80F48.fromNumber(0.9),
|
||||
initAssetWeight: I80F48.fromNumber(0.8),
|
||||
maintLiabWeight: I80F48.fromNumber(1.1),
|
||||
|
@ -28,7 +385,7 @@ describe('Health Cache', () => {
|
|||
const hc = new HealthCache(
|
||||
[
|
||||
new TokenInfo(
|
||||
0,
|
||||
0 as TokenIndex,
|
||||
sourceBank.maintAssetWeight,
|
||||
sourceBank.initAssetWeight,
|
||||
sourceBank.maintLiabWeight,
|
||||
|
@ -39,7 +396,7 @@ describe('Health Cache', () => {
|
|||
),
|
||||
|
||||
new TokenInfo(
|
||||
1,
|
||||
1 as TokenIndex,
|
||||
targetBank.maintAssetWeight,
|
||||
targetBank.initAssetWeight,
|
||||
targetBank.maintLiabWeight,
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
import { BN } from '@project-serum/anchor';
|
||||
import { OpenOrders } from '@project-serum/serum';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import _ from 'lodash';
|
||||
import { Bank, BankForHealth } from './bank';
|
||||
import { Bank, BankForHealth, TokenIndex } from './bank';
|
||||
import { Group } from './group';
|
||||
import {
|
||||
HUNDRED_I80F48,
|
||||
I80F48,
|
||||
I80F48Dto,
|
||||
MAX_I80F48,
|
||||
ONE_I80F48,
|
||||
ZERO_I80F48,
|
||||
} from './I80F48';
|
||||
import { HealthType } from './mangoAccount';
|
||||
|
||||
import { HealthType, MangoAccount, PerpPosition } from './mangoAccount';
|
||||
import { PerpMarket, PerpOrderSide } from './perp';
|
||||
import { Serum3Market, Serum3Side } from './serum3';
|
||||
import { MarketIndex, Serum3Market, Serum3Side } from './serum3';
|
||||
|
||||
// ░░░░
|
||||
//
|
||||
|
@ -45,7 +47,63 @@ export class HealthCache {
|
|||
public perpInfos: PerpInfo[],
|
||||
) {}
|
||||
|
||||
static fromDto(dto) {
|
||||
static fromMangoAccount(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
): HealthCache {
|
||||
// token contribution from token accounts
|
||||
const tokenInfos = mangoAccount.tokensActive().map((tokenPosition) => {
|
||||
const bank = group.getFirstBankByTokenIndex(tokenPosition.tokenIndex);
|
||||
return TokenInfo.fromBank(bank, tokenPosition.balance(bank));
|
||||
});
|
||||
|
||||
// Fill the TokenInfo balance with free funds in serum3 oo accounts, and fill
|
||||
// the serum3MaxReserved with their reserved funds. Also build Serum3Infos.
|
||||
const serum3Infos = mangoAccount.serum3Active().map((serum3) => {
|
||||
const oo = mangoAccount.getSerum3OoAccount(serum3.marketIndex);
|
||||
|
||||
// find the TokenInfos for the market's base and quote tokens
|
||||
const baseIndex = tokenInfos.findIndex(
|
||||
(tokenInfo) => tokenInfo.tokenIndex === serum3.baseTokenIndex,
|
||||
);
|
||||
const baseInfo = tokenInfos[baseIndex];
|
||||
if (!baseInfo) {
|
||||
throw new Error(
|
||||
`BaseInfo not found for market with marketIndex ${serum3.marketIndex}!`,
|
||||
);
|
||||
}
|
||||
const quoteIndex = tokenInfos.findIndex(
|
||||
(tokenInfo) => tokenInfo.tokenIndex === serum3.quoteTokenIndex,
|
||||
);
|
||||
const quoteInfo = tokenInfos[quoteIndex];
|
||||
if (!quoteInfo) {
|
||||
throw new Error(
|
||||
`QuoteInfo not found for market with marketIndex ${serum3.marketIndex}!`,
|
||||
);
|
||||
}
|
||||
|
||||
return Serum3Info.fromOoModifyingTokenInfos(
|
||||
baseIndex,
|
||||
baseInfo,
|
||||
quoteIndex,
|
||||
quoteInfo,
|
||||
serum3.marketIndex,
|
||||
oo,
|
||||
);
|
||||
});
|
||||
|
||||
// health contribution from perp accounts
|
||||
const perpInfos = mangoAccount.perpActive().map((perpPosition) => {
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(
|
||||
perpPosition.marketIndex,
|
||||
);
|
||||
return PerpInfo.fromPerpPosition(perpMarket, perpPosition);
|
||||
});
|
||||
|
||||
return new HealthCache(tokenInfos, serum3Infos, perpInfos);
|
||||
}
|
||||
|
||||
static fromDto(dto): HealthCache {
|
||||
return new HealthCache(
|
||||
dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)),
|
||||
dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)),
|
||||
|
@ -57,6 +115,7 @@ export class HealthCache {
|
|||
const health = ZERO_I80F48();
|
||||
for (const tokenInfo of this.tokenInfos) {
|
||||
const contrib = tokenInfo.healthContribution(healthType);
|
||||
// console.log(` - ti ${contrib}`);
|
||||
health.iadd(contrib);
|
||||
}
|
||||
for (const serum3Info of this.serum3Infos) {
|
||||
|
@ -64,10 +123,12 @@ export class HealthCache {
|
|||
healthType,
|
||||
this.tokenInfos,
|
||||
);
|
||||
// console.log(` - si ${contrib}`);
|
||||
health.iadd(contrib);
|
||||
}
|
||||
for (const perpInfo of this.perpInfos) {
|
||||
const contrib = perpInfo.healthContribution(healthType);
|
||||
// console.log(` - pi ${contrib}`);
|
||||
health.iadd(contrib);
|
||||
}
|
||||
return health;
|
||||
|
@ -164,34 +225,32 @@ export class HealthCache {
|
|||
}
|
||||
}
|
||||
|
||||
findTokenInfoIndex(tokenIndex: number): number {
|
||||
findTokenInfoIndex(tokenIndex: TokenIndex): number {
|
||||
return this.tokenInfos.findIndex(
|
||||
(tokenInfo) => tokenInfo.tokenIndex == tokenIndex,
|
||||
(tokenInfo) => tokenInfo.tokenIndex === tokenIndex,
|
||||
);
|
||||
}
|
||||
|
||||
getOrCreateTokenInfoIndex(bank: BankForHealth): number {
|
||||
const index = this.findTokenInfoIndex(bank.tokenIndex);
|
||||
if (index == -1) {
|
||||
this.tokenInfos.push(TokenInfo.emptyFromBank(bank));
|
||||
this.tokenInfos.push(TokenInfo.fromBank(bank));
|
||||
}
|
||||
return this.findTokenInfoIndex(bank.tokenIndex);
|
||||
}
|
||||
|
||||
findSerum3InfoIndex(marketIndex: number): number {
|
||||
findSerum3InfoIndex(marketIndex: MarketIndex): number {
|
||||
return this.serum3Infos.findIndex(
|
||||
(serum3Info) => serum3Info.marketIndex === marketIndex,
|
||||
);
|
||||
}
|
||||
|
||||
getOrCreateSerum3InfoIndex(group: Group, serum3Market: Serum3Market): number {
|
||||
getOrCreateSerum3InfoIndex(
|
||||
baseBank: BankForHealth,
|
||||
quoteBank: BankForHealth,
|
||||
serum3Market: Serum3Market,
|
||||
): number {
|
||||
const index = this.findSerum3InfoIndex(serum3Market.marketIndex);
|
||||
const baseBank = group.getFirstBankByTokenIndex(
|
||||
serum3Market.baseTokenIndex,
|
||||
);
|
||||
const quoteBank = group.getFirstBankByTokenIndex(
|
||||
serum3Market.quoteTokenIndex,
|
||||
);
|
||||
const baseEntryIndex = this.getOrCreateTokenInfoIndex(baseBank);
|
||||
const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank);
|
||||
if (index == -1) {
|
||||
|
@ -208,20 +267,14 @@ export class HealthCache {
|
|||
|
||||
adjustSerum3Reserved(
|
||||
// todo change indices to types from numbers
|
||||
group: Group,
|
||||
baseBank: BankForHealth,
|
||||
quoteBank: BankForHealth,
|
||||
serum3Market: Serum3Market,
|
||||
reservedBaseChange: I80F48,
|
||||
freeBaseChange: I80F48,
|
||||
reservedQuoteChange: I80F48,
|
||||
freeQuoteChange: I80F48,
|
||||
) {
|
||||
const baseBank = group.getFirstBankByTokenIndex(
|
||||
serum3Market.baseTokenIndex,
|
||||
);
|
||||
const quoteBank = group.getFirstBankByTokenIndex(
|
||||
serum3Market.quoteTokenIndex,
|
||||
);
|
||||
|
||||
): void {
|
||||
const baseEntryIndex = this.getOrCreateTokenInfoIndex(baseBank);
|
||||
const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank);
|
||||
|
||||
|
@ -238,7 +291,11 @@ export class HealthCache {
|
|||
quoteEntry.balance.iadd(freeQuoteChange.mul(quoteEntry.oraclePrice));
|
||||
|
||||
// Apply it to the serum3 info
|
||||
const index = this.getOrCreateSerum3InfoIndex(group, serum3Market);
|
||||
const index = this.getOrCreateSerum3InfoIndex(
|
||||
baseBank,
|
||||
quoteBank,
|
||||
serum3Market,
|
||||
);
|
||||
const serum3Info = this.serum3Infos[index];
|
||||
serum3Info.reserved = serum3Info.reserved.add(reservedAmount);
|
||||
}
|
||||
|
@ -257,7 +314,7 @@ export class HealthCache {
|
|||
return this.findPerpInfoIndex(perpMarket.perpMarketIndex);
|
||||
}
|
||||
|
||||
public static logHealthCache(debug: string, healthCache: HealthCache) {
|
||||
public static logHealthCache(debug: string, healthCache: HealthCache): void {
|
||||
if (debug) console.log(debug);
|
||||
for (const token of healthCache.tokenInfos) {
|
||||
console.log(` ${token.toString()}`);
|
||||
|
@ -293,10 +350,6 @@ export class HealthCache {
|
|||
for (const change of nativeTokenChanges) {
|
||||
const bank: Bank = group.getFirstBankByMint(change.mintPk);
|
||||
const changeIndex = adjustedCache.getOrCreateTokenInfoIndex(bank);
|
||||
if (!bank.price)
|
||||
throw new Error(
|
||||
`Oracle price not loaded for ${change.mintPk.toString()}`,
|
||||
);
|
||||
adjustedCache.tokenInfos[changeIndex].balance.iadd(
|
||||
change.nativeTokenAmount.mul(bank.price),
|
||||
);
|
||||
|
@ -306,18 +359,13 @@ export class HealthCache {
|
|||
}
|
||||
|
||||
simHealthRatioWithSerum3BidChanges(
|
||||
group: Group,
|
||||
baseBank: BankForHealth,
|
||||
quoteBank: BankForHealth,
|
||||
bidNativeQuoteAmount: I80F48,
|
||||
serum3Market: Serum3Market,
|
||||
healthType: HealthType = HealthType.init,
|
||||
): I80F48 {
|
||||
const adjustedCache: HealthCache = _.cloneDeep(this);
|
||||
const quoteBank = group.getFirstBankByTokenIndex(
|
||||
serum3Market.quoteTokenIndex,
|
||||
);
|
||||
if (!quoteBank) {
|
||||
throw new Error(`No bank for index ${serum3Market.quoteTokenIndex}`);
|
||||
}
|
||||
const quoteIndex = adjustedCache.getOrCreateTokenInfoIndex(quoteBank);
|
||||
const quote = adjustedCache.tokenInfos[quoteIndex];
|
||||
|
||||
|
@ -331,7 +379,8 @@ export class HealthCache {
|
|||
|
||||
// Increase reserved in Serum3Info for quote
|
||||
adjustedCache.adjustSerum3Reserved(
|
||||
group,
|
||||
baseBank,
|
||||
quoteBank,
|
||||
serum3Market,
|
||||
ZERO_I80F48(),
|
||||
ZERO_I80F48(),
|
||||
|
@ -342,18 +391,13 @@ export class HealthCache {
|
|||
}
|
||||
|
||||
simHealthRatioWithSerum3AskChanges(
|
||||
group: Group,
|
||||
baseBank: BankForHealth,
|
||||
quoteBank: BankForHealth,
|
||||
askNativeBaseAmount: I80F48,
|
||||
serum3Market: Serum3Market,
|
||||
healthType: HealthType = HealthType.init,
|
||||
): I80F48 {
|
||||
const adjustedCache: HealthCache = _.cloneDeep(this);
|
||||
const baseBank = group.getFirstBankByTokenIndex(
|
||||
serum3Market.baseTokenIndex,
|
||||
);
|
||||
if (!baseBank) {
|
||||
throw new Error(`No bank for index ${serum3Market.quoteTokenIndex}`);
|
||||
}
|
||||
const baseIndex = adjustedCache.getOrCreateTokenInfoIndex(baseBank);
|
||||
const base = adjustedCache.tokenInfos[baseIndex];
|
||||
|
||||
|
@ -367,7 +411,8 @@ export class HealthCache {
|
|||
|
||||
// Increase reserved in Serum3Info for base
|
||||
adjustedCache.adjustSerum3Reserved(
|
||||
group,
|
||||
baseBank,
|
||||
quoteBank,
|
||||
serum3Market,
|
||||
askNativeBaseAmount,
|
||||
ZERO_I80F48(),
|
||||
|
@ -384,7 +429,7 @@ export class HealthCache {
|
|||
rightRatio: I80F48,
|
||||
targetRatio: I80F48,
|
||||
healthRatioAfterActionFn: (I80F48) => I80F48,
|
||||
) {
|
||||
): I80F48 {
|
||||
const maxIterations = 40;
|
||||
// TODO: make relative to health ratio decimals? Might be over engineering
|
||||
const targetError = I80F48.fromNumber(0.001);
|
||||
|
@ -396,11 +441,12 @@ export class HealthCache {
|
|||
rightRatio.sub(targetRatio).isNeg())
|
||||
) {
|
||||
throw new Error(
|
||||
`internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}`,
|
||||
`Internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}, likely reason is the zeroAmount not been tight enough!`,
|
||||
);
|
||||
}
|
||||
|
||||
let newAmount;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const key of Array(maxIterations).fill(0).keys()) {
|
||||
newAmount = left.add(right).mul(I80F48.fromNumber(0.5));
|
||||
const newAmountRatio = healthRatioAfterActionFn(newAmount);
|
||||
|
@ -427,15 +473,6 @@ export class HealthCache {
|
|||
minRatio: I80F48,
|
||||
priceFactor: I80F48,
|
||||
): I80F48 {
|
||||
if (
|
||||
!sourceBank.price ||
|
||||
sourceBank.price.lte(ZERO_I80F48()) ||
|
||||
!targetBank.price ||
|
||||
targetBank.price.lte(ZERO_I80F48())
|
||||
) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
|
||||
if (
|
||||
sourceBank.initLiabWeight
|
||||
.sub(targetBank.initAssetWeight)
|
||||
|
@ -454,6 +491,7 @@ export class HealthCache {
|
|||
// - be careful about finding the minRatio point: the function isn't convex
|
||||
|
||||
const initialRatio = this.healthRatio(HealthType.init);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const initialHealth = this.health(HealthType.init);
|
||||
if (initialRatio.lte(ZERO_I80F48())) {
|
||||
return ZERO_I80F48();
|
||||
|
@ -481,7 +519,7 @@ export class HealthCache {
|
|||
// negative.
|
||||
// The maximum will be at one of these points (ignoring serum3 effects).
|
||||
|
||||
function cacheAfterSwap(amount: I80F48) {
|
||||
function cacheAfterSwap(amount: I80F48): HealthCache {
|
||||
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
|
||||
// HealthCache.logHealthCache('beforeSwap', adjustedCache);
|
||||
adjustedCache.tokenInfos[sourceIndex].balance.isub(amount);
|
||||
|
@ -578,24 +616,12 @@ export class HealthCache {
|
|||
}
|
||||
|
||||
getMaxSerum3OrderForHealthRatio(
|
||||
group: Group,
|
||||
baseBank: BankForHealth,
|
||||
quoteBank: BankForHealth,
|
||||
serum3Market: Serum3Market,
|
||||
side: Serum3Side,
|
||||
minRatio: I80F48,
|
||||
) {
|
||||
const baseBank = group.getFirstBankByTokenIndex(
|
||||
serum3Market.baseTokenIndex,
|
||||
);
|
||||
if (!baseBank) {
|
||||
throw new Error(`No bank for index ${serum3Market.baseTokenIndex}`);
|
||||
}
|
||||
const quoteBank = group.getFirstBankByTokenIndex(
|
||||
serum3Market.quoteTokenIndex,
|
||||
);
|
||||
if (!quoteBank) {
|
||||
throw new Error(`No bank for index ${serum3Market.quoteTokenIndex}`);
|
||||
}
|
||||
|
||||
): I80F48 {
|
||||
const healthCacheClone: HealthCache = _.cloneDeep(this);
|
||||
|
||||
const baseIndex = healthCacheClone.getOrCreateTokenInfoIndex(baseBank);
|
||||
|
@ -652,10 +678,11 @@ export class HealthCache {
|
|||
}
|
||||
|
||||
const cache = cacheAfterPlacingOrder(zeroAmount);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const zeroAmountHealth = cache.health(HealthType.init);
|
||||
const zeroAmountRatio = cache.healthRatio(HealthType.init);
|
||||
|
||||
function cacheAfterPlacingOrder(amount: I80F48) {
|
||||
function cacheAfterPlacingOrder(amount: I80F48): HealthCache {
|
||||
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
|
||||
|
||||
side === Serum3Side.ask
|
||||
|
@ -663,7 +690,8 @@ export class HealthCache {
|
|||
: adjustedCache.tokenInfos[quoteIndex].balance.isub(amount);
|
||||
|
||||
adjustedCache.adjustSerum3Reserved(
|
||||
group,
|
||||
baseBank,
|
||||
quoteBank,
|
||||
serum3Market,
|
||||
side === Serum3Side.ask ? amount.div(base.oraclePrice) : ZERO_I80F48(),
|
||||
ZERO_I80F48(),
|
||||
|
@ -687,18 +715,7 @@ export class HealthCache {
|
|||
healthRatioAfterPlacingOrder,
|
||||
);
|
||||
|
||||
// If its a bid then the reserved fund and potential loan is in quote,
|
||||
// If its a ask then the reserved fund and potential loan is in base,
|
||||
// also keep some buffer for fees, use taker fees for worst case simulation.
|
||||
return side === Serum3Side.bid
|
||||
? amount
|
||||
.div(quote.oraclePrice)
|
||||
.div(ONE_I80F48().add(baseBank.loanOriginationFeeRate))
|
||||
.div(ONE_I80F48().add(I80F48.fromNumber(group.getFeeRate(false))))
|
||||
: amount
|
||||
.div(base.oraclePrice)
|
||||
.div(ONE_I80F48().add(quoteBank.loanOriginationFeeRate))
|
||||
.div(ONE_I80F48().add(I80F48.fromNumber(group.getFeeRate(false))));
|
||||
return amount;
|
||||
}
|
||||
|
||||
getMaxPerpForHealthRatio(
|
||||
|
@ -813,7 +830,7 @@ export class HealthCache {
|
|||
|
||||
export class TokenInfo {
|
||||
constructor(
|
||||
public tokenIndex: number,
|
||||
public tokenIndex: TokenIndex,
|
||||
public maintAssetWeight: I80F48,
|
||||
public initAssetWeight: I80F48,
|
||||
public maintLiabWeight: I80F48,
|
||||
|
@ -828,7 +845,7 @@ export class TokenInfo {
|
|||
|
||||
static fromDto(dto: TokenInfoDto): TokenInfo {
|
||||
return new TokenInfo(
|
||||
dto.tokenIndex,
|
||||
dto.tokenIndex as TokenIndex,
|
||||
I80F48.from(dto.maintAssetWeight),
|
||||
I80F48.from(dto.initAssetWeight),
|
||||
I80F48.from(dto.maintLiabWeight),
|
||||
|
@ -839,11 +856,11 @@ export class TokenInfo {
|
|||
);
|
||||
}
|
||||
|
||||
static emptyFromBank(bank: BankForHealth): TokenInfo {
|
||||
if (!bank.price)
|
||||
throw new Error(
|
||||
`Failed to create TokenInfo. Bank price unavailable for bank with tokenIndex ${bank.tokenIndex}`,
|
||||
);
|
||||
static fromBank(
|
||||
bank: BankForHealth,
|
||||
nativeBalance?: I80F48,
|
||||
serum3MaxReserved?: I80F48,
|
||||
): TokenInfo {
|
||||
return new TokenInfo(
|
||||
bank.tokenIndex,
|
||||
bank.maintAssetWeight,
|
||||
|
@ -851,8 +868,8 @@ export class TokenInfo {
|
|||
bank.maintLiabWeight,
|
||||
bank.initLiabWeight,
|
||||
bank.price,
|
||||
ZERO_I80F48(),
|
||||
ZERO_I80F48(),
|
||||
nativeBalance ? nativeBalance.mul(bank.price) : ZERO_I80F48(),
|
||||
serum3MaxReserved ? serum3MaxReserved : ZERO_I80F48(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -876,7 +893,7 @@ export class TokenInfo {
|
|||
).mul(this.balance);
|
||||
}
|
||||
|
||||
toString() {
|
||||
toString(): string {
|
||||
return ` tokenIndex: ${this.tokenIndex}, balance: ${
|
||||
this.balance
|
||||
}, serum3MaxReserved: ${
|
||||
|
@ -890,15 +907,15 @@ export class Serum3Info {
|
|||
public reserved: I80F48,
|
||||
public baseIndex: number,
|
||||
public quoteIndex: number,
|
||||
public marketIndex: number,
|
||||
public marketIndex: MarketIndex,
|
||||
) {}
|
||||
|
||||
static fromDto(dto: Serum3InfoDto) {
|
||||
static fromDto(dto: Serum3InfoDto): Serum3Info {
|
||||
return new Serum3Info(
|
||||
I80F48.from(dto.reserved),
|
||||
dto.baseIndex,
|
||||
dto.quoteIndex,
|
||||
dto.marketIndex,
|
||||
dto.marketIndex as MarketIndex,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -906,7 +923,7 @@ export class Serum3Info {
|
|||
serum3Market: Serum3Market,
|
||||
baseEntryIndex: number,
|
||||
quoteEntryIndex: number,
|
||||
) {
|
||||
): Serum3Info {
|
||||
return new Serum3Info(
|
||||
ZERO_I80F48(),
|
||||
baseEntryIndex,
|
||||
|
@ -915,10 +932,47 @@ export class Serum3Info {
|
|||
);
|
||||
}
|
||||
|
||||
static fromOoModifyingTokenInfos(
|
||||
baseIndex: number,
|
||||
baseInfo: TokenInfo,
|
||||
quoteIndex: number,
|
||||
quoteInfo: TokenInfo,
|
||||
marketIndex: MarketIndex,
|
||||
oo: OpenOrders,
|
||||
): Serum3Info {
|
||||
// add the amounts that are freely settleable
|
||||
const baseFree = I80F48.fromString(oo.baseTokenFree.toString());
|
||||
// NOTE: referrerRebatesAccrued is not declared on oo class, but the layout
|
||||
// is aware of it
|
||||
const quoteFree = I80F48.fromString(
|
||||
oo.quoteTokenFree.add((oo as any).referrerRebatesAccrued).toString(),
|
||||
);
|
||||
baseInfo.balance.iadd(baseFree.mul(baseInfo.oraclePrice));
|
||||
quoteInfo.balance.iadd(quoteFree.mul(quoteInfo.oraclePrice));
|
||||
|
||||
// add the reserved amount to both sides, to have the worst-case covered
|
||||
const reservedBase = I80F48.fromString(
|
||||
oo.baseTokenTotal.sub(oo.baseTokenFree).toString(),
|
||||
);
|
||||
const reservedQuote = I80F48.fromString(
|
||||
oo.quoteTokenTotal.sub(oo.quoteTokenFree).toString(),
|
||||
);
|
||||
const reservedBalance = reservedBase
|
||||
.mul(baseInfo.oraclePrice)
|
||||
.add(reservedQuote.mul(quoteInfo.oraclePrice));
|
||||
baseInfo.serum3MaxReserved.iadd(reservedBalance);
|
||||
quoteInfo.serum3MaxReserved.iadd(reservedBalance);
|
||||
|
||||
return new Serum3Info(reservedBalance, baseIndex, quoteIndex, marketIndex);
|
||||
}
|
||||
|
||||
healthContribution(healthType: HealthType, tokenInfos: TokenInfo[]): I80F48 {
|
||||
const baseInfo = tokenInfos[this.baseIndex];
|
||||
const quoteInfo = tokenInfos[this.quoteIndex];
|
||||
const reserved = this.reserved;
|
||||
// console.log(` - reserved ${reserved}`);
|
||||
// console.log(` - this.baseIndex ${this.baseIndex}`);
|
||||
// console.log(` - this.quoteIndex ${this.quoteIndex}`);
|
||||
|
||||
if (reserved.isZero()) {
|
||||
return ZERO_I80F48();
|
||||
|
@ -926,7 +980,7 @@ export class Serum3Info {
|
|||
|
||||
// How much the health would increase if the reserved balance were applied to the passed
|
||||
// token info?
|
||||
const computeHealthEffect = function (tokenInfo: TokenInfo) {
|
||||
const computeHealthEffect = function (tokenInfo: TokenInfo): I80F48 {
|
||||
// This balance includes all possible reserved funds from markets that relate to the
|
||||
// token, including this market itself: `reserved` is already included in `max_balance`.
|
||||
const maxBalance = tokenInfo.balance.add(tokenInfo.serum3MaxReserved);
|
||||
|
@ -946,15 +1000,25 @@ export class Serum3Info {
|
|||
}
|
||||
const assetWeight = tokenInfo.assetWeight(healthType);
|
||||
const liabWeight = tokenInfo.liabWeight(healthType);
|
||||
|
||||
// console.log(` - tokenInfo.index ${tokenInfo.tokenIndex}`);
|
||||
// console.log(` - tokenInfo.balance ${tokenInfo.balance}`);
|
||||
// console.log(
|
||||
// ` - tokenInfo.serum3MaxReserved ${tokenInfo.serum3MaxReserved}`,
|
||||
// );
|
||||
// console.log(` - assetPart ${assetPart}`);
|
||||
// console.log(` - liabPart ${liabPart}`);
|
||||
return assetWeight.mul(assetPart).add(liabWeight.mul(liabPart));
|
||||
};
|
||||
|
||||
const reservedAsBase = computeHealthEffect(baseInfo);
|
||||
const reservedAsQuote = computeHealthEffect(quoteInfo);
|
||||
// console.log(` - reservedAsBase ${reservedAsBase}`);
|
||||
// console.log(` - reservedAsQuote ${reservedAsQuote}`);
|
||||
return reservedAsBase.min(reservedAsQuote);
|
||||
}
|
||||
|
||||
toString(tokenInfos: TokenInfo[]) {
|
||||
toString(tokenInfos: TokenInfo[]): string {
|
||||
return ` marketIndex: ${this.marketIndex}, baseIndex: ${
|
||||
this.baseIndex
|
||||
}, quoteIndex: ${this.quoteIndex}, reserved: ${
|
||||
|
@ -970,15 +1034,14 @@ export class PerpInfo {
|
|||
public initAssetWeight: I80F48,
|
||||
public maintLiabWeight: I80F48,
|
||||
public initLiabWeight: I80F48,
|
||||
// in health-reference-token native units, needs scaling by asset/liab
|
||||
public base: I80F48,
|
||||
// in health-reference-token native units, no asset/liab factor needed
|
||||
public quote: I80F48,
|
||||
public oraclePrice: I80F48,
|
||||
public hasOpenOrders: boolean,
|
||||
public trustedMarket: boolean,
|
||||
) {}
|
||||
|
||||
static fromDto(dto: PerpInfoDto) {
|
||||
static fromDto(dto: PerpInfoDto): PerpInfo {
|
||||
return new PerpInfo(
|
||||
dto.perpMarketIndex,
|
||||
I80F48.from(dto.maintAssetWeight),
|
||||
|
@ -989,6 +1052,114 @@ export class PerpInfo {
|
|||
I80F48.from(dto.quote),
|
||||
I80F48.from(dto.oraclePrice),
|
||||
dto.hasOpenOrders,
|
||||
dto.trustedMarket,
|
||||
);
|
||||
}
|
||||
|
||||
static fromPerpPosition(
|
||||
perpMarket: PerpMarket,
|
||||
perpPosition: PerpPosition,
|
||||
): PerpInfo {
|
||||
const baseLotSize = I80F48.fromString(perpMarket.baseLotSize.toString());
|
||||
const baseLots = I80F48.fromNumber(
|
||||
perpPosition.basePositionLots + perpPosition.takerBaseLots,
|
||||
);
|
||||
|
||||
const unsettledFunding = perpPosition.unsettledFunding(perpMarket);
|
||||
|
||||
const takerQuote = I80F48.fromString(
|
||||
new BN(perpPosition.takerQuoteLots)
|
||||
.mul(perpMarket.quoteLotSize)
|
||||
.toString(),
|
||||
);
|
||||
const quoteCurrent = I80F48.fromString(
|
||||
perpPosition.quotePositionNative.toString(),
|
||||
)
|
||||
.sub(unsettledFunding)
|
||||
.add(takerQuote);
|
||||
|
||||
// Two scenarios:
|
||||
// 1. The price goes low and all bids execute, converting to base.
|
||||
// That means the perp position is increased by `bids` and the quote position
|
||||
// is decreased by `bids * baseLotSize * price`.
|
||||
// The health for this case is:
|
||||
// (weighted(baseLots + bids) - bids) * baseLotSize * price + quote
|
||||
// 2. The price goes high and all asks execute, converting to quote.
|
||||
// The health for this case is:
|
||||
// (weighted(baseLots - asks) + asks) * baseLotSize * price + quote
|
||||
//
|
||||
// Comparing these makes it clear we need to pick the worse subfactor
|
||||
// weighted(baseLots + bids) - bids =: scenario1
|
||||
// or
|
||||
// weighted(baseLots - asks) + asks =: scenario2
|
||||
//
|
||||
// Additionally, we want this scenario choice to be the same no matter whether we're
|
||||
// computing init or maint health. This can be guaranteed by requiring the weights
|
||||
// to satisfy the property (P):
|
||||
//
|
||||
// (1 - initAssetWeight) / (initLiabWeight - 1)
|
||||
// == (1 - maintAssetWeight) / (maintLiabWeight - 1)
|
||||
//
|
||||
// Derivation:
|
||||
// Set asksNetLots := baseLots - asks, bidsNetLots := baseLots + bids.
|
||||
// Now
|
||||
// scenario1 = weighted(bidsNetLots) - bidsNetLots + baseLots and
|
||||
// scenario2 = weighted(asksNetLots) - asksNetLots + baseLots
|
||||
// So with expanding weigthed(a) = weightFactorForA * a, the question
|
||||
// scenario1 < scenario2
|
||||
// becomes:
|
||||
// (weightFactorForBidsNetLots - 1) * bidsNetLots
|
||||
// < (weightFactorForAsksNetLots - 1) * asksNetLots
|
||||
// Since asksNetLots < 0 and bidsNetLots > 0 is the only interesting case, (P) follows.
|
||||
//
|
||||
// We satisfy (P) by requiring
|
||||
// assetWeight = 1 - x and liabWeight = 1 + x
|
||||
//
|
||||
// And with that assumption the scenario choice condition further simplifies to:
|
||||
// scenario1 < scenario2
|
||||
// iff abs(bidsNetLots) > abs(asksNetLots)
|
||||
|
||||
const bidsNetLots = baseLots.add(
|
||||
I80F48.fromNumber(perpPosition.bidsBaseLots),
|
||||
);
|
||||
const asksNetLots = baseLots.sub(
|
||||
I80F48.fromNumber(perpPosition.asksBaseLots),
|
||||
);
|
||||
|
||||
const lotsToQuote = baseLotSize.mul(perpMarket.price);
|
||||
|
||||
let base, quote;
|
||||
if (bidsNetLots.abs().gt(asksNetLots.abs())) {
|
||||
const bidsBaseLots = I80F48.fromString(
|
||||
perpPosition.bidsBaseLots.toString(),
|
||||
);
|
||||
base = bidsNetLots.mul(lotsToQuote);
|
||||
quote = quoteCurrent.sub(bidsBaseLots.mul(lotsToQuote));
|
||||
} else {
|
||||
const asksBaseLots = I80F48.fromString(
|
||||
perpPosition.asksBaseLots.toString(),
|
||||
);
|
||||
base = asksNetLots.mul(lotsToQuote);
|
||||
quote = quoteCurrent.add(asksBaseLots.mul(lotsToQuote));
|
||||
}
|
||||
|
||||
// console.log(`bidsNetLots ${bidsNetLots}`);
|
||||
// console.log(`asksNetLots ${asksNetLots}`);
|
||||
// console.log(`quoteCurrent ${quoteCurrent}`);
|
||||
// console.log(`base ${base}`);
|
||||
// console.log(`quote ${quote}`);
|
||||
|
||||
return new PerpInfo(
|
||||
perpMarket.perpMarketIndex,
|
||||
perpMarket.maintAssetWeight,
|
||||
perpMarket.initAssetWeight,
|
||||
perpMarket.maintLiabWeight,
|
||||
perpMarket.initLiabWeight,
|
||||
base,
|
||||
quote,
|
||||
perpMarket.price,
|
||||
perpPosition.hasOpenOrders(),
|
||||
perpMarket.trustedMarket,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1006,16 +1177,21 @@ export class PerpInfo {
|
|||
weight = this.maintAssetWeight;
|
||||
}
|
||||
|
||||
// FUTURE: Allow v3-style "reliable" markets where we can return
|
||||
// `self.quote + weight * self.base` here
|
||||
return this.quote.add(weight.mul(this.base)).min(ZERO_I80F48());
|
||||
// console.log(`initLiabWeight ${this.initLiabWeight}`);
|
||||
// console.log(`initAssetWeight ${this.initAssetWeight}`);
|
||||
// console.log(`weight ${weight}`);
|
||||
// console.log(`this.quote ${this.quote}`);
|
||||
// console.log(`this.base ${this.base}`);
|
||||
|
||||
const uncappedHealthContribution = this.quote.add(weight.mul(this.base));
|
||||
if (this.trustedMarket) {
|
||||
return uncappedHealthContribution;
|
||||
} else {
|
||||
return uncappedHealthContribution.min(ZERO_I80F48());
|
||||
}
|
||||
}
|
||||
|
||||
static emptyFromPerpMarket(perpMarket: PerpMarket): PerpInfo {
|
||||
if (!perpMarket.price)
|
||||
throw new Error(
|
||||
`Failed to create PerpInfo. Oracle price unavailable. ${perpMarket.oracle.toString()}`,
|
||||
);
|
||||
return new PerpInfo(
|
||||
perpMarket.perpMarketIndex,
|
||||
perpMarket.maintAssetWeight,
|
||||
|
@ -1024,10 +1200,19 @@ export class PerpInfo {
|
|||
perpMarket.initLiabWeight,
|
||||
ZERO_I80F48(),
|
||||
ZERO_I80F48(),
|
||||
I80F48.fromNumber(perpMarket.price),
|
||||
perpMarket.price,
|
||||
false,
|
||||
perpMarket.trustedMarket,
|
||||
);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return ` perpMarketIndex: ${this.perpMarketIndex}, base: ${
|
||||
this.base
|
||||
}, quote: ${this.quote}, oraclePrice: ${
|
||||
this.oraclePrice
|
||||
}, initHealth ${this.healthContribution(HealthType.init)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class HealthCacheDto {
|
||||
|
@ -1087,10 +1272,9 @@ export class PerpInfoDto {
|
|||
initAssetWeight: I80F48Dto;
|
||||
maintLiabWeight: I80F48Dto;
|
||||
initLiabWeight: I80F48Dto;
|
||||
// in health-reference-token native units, needs scaling by asset/liab
|
||||
base: I80F48Dto;
|
||||
// in health-reference-token native units, no asset/liab factor needed
|
||||
quote: I80F48Dto;
|
||||
oraclePrice: I80F48Dto;
|
||||
hasOpenOrders: boolean;
|
||||
trustedMarket: boolean;
|
||||
}
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
import { BN } from '@project-serum/anchor';
|
||||
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
|
||||
import { Order, Orderbook } from '@project-serum/serum/lib/market';
|
||||
import { OpenOrders, Order, Orderbook } from '@project-serum/serum/lib/market';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import { MangoClient } from '../client';
|
||||
import { SERUM3_PROGRAM_ID } from '../constants';
|
||||
import {
|
||||
nativeI80F48ToUi,
|
||||
toNative,
|
||||
toUiDecimals,
|
||||
toUiDecimalsForQuote,
|
||||
} from '../utils';
|
||||
import { Bank } from './bank';
|
||||
import { Bank, TokenIndex } from './bank';
|
||||
import { Group } from './group';
|
||||
import { HealthCache, HealthCacheDto } from './healthCache';
|
||||
import { HealthCache } from './healthCache';
|
||||
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48';
|
||||
import { PerpOrder, PerpOrderSide } from './perp';
|
||||
import { Serum3Side } from './serum3';
|
||||
import { PerpMarket, PerpMarketIndex, PerpOrder, PerpOrderSide } from './perp';
|
||||
import { MarketIndex, Serum3Side } from './serum3';
|
||||
export class MangoAccount {
|
||||
public tokens: TokenPosition[];
|
||||
public serum3: Serum3Orders[];
|
||||
|
@ -58,7 +59,7 @@ export class MangoAccount {
|
|||
obj.serum3 as Serum3PositionDto[],
|
||||
obj.perps as PerpPositionDto[],
|
||||
obj.perpOpenOrders as PerpOoDto[],
|
||||
{} as any,
|
||||
new Map(), // serum3OosMapByMarketIndex
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -78,39 +79,56 @@ export class MangoAccount {
|
|||
serum3: Serum3PositionDto[],
|
||||
perps: PerpPositionDto[],
|
||||
perpOpenOrders: PerpOoDto[],
|
||||
public accountData: undefined | MangoAccountData,
|
||||
public serum3OosMapByMarketIndex: Map<number, OpenOrders>,
|
||||
) {
|
||||
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
||||
this.tokens = tokens.map((dto) => TokenPosition.from(dto));
|
||||
this.serum3 = serum3.map((dto) => Serum3Orders.from(dto));
|
||||
this.perps = perps.map((dto) => PerpPosition.from(dto));
|
||||
this.perpOpenOrders = perpOpenOrders.map((dto) => PerpOo.from(dto));
|
||||
this.accountData = undefined;
|
||||
this.netDeposits = netDeposits;
|
||||
}
|
||||
|
||||
async reload(client: MangoClient, group: Group): Promise<MangoAccount> {
|
||||
async reload(client: MangoClient): Promise<MangoAccount> {
|
||||
const mangoAccount = await client.getMangoAccount(this);
|
||||
await mangoAccount.reloadAccountData(client, group);
|
||||
await mangoAccount.reloadAccountData(client);
|
||||
Object.assign(this, mangoAccount);
|
||||
return mangoAccount;
|
||||
}
|
||||
|
||||
async reloadWithSlot(
|
||||
client: MangoClient,
|
||||
group: Group,
|
||||
): Promise<{ value: MangoAccount; slot: number }> {
|
||||
const resp = await client.getMangoAccountWithSlot(this.publicKey);
|
||||
await resp?.value.reloadAccountData(client, group);
|
||||
await resp?.value.reloadAccountData(client);
|
||||
Object.assign(this, resp?.value);
|
||||
return { value: resp!.value, slot: resp!.slot };
|
||||
}
|
||||
|
||||
async reloadAccountData(
|
||||
client: MangoClient,
|
||||
group: Group,
|
||||
): Promise<MangoAccount> {
|
||||
this.accountData = await client.computeAccountData(group, this);
|
||||
async reloadAccountData(client: MangoClient): Promise<MangoAccount> {
|
||||
const serum3Active = this.serum3Active();
|
||||
const ais =
|
||||
await client.program.provider.connection.getMultipleAccountsInfo(
|
||||
serum3Active.map((serum3) => serum3.openOrders),
|
||||
);
|
||||
this.serum3OosMapByMarketIndex = new Map(
|
||||
Array.from(
|
||||
ais.map((ai, i) => {
|
||||
if (!ai) {
|
||||
throw new Error(
|
||||
`Undefined AI for open orders ${serum3Active[i].openOrders} and market ${serum3Active[i].marketIndex}!`,
|
||||
);
|
||||
}
|
||||
const oo = OpenOrders.fromAccountInfo(
|
||||
serum3Active[i].openOrders,
|
||||
ai,
|
||||
SERUM3_PROGRAM_ID[client.cluster],
|
||||
);
|
||||
return [serum3Active[i].marketIndex, oo];
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -132,14 +150,26 @@ export class MangoAccount {
|
|||
);
|
||||
}
|
||||
|
||||
findToken(tokenIndex: number): TokenPosition | undefined {
|
||||
getToken(tokenIndex: TokenIndex): TokenPosition | undefined {
|
||||
return this.tokens.find((ta) => ta.tokenIndex == tokenIndex);
|
||||
}
|
||||
|
||||
findSerum3Account(marketIndex: number): Serum3Orders | undefined {
|
||||
getSerum3Account(marketIndex: MarketIndex): Serum3Orders | undefined {
|
||||
return this.serum3.find((sa) => sa.marketIndex == marketIndex);
|
||||
}
|
||||
|
||||
getSerum3OoAccount(marketIndex: MarketIndex): OpenOrders {
|
||||
const oo: OpenOrders | undefined =
|
||||
this.serum3OosMapByMarketIndex.get(marketIndex);
|
||||
|
||||
if (!oo) {
|
||||
throw new Error(
|
||||
`Open orders account not loaded for market with marketIndex ${marketIndex}!`,
|
||||
);
|
||||
}
|
||||
return oo;
|
||||
}
|
||||
|
||||
// How to navigate
|
||||
// * if a function is returning a I80F48, then usually the return value is in native quote or native token, unless specified
|
||||
// * if a function is returning a number, then usually the return value is in ui tokens, unless specified
|
||||
|
@ -152,7 +182,7 @@ export class MangoAccount {
|
|||
* @returns native balance for a token, is signed
|
||||
*/
|
||||
getTokenBalance(bank: Bank): I80F48 {
|
||||
const tp = this.findToken(bank.tokenIndex);
|
||||
const tp = this.getToken(bank.tokenIndex);
|
||||
return tp ? tp.balance(bank) : ZERO_I80F48();
|
||||
}
|
||||
|
||||
|
@ -162,7 +192,7 @@ export class MangoAccount {
|
|||
* @returns native deposits for a token, 0 if position has borrows
|
||||
*/
|
||||
getTokenDeposits(bank: Bank): I80F48 {
|
||||
const tp = this.findToken(bank.tokenIndex);
|
||||
const tp = this.getToken(bank.tokenIndex);
|
||||
return tp ? tp.deposits(bank) : ZERO_I80F48();
|
||||
}
|
||||
|
||||
|
@ -172,7 +202,7 @@ export class MangoAccount {
|
|||
* @returns native borrows for a token, 0 if position has deposits
|
||||
*/
|
||||
getTokenBorrows(bank: Bank): I80F48 {
|
||||
const tp = this.findToken(bank.tokenIndex);
|
||||
const tp = this.getToken(bank.tokenIndex);
|
||||
return tp ? tp.borrows(bank) : ZERO_I80F48();
|
||||
}
|
||||
|
||||
|
@ -182,7 +212,7 @@ export class MangoAccount {
|
|||
* @returns UI balance for a token, is signed
|
||||
*/
|
||||
getTokenBalanceUi(bank: Bank): number {
|
||||
const tp = this.findToken(bank.tokenIndex);
|
||||
const tp = this.getToken(bank.tokenIndex);
|
||||
return tp ? tp.balanceUi(bank) : 0;
|
||||
}
|
||||
|
||||
|
@ -192,7 +222,7 @@ export class MangoAccount {
|
|||
* @returns UI deposits for a token, 0 or more
|
||||
*/
|
||||
getTokenDepositsUi(bank: Bank): number {
|
||||
const ta = this.findToken(bank.tokenIndex);
|
||||
const ta = this.getToken(bank.tokenIndex);
|
||||
return ta ? ta.depositsUi(bank) : 0;
|
||||
}
|
||||
|
||||
|
@ -202,7 +232,7 @@ export class MangoAccount {
|
|||
* @returns UI borrows for a token, 0 or less
|
||||
*/
|
||||
getTokenBorrowsUi(bank: Bank): number {
|
||||
const ta = this.findToken(bank.tokenIndex);
|
||||
const ta = this.getToken(bank.tokenIndex);
|
||||
return ta ? ta.borrowsUi(bank) : 0;
|
||||
}
|
||||
|
||||
|
@ -211,10 +241,9 @@ export class MangoAccount {
|
|||
* @param healthType
|
||||
* @returns raw health number, in native quote
|
||||
*/
|
||||
getHealth(healthType: HealthType): I80F48 | undefined {
|
||||
return healthType == HealthType.init
|
||||
? this.accountData?.initHealth
|
||||
: this.accountData?.maintHealth;
|
||||
getHealth(group: Group, healthType: HealthType): I80F48 {
|
||||
const hc = HealthCache.fromMangoAccount(group, this);
|
||||
return hc.health(healthType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -223,8 +252,9 @@ export class MangoAccount {
|
|||
* @param healthType
|
||||
* @returns health ratio, in percentage form
|
||||
*/
|
||||
getHealthRatio(healthType: HealthType): I80F48 | undefined {
|
||||
return this.accountData?.healthCache.healthRatio(healthType);
|
||||
getHealthRatio(group: Group, healthType: HealthType): I80F48 {
|
||||
const hc = HealthCache.fromMangoAccount(group, this);
|
||||
return hc.healthRatio(healthType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -232,8 +262,8 @@ export class MangoAccount {
|
|||
* @param healthType
|
||||
* @returns health ratio, in percentage form, capped to 100
|
||||
*/
|
||||
getHealthRatioUi(healthType: HealthType): number | undefined {
|
||||
const ratio = this.getHealthRatio(healthType)?.toNumber();
|
||||
getHealthRatioUi(group: Group, healthType: HealthType): number | undefined {
|
||||
const ratio = this.getHealthRatio(group, healthType).toNumber();
|
||||
if (ratio) {
|
||||
return ratio > 100 ? 100 : Math.trunc(ratio);
|
||||
} else {
|
||||
|
@ -245,40 +275,73 @@ export class MangoAccount {
|
|||
* Sum of all the assets i.e. token deposits, borrows, total assets in spot open orders, (perps positions is todo) in terms of quote value.
|
||||
* @returns equity, in native quote
|
||||
*/
|
||||
getEquity(): I80F48 | undefined {
|
||||
if (this.accountData) {
|
||||
const equity = this.accountData.equity;
|
||||
const total_equity = equity.tokens.reduce(
|
||||
(a, b) => a.add(b.value),
|
||||
ZERO_I80F48(),
|
||||
);
|
||||
return total_equity;
|
||||
getEquity(group: Group): I80F48 {
|
||||
const tokensMap = new Map<number, I80F48>();
|
||||
for (const tp of this.tokensActive()) {
|
||||
const bank = group.getFirstBankByTokenIndex(tp.tokenIndex);
|
||||
tokensMap.set(tp.tokenIndex, tp.balance(bank).mul(bank.price));
|
||||
}
|
||||
return undefined;
|
||||
|
||||
for (const sp of this.serum3Active()) {
|
||||
const oo = this.getSerum3OoAccount(sp.marketIndex);
|
||||
const baseBank = group.getFirstBankByTokenIndex(sp.baseTokenIndex);
|
||||
tokensMap
|
||||
.get(baseBank.tokenIndex)!
|
||||
.iadd(
|
||||
I80F48.fromString(oo.baseTokenTotal.toString()).mul(baseBank.price),
|
||||
);
|
||||
const quoteBank = group.getFirstBankByTokenIndex(sp.quoteTokenIndex);
|
||||
// NOTE: referrerRebatesAccrued is not declared on oo class, but the layout
|
||||
// is aware of it
|
||||
tokensMap
|
||||
.get(baseBank.tokenIndex)!
|
||||
.iadd(
|
||||
I80F48.fromString(
|
||||
oo.quoteTokenTotal
|
||||
.add((oo as any).referrerRebatesAccrued)
|
||||
.toString(),
|
||||
).mul(quoteBank.price),
|
||||
);
|
||||
}
|
||||
|
||||
const tokenEquity = Array.from(tokensMap.values()).reduce(
|
||||
(a, b) => a.add(b),
|
||||
ZERO_I80F48(),
|
||||
);
|
||||
|
||||
const perpEquity = this.perpActive().reduce(
|
||||
(a, b) =>
|
||||
a.add(b.getEquity(group.getPerpMarketByMarketIndex(b.marketIndex))),
|
||||
ZERO_I80F48(),
|
||||
);
|
||||
|
||||
return tokenEquity.add(perpEquity);
|
||||
}
|
||||
|
||||
/**
|
||||
* The amount of native quote you could withdraw against your existing assets.
|
||||
* @returns collateral value, in native quote
|
||||
*/
|
||||
getCollateralValue(): I80F48 | undefined {
|
||||
return this.getHealth(HealthType.init);
|
||||
getCollateralValue(group: Group): I80F48 {
|
||||
return this.getHealth(group, HealthType.init);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum of all positive assets.
|
||||
* @returns assets, in native quote
|
||||
*/
|
||||
getAssetsValue(healthType: HealthType): I80F48 | undefined {
|
||||
return this.accountData?.healthCache.assets(healthType);
|
||||
getAssetsValue(group: Group, healthType: HealthType): I80F48 {
|
||||
const hc = HealthCache.fromMangoAccount(group, this);
|
||||
return hc.assets(healthType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum of all negative assets.
|
||||
* @returns liabs, in native quote
|
||||
*/
|
||||
getLiabsValue(healthType: HealthType): I80F48 | undefined {
|
||||
return this.accountData?.healthCache.liabs(healthType);
|
||||
getLiabsValue(group: Group, healthType: HealthType): I80F48 {
|
||||
const hc = HealthCache.fromMangoAccount(group, this);
|
||||
return hc.liabs(healthType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -286,8 +349,8 @@ export class MangoAccount {
|
|||
* PNL is defined here as spot value + serum3 open orders value + perp value - net deposits value (evaluated at native quote price at the time of the deposit/withdraw)
|
||||
* spot value + serum3 open orders value + perp value is returned by getEquity (open orders values are added to spot token values implicitly)
|
||||
*/
|
||||
getPnl(): I80F48 | undefined {
|
||||
return this.getEquity()?.add(
|
||||
getPnl(group: Group): I80F48 {
|
||||
return this.getEquity(group)?.add(
|
||||
I80F48.fromI64(this.netDeposits).mul(I80F48.fromNumber(-1)),
|
||||
);
|
||||
}
|
||||
|
@ -301,7 +364,7 @@ export class MangoAccount {
|
|||
mintPk: PublicKey,
|
||||
): I80F48 | undefined {
|
||||
const tokenBank: Bank = group.getFirstBankByMint(mintPk);
|
||||
const initHealth = this.accountData?.initHealth;
|
||||
const initHealth = this.getHealth(group, HealthType.init);
|
||||
|
||||
if (!initHealth) return undefined;
|
||||
|
||||
|
@ -314,8 +377,7 @@ export class MangoAccount {
|
|||
// Deposits need special treatment since they would neither count towards liabilities
|
||||
// nor would be charged loanOriginationFeeRate when withdrawn
|
||||
|
||||
const tp = this.findToken(tokenBank.tokenIndex);
|
||||
if (!tokenBank.price) return undefined;
|
||||
const tp = this.getToken(tokenBank.tokenIndex);
|
||||
const existingTokenDeposits = tp ? tp.deposits(tokenBank) : ZERO_I80F48();
|
||||
let existingPositionHealthContrib = ZERO_I80F48();
|
||||
if (existingTokenDeposits.gt(ZERO_I80F48())) {
|
||||
|
@ -377,18 +439,14 @@ export class MangoAccount {
|
|||
targetMintPk: PublicKey,
|
||||
priceFactor: number,
|
||||
): number | undefined {
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
if (sourceMintPk.equals(targetMintPk)) {
|
||||
return 0;
|
||||
}
|
||||
const maxSource = this.accountData.healthCache.getMaxSourceForTokenSwap(
|
||||
const hc = HealthCache.fromMangoAccount(group, this);
|
||||
const maxSource = hc.getMaxSourceForTokenSwap(
|
||||
group.getFirstBankByMint(sourceMintPk),
|
||||
group.getFirstBankByMint(targetMintPk),
|
||||
ONE_I80F48(), // target 1% health
|
||||
I80F48.fromNumber(2), // target 2% health
|
||||
I80F48.fromNumber(priceFactor),
|
||||
);
|
||||
maxSource.idiv(
|
||||
|
@ -426,7 +484,8 @@ export class MangoAccount {
|
|||
mintPk: tokenChange.mintPk,
|
||||
};
|
||||
});
|
||||
return this.accountData?.healthCache
|
||||
const hc = HealthCache.fromMangoAccount(group, this);
|
||||
return hc
|
||||
.simHealthRatioWithTokenPositionChanges(
|
||||
group,
|
||||
nativeTokenChanges,
|
||||
|
@ -440,14 +499,8 @@ export class MangoAccount {
|
|||
group: Group,
|
||||
externalMarketPk: PublicKey,
|
||||
): Promise<Order[]> {
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
);
|
||||
if (!serum3Market) {
|
||||
throw new Error(
|
||||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
||||
);
|
||||
}
|
||||
const serum3Market =
|
||||
group.getSerum3MarketByExternalMarket(externalMarketPk);
|
||||
const serum3OO = this.serum3Active().find(
|
||||
(s) => s.marketIndex === serum3Market.marketIndex,
|
||||
);
|
||||
|
@ -455,7 +508,7 @@ export class MangoAccount {
|
|||
throw new Error(`No open orders account found for ${externalMarketPk}`);
|
||||
}
|
||||
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
const serum3MarketExternal = group.serum3ExternalMarketsMap.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
const [bidsInfo, asksInfo] =
|
||||
|
@ -463,9 +516,14 @@ export class MangoAccount {
|
|||
serum3MarketExternal.bidsAddress,
|
||||
serum3MarketExternal.asksAddress,
|
||||
]);
|
||||
if (!bidsInfo || !asksInfo) {
|
||||
if (!bidsInfo) {
|
||||
throw new Error(
|
||||
`bids and asks ai were not fetched for ${externalMarketPk.toString()}`,
|
||||
`Undefined bidsInfo for serum3Market with externalMarket ${externalMarketPk.toString()!}`,
|
||||
);
|
||||
}
|
||||
if (!asksInfo) {
|
||||
throw new Error(
|
||||
`Undefined asksInfo for serum3Market with externalMarket ${externalMarketPk.toString()!}`,
|
||||
);
|
||||
}
|
||||
const bids = Orderbook.decode(serum3MarketExternal, bidsInfo.data);
|
||||
|
@ -476,7 +534,6 @@ export class MangoAccount {
|
|||
}
|
||||
|
||||
/**
|
||||
* TODO priceFactor
|
||||
* @param group
|
||||
* @param externalMarketPk
|
||||
* @returns maximum ui quote which can be traded for base token given current health
|
||||
|
@ -485,26 +542,28 @@ export class MangoAccount {
|
|||
group: Group,
|
||||
externalMarketPk: PublicKey,
|
||||
): number {
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
const serum3Market =
|
||||
group.getSerum3MarketByExternalMarket(externalMarketPk);
|
||||
const baseBank = group.getFirstBankByTokenIndex(
|
||||
serum3Market.baseTokenIndex,
|
||||
);
|
||||
if (!serum3Market) {
|
||||
throw new Error(
|
||||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
||||
);
|
||||
}
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
const nativeAmount =
|
||||
this.accountData.healthCache.getMaxSerum3OrderForHealthRatio(
|
||||
group,
|
||||
serum3Market,
|
||||
Serum3Side.bid,
|
||||
I80F48.fromNumber(1),
|
||||
);
|
||||
const quoteBank = group.getFirstBankByTokenIndex(
|
||||
serum3Market.quoteTokenIndex,
|
||||
);
|
||||
const hc = HealthCache.fromMangoAccount(group, this);
|
||||
let nativeAmount = hc.getMaxSerum3OrderForHealthRatio(
|
||||
baseBank,
|
||||
quoteBank,
|
||||
serum3Market,
|
||||
Serum3Side.bid,
|
||||
I80F48.fromNumber(2),
|
||||
);
|
||||
// If its a bid then the reserved fund and potential loan is in base
|
||||
// also keep some buffer for fees, use taker fees for worst case simulation.
|
||||
nativeAmount = nativeAmount
|
||||
.div(quoteBank.price)
|
||||
.div(ONE_I80F48().add(baseBank.loanOriginationFeeRate))
|
||||
.div(ONE_I80F48().add(I80F48.fromNumber(group.getSerum3FeeRates(false))));
|
||||
return toUiDecimals(
|
||||
nativeAmount,
|
||||
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex).mintDecimals,
|
||||
|
@ -512,7 +571,6 @@ export class MangoAccount {
|
|||
}
|
||||
|
||||
/**
|
||||
* TODO priceFactor
|
||||
* @param group
|
||||
* @param externalMarketPk
|
||||
* @returns maximum ui base which can be traded for quote token given current health
|
||||
|
@ -521,26 +579,28 @@ export class MangoAccount {
|
|||
group: Group,
|
||||
externalMarketPk: PublicKey,
|
||||
): number {
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
const serum3Market =
|
||||
group.getSerum3MarketByExternalMarket(externalMarketPk);
|
||||
const baseBank = group.getFirstBankByTokenIndex(
|
||||
serum3Market.baseTokenIndex,
|
||||
);
|
||||
if (!serum3Market) {
|
||||
throw new Error(
|
||||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
||||
);
|
||||
}
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
const nativeAmount =
|
||||
this.accountData.healthCache.getMaxSerum3OrderForHealthRatio(
|
||||
group,
|
||||
serum3Market,
|
||||
Serum3Side.ask,
|
||||
I80F48.fromNumber(1),
|
||||
);
|
||||
const quoteBank = group.getFirstBankByTokenIndex(
|
||||
serum3Market.quoteTokenIndex,
|
||||
);
|
||||
const hc = HealthCache.fromMangoAccount(group, this);
|
||||
let nativeAmount = hc.getMaxSerum3OrderForHealthRatio(
|
||||
baseBank,
|
||||
quoteBank,
|
||||
serum3Market,
|
||||
Serum3Side.ask,
|
||||
I80F48.fromNumber(2),
|
||||
);
|
||||
// If its a ask then the reserved fund and potential loan is in base
|
||||
// also keep some buffer for fees, use taker fees for worst case simulation.
|
||||
nativeAmount = nativeAmount
|
||||
.div(baseBank.price)
|
||||
.div(ONE_I80F48().add(baseBank.loanOriginationFeeRate))
|
||||
.div(ONE_I80F48().add(I80F48.fromNumber(group.getSerum3FeeRates(false))));
|
||||
return toUiDecimals(
|
||||
nativeAmount,
|
||||
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex).mintDecimals,
|
||||
|
@ -561,22 +621,19 @@ export class MangoAccount {
|
|||
externalMarketPk: PublicKey,
|
||||
healthType: HealthType = HealthType.init,
|
||||
): number {
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
const serum3Market =
|
||||
group.getSerum3MarketByExternalMarket(externalMarketPk);
|
||||
const baseBank = group.getFirstBankByTokenIndex(
|
||||
serum3Market.baseTokenIndex,
|
||||
);
|
||||
if (!serum3Market) {
|
||||
throw new Error(
|
||||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
||||
);
|
||||
}
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
return this.accountData.healthCache
|
||||
const quoteBank = group.getFirstBankByTokenIndex(
|
||||
serum3Market.quoteTokenIndex,
|
||||
);
|
||||
const hc = HealthCache.fromMangoAccount(group, this);
|
||||
return hc
|
||||
.simHealthRatioWithSerum3BidChanges(
|
||||
group,
|
||||
baseBank,
|
||||
quoteBank,
|
||||
toNative(
|
||||
uiQuoteAmount,
|
||||
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex)
|
||||
|
@ -602,22 +659,19 @@ export class MangoAccount {
|
|||
externalMarketPk: PublicKey,
|
||||
healthType: HealthType = HealthType.init,
|
||||
): number {
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
const serum3Market =
|
||||
group.getSerum3MarketByExternalMarket(externalMarketPk);
|
||||
const baseBank = group.getFirstBankByTokenIndex(
|
||||
serum3Market.baseTokenIndex,
|
||||
);
|
||||
if (!serum3Market) {
|
||||
throw new Error(
|
||||
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
|
||||
);
|
||||
}
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
return this.accountData.healthCache
|
||||
const quoteBank = group.getFirstBankByTokenIndex(
|
||||
serum3Market.quoteTokenIndex,
|
||||
);
|
||||
const hc = HealthCache.fromMangoAccount(group, this);
|
||||
return hc
|
||||
.simHealthRatioWithSerum3AskChanges(
|
||||
group,
|
||||
baseBank,
|
||||
quoteBank,
|
||||
toNative(
|
||||
uiBaseAmount,
|
||||
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
|
||||
|
@ -638,28 +692,21 @@ export class MangoAccount {
|
|||
*/
|
||||
public getMaxQuoteForPerpBidUi(
|
||||
group: Group,
|
||||
perpMarketName: string,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
uiPrice: number,
|
||||
): number {
|
||||
const perpMarket = group.perpMarketsMap.get(perpMarketName);
|
||||
if (!perpMarket) {
|
||||
throw new Error(`PerpMarket for ${perpMarketName} not found!`);
|
||||
}
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
const baseLots = this.accountData.healthCache.getMaxPerpForHealthRatio(
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
const hc = HealthCache.fromMangoAccount(group, this);
|
||||
const baseLots = hc.getMaxPerpForHealthRatio(
|
||||
perpMarket,
|
||||
PerpOrderSide.bid,
|
||||
I80F48.fromNumber(1),
|
||||
I80F48.fromNumber(2),
|
||||
group.toNativePrice(uiPrice, perpMarket.baseDecimals),
|
||||
);
|
||||
const nativeBase = baseLots.mul(
|
||||
I80F48.fromString(perpMarket.baseLotSize.toString()),
|
||||
);
|
||||
const nativeQuote = nativeBase.mul(I80F48.fromNumber(perpMarket.price));
|
||||
const nativeQuote = nativeBase.mul(perpMarket.price);
|
||||
return toUiDecimalsForQuote(nativeQuote.toNumber());
|
||||
}
|
||||
|
||||
|
@ -672,22 +719,15 @@ export class MangoAccount {
|
|||
*/
|
||||
public getMaxBaseForPerpAskUi(
|
||||
group: Group,
|
||||
perpMarketName: string,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
uiPrice: number,
|
||||
): number {
|
||||
const perpMarket = group.perpMarketsMap.get(perpMarketName);
|
||||
if (!perpMarket) {
|
||||
throw new Error(`PerpMarket for ${perpMarketName} not found!`);
|
||||
}
|
||||
if (!this.accountData) {
|
||||
throw new Error(
|
||||
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
|
||||
);
|
||||
}
|
||||
const baseLots = this.accountData.healthCache.getMaxPerpForHealthRatio(
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
const hc = HealthCache.fromMangoAccount(group, this);
|
||||
const baseLots = hc.getMaxPerpForHealthRatio(
|
||||
perpMarket,
|
||||
PerpOrderSide.ask,
|
||||
I80F48.fromNumber(1),
|
||||
I80F48.fromNumber(2),
|
||||
group.toNativePrice(uiPrice, perpMarket.baseDecimals),
|
||||
);
|
||||
return perpMarket.baseLotsToUi(new BN(baseLots.toString()));
|
||||
|
@ -696,12 +736,9 @@ export class MangoAccount {
|
|||
public async loadPerpOpenOrdersForMarket(
|
||||
client: MangoClient,
|
||||
group: Group,
|
||||
perpMarketName: string,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
): Promise<PerpOrder[]> {
|
||||
const perpMarket = group.perpMarketsMap.get(perpMarketName);
|
||||
if (!perpMarket) {
|
||||
throw new Error(`Perp Market ${perpMarketName} not found!`);
|
||||
}
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
const [bids, asks] = await Promise.all([
|
||||
perpMarket.loadBids(client),
|
||||
perpMarket.loadAsks(client),
|
||||
|
@ -759,17 +796,17 @@ export class MangoAccount {
|
|||
|
||||
export class TokenPosition {
|
||||
static TokenIndexUnset = 65535;
|
||||
static from(dto: TokenPositionDto) {
|
||||
static from(dto: TokenPositionDto): TokenPosition {
|
||||
return new TokenPosition(
|
||||
I80F48.from(dto.indexedPosition),
|
||||
dto.tokenIndex,
|
||||
dto.tokenIndex as TokenIndex,
|
||||
dto.inUseCount,
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
public indexedPosition: I80F48,
|
||||
public tokenIndex: number,
|
||||
public tokenIndex: TokenIndex,
|
||||
public inUseCount: number,
|
||||
) {}
|
||||
|
||||
|
@ -877,17 +914,17 @@ export class Serum3Orders {
|
|||
static from(dto: Serum3PositionDto): Serum3Orders {
|
||||
return new Serum3Orders(
|
||||
dto.openOrders,
|
||||
dto.marketIndex,
|
||||
dto.baseTokenIndex,
|
||||
dto.quoteTokenIndex,
|
||||
dto.marketIndex as MarketIndex,
|
||||
dto.baseTokenIndex as TokenIndex,
|
||||
dto.quoteTokenIndex as TokenIndex,
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
public openOrders: PublicKey,
|
||||
public marketIndex: number,
|
||||
public baseTokenIndex: number,
|
||||
public quoteTokenIndex: number,
|
||||
public marketIndex: MarketIndex,
|
||||
public baseTokenIndex: TokenIndex,
|
||||
public quoteTokenIndex: TokenIndex,
|
||||
) {}
|
||||
|
||||
public isActive(): boolean {
|
||||
|
@ -907,31 +944,77 @@ export class Serum3PositionDto {
|
|||
|
||||
export class PerpPosition {
|
||||
static PerpMarketIndexUnset = 65535;
|
||||
static from(dto: PerpPositionDto) {
|
||||
static from(dto: PerpPositionDto): PerpPosition {
|
||||
return new PerpPosition(
|
||||
dto.marketIndex,
|
||||
dto.marketIndex as PerpMarketIndex,
|
||||
dto.basePositionLots.toNumber(),
|
||||
dto.quotePositionNative.val,
|
||||
I80F48.from(dto.quotePositionNative),
|
||||
dto.bidsBaseLots.toNumber(),
|
||||
dto.asksBaseLots.toNumber(),
|
||||
dto.takerBaseLots.toNumber(),
|
||||
dto.takerQuoteLots.toNumber(),
|
||||
I80F48.from(dto.longSettledFunding),
|
||||
I80F48.from(dto.shortSettledFunding),
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
public marketIndex: number,
|
||||
public marketIndex: PerpMarketIndex,
|
||||
public basePositionLots: number,
|
||||
public quotePositionNative: BN,
|
||||
public quotePositionNative: I80F48,
|
||||
public bidsBaseLots: number,
|
||||
public asksBaseLots: number,
|
||||
public takerBaseLots: number,
|
||||
public takerQuoteLots: number,
|
||||
public longSettledFunding: I80F48,
|
||||
public shortSettledFunding: I80F48,
|
||||
) {}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.marketIndex != PerpPosition.PerpMarketIndexUnset;
|
||||
}
|
||||
|
||||
public unsettledFunding(perpMarket: PerpMarket): I80F48 {
|
||||
if (this.basePositionLots > 0) {
|
||||
return perpMarket.longFunding
|
||||
.sub(this.longSettledFunding)
|
||||
.mul(I80F48.fromString(this.basePositionLots.toString()));
|
||||
} else if (this.basePositionLots < 0) {
|
||||
return perpMarket.shortFunding
|
||||
.sub(this.shortSettledFunding)
|
||||
.mul(I80F48.fromString(this.basePositionLots.toString()));
|
||||
}
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
|
||||
public getEquity(perpMarket: PerpMarket): I80F48 {
|
||||
const lotsToQuote = I80F48.fromString(
|
||||
perpMarket.baseLotSize.toString(),
|
||||
).mul(perpMarket.price);
|
||||
|
||||
const baseLots = I80F48.fromNumber(
|
||||
this.basePositionLots + this.takerBaseLots,
|
||||
);
|
||||
|
||||
const unsettledFunding = this.unsettledFunding(perpMarket);
|
||||
const takerQuote = I80F48.fromString(
|
||||
new BN(this.takerQuoteLots).mul(perpMarket.quoteLotSize).toString(),
|
||||
);
|
||||
const quoteCurrent = I80F48.fromString(this.quotePositionNative.toString())
|
||||
.sub(unsettledFunding)
|
||||
.add(takerQuote);
|
||||
|
||||
return baseLots.mul(lotsToQuote).add(quoteCurrent);
|
||||
}
|
||||
|
||||
public hasOpenOrders(): boolean {
|
||||
return (
|
||||
this.asksBaseLots != 0 ||
|
||||
this.bidsBaseLots != 0 ||
|
||||
this.takerBaseLots != 0 ||
|
||||
this.takerQuoteLots != 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class PerpPositionDto {
|
||||
|
@ -944,12 +1027,14 @@ export class PerpPositionDto {
|
|||
public asksBaseLots: BN,
|
||||
public takerBaseLots: BN,
|
||||
public takerQuoteLots: BN,
|
||||
public longSettledFunding: I80F48Dto,
|
||||
public shortSettledFunding: I80F48Dto,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class PerpOo {
|
||||
static OrderMarketUnset = 65535;
|
||||
static from(dto: PerpOoDto) {
|
||||
static from(dto: PerpOoDto): PerpOo {
|
||||
return new PerpOo(
|
||||
dto.orderSide,
|
||||
dto.orderMarket,
|
||||
|
@ -978,66 +1063,3 @@ export class HealthType {
|
|||
static maint = { maint: {} };
|
||||
static init = { init: {} };
|
||||
}
|
||||
|
||||
export class MangoAccountData {
|
||||
constructor(
|
||||
public healthCache: HealthCache,
|
||||
public initHealth: I80F48,
|
||||
public maintHealth: I80F48,
|
||||
public equity: Equity,
|
||||
) {}
|
||||
|
||||
static from(event: {
|
||||
healthCache: HealthCacheDto;
|
||||
initHealth: I80F48Dto;
|
||||
maintHealth: I80F48Dto;
|
||||
equity: {
|
||||
tokens: [{ tokenIndex: number; value: I80F48Dto }];
|
||||
perps: [{ perpMarketIndex: number; value: I80F48Dto }];
|
||||
};
|
||||
initHealthLiabs: I80F48Dto;
|
||||
tokenAssets: any;
|
||||
}) {
|
||||
return new MangoAccountData(
|
||||
HealthCache.fromDto(event.healthCache),
|
||||
I80F48.from(event.initHealth),
|
||||
I80F48.from(event.maintHealth),
|
||||
Equity.from(event.equity),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class Equity {
|
||||
public constructor(
|
||||
public tokens: TokenEquity[],
|
||||
public perps: PerpEquity[],
|
||||
) {}
|
||||
|
||||
static from(dto: EquityDto): Equity {
|
||||
return new Equity(
|
||||
dto.tokens.map(
|
||||
(token) => new TokenEquity(token.tokenIndex, I80F48.from(token.value)),
|
||||
),
|
||||
dto.perps.map(
|
||||
(perpAccount) =>
|
||||
new PerpEquity(
|
||||
perpAccount.perpMarketIndex,
|
||||
I80F48.from(perpAccount.value),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class TokenEquity {
|
||||
public constructor(public tokenIndex: number, public value: I80F48) {}
|
||||
}
|
||||
|
||||
export class PerpEquity {
|
||||
public constructor(public perpMarketIndex: number, public value: I80F48) {}
|
||||
}
|
||||
|
||||
export class EquityDto {
|
||||
tokens: { tokenIndex: number; value: I80F48Dto }[];
|
||||
perps: { perpMarketIndex: number; value: I80F48Dto }[];
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ export async function parseSwitchboardOracle(
|
|||
return parseSwitcboardOracleV1(accountInfo);
|
||||
}
|
||||
|
||||
throw new Error(`Unable to parse switchboard oracle ${accountInfo.owner}`);
|
||||
throw new Error(`Should not be reached!`);
|
||||
}
|
||||
|
||||
export function isSwitchboardOracle(accountInfo: AccountInfo<Buffer>): boolean {
|
||||
|
|
|
@ -3,10 +3,12 @@ import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
|
|||
import { PublicKey } from '@solana/web3.js';
|
||||
import Big from 'big.js';
|
||||
import { MangoClient } from '../client';
|
||||
import { U64_MAX_BN } from '../utils';
|
||||
import { As, U64_MAX_BN } from '../utils';
|
||||
import { OracleConfig, QUOTE_DECIMALS } from './bank';
|
||||
import { I80F48, I80F48Dto } from './I80F48';
|
||||
|
||||
export type PerpMarketIndex = number & As<'perp-market-index'>;
|
||||
|
||||
export class PerpMarket {
|
||||
public name: string;
|
||||
public maintAssetWeight: I80F48;
|
||||
|
@ -18,21 +20,23 @@ export class PerpMarket {
|
|||
public takerFee: I80F48;
|
||||
public minFunding: I80F48;
|
||||
public maxFunding: I80F48;
|
||||
public longFunding: I80F48;
|
||||
public shortFunding: I80F48;
|
||||
public openInterest: number;
|
||||
public seqNum: number;
|
||||
public feesAccrued: I80F48;
|
||||
priceLotsToUiConverter: number;
|
||||
baseLotsToUiConverter: number;
|
||||
quoteLotsToUiConverter: number;
|
||||
public price: number;
|
||||
public uiPrice: number;
|
||||
public _price: I80F48;
|
||||
public _uiPrice: number;
|
||||
|
||||
static from(
|
||||
publicKey: PublicKey,
|
||||
obj: {
|
||||
group: PublicKey;
|
||||
quoteTokenIndex: number;
|
||||
perpMarketIndex: number;
|
||||
trustedMarket: number;
|
||||
name: number[];
|
||||
oracle: PublicKey;
|
||||
oracleConfig: OracleConfig;
|
||||
|
@ -55,7 +59,7 @@ export class PerpMarket {
|
|||
shortFunding: I80F48Dto;
|
||||
fundingLastUpdated: BN;
|
||||
openInterest: BN;
|
||||
seqNum: any; // TODO: ts complains that this is unknown for whatever reason
|
||||
seqNum: BN;
|
||||
feesAccrued: I80F48Dto;
|
||||
bump: number;
|
||||
baseDecimals: number;
|
||||
|
@ -65,8 +69,8 @@ export class PerpMarket {
|
|||
return new PerpMarket(
|
||||
publicKey,
|
||||
obj.group,
|
||||
obj.quoteTokenIndex,
|
||||
obj.perpMarketIndex,
|
||||
obj.perpMarketIndex as PerpMarketIndex,
|
||||
obj.trustedMarket == 1,
|
||||
obj.name,
|
||||
obj.oracle,
|
||||
obj.oracleConfig,
|
||||
|
@ -100,8 +104,8 @@ export class PerpMarket {
|
|||
constructor(
|
||||
public publicKey: PublicKey,
|
||||
public group: PublicKey,
|
||||
public quoteTokenIndex: number,
|
||||
public perpMarketIndex: number,
|
||||
public perpMarketIndex: PerpMarketIndex, // TODO rename to marketIndex?
|
||||
public trustedMarket: boolean,
|
||||
name: number[],
|
||||
public oracle: PublicKey,
|
||||
oracleConfig: OracleConfig,
|
||||
|
@ -140,6 +144,8 @@ export class PerpMarket {
|
|||
this.takerFee = I80F48.from(takerFee);
|
||||
this.minFunding = I80F48.from(minFunding);
|
||||
this.maxFunding = I80F48.from(maxFunding);
|
||||
this.longFunding = I80F48.from(longFunding);
|
||||
this.shortFunding = I80F48.from(shortFunding);
|
||||
this.openInterest = openInterest.toNumber();
|
||||
this.seqNum = seqNum.toNumber();
|
||||
this.feesAccrued = I80F48.from(feesAccrued);
|
||||
|
@ -159,6 +165,23 @@ export class PerpMarket {
|
|||
.toNumber();
|
||||
}
|
||||
|
||||
get price(): I80F48 {
|
||||
if (!this._price) {
|
||||
throw new Error(
|
||||
`Undefined price for perpMarket ${this.publicKey} with marketIndex ${this.perpMarketIndex}!`,
|
||||
);
|
||||
}
|
||||
return this._price;
|
||||
}
|
||||
|
||||
get uiPrice(): number {
|
||||
if (!this._uiPrice) {
|
||||
throw new Error(
|
||||
`Undefined price for perpMarket ${this.publicKey} with marketIndex ${this.perpMarketIndex}!`,
|
||||
);
|
||||
}
|
||||
return this._uiPrice;
|
||||
}
|
||||
public async loadAsks(client: MangoClient): Promise<BookSide> {
|
||||
const asks = await client.program.account.bookSide.fetch(this.asks);
|
||||
return BookSide.from(client, this, BookSideType.asks, asks);
|
||||
|
@ -176,7 +199,10 @@ export class PerpMarket {
|
|||
return new PerpEventQueue(client, eventQueue.header, eventQueue.buf);
|
||||
}
|
||||
|
||||
public async loadFills(client: MangoClient, lastSeqNum: BN) {
|
||||
public async loadFills(
|
||||
client: MangoClient,
|
||||
lastSeqNum: BN,
|
||||
): Promise<(OutEvent | FillEvent | LiquidateEvent)[]> {
|
||||
const eventQueue = await this.loadEventQueue(client);
|
||||
return eventQueue
|
||||
.eventsSince(lastSeqNum)
|
||||
|
@ -189,13 +215,13 @@ export class PerpMarket {
|
|||
* @param asks
|
||||
* @returns returns funding rate per hour
|
||||
*/
|
||||
public getCurrentFundingRate(bids: BookSide, asks: BookSide) {
|
||||
public getCurrentFundingRate(bids: BookSide, asks: BookSide): number {
|
||||
const MIN_FUNDING = this.minFunding.toNumber();
|
||||
const MAX_FUNDING = this.maxFunding.toNumber();
|
||||
|
||||
const bid = bids.getImpactPriceUi(new BN(this.impactQuantity));
|
||||
const ask = asks.getImpactPriceUi(new BN(this.impactQuantity));
|
||||
const indexPrice = this.uiPrice;
|
||||
const indexPrice = this._uiPrice;
|
||||
|
||||
let funding;
|
||||
if (bid !== undefined && ask !== undefined) {
|
||||
|
@ -284,7 +310,7 @@ export class BookSide {
|
|||
leafCount: number;
|
||||
nodes: unknown;
|
||||
},
|
||||
) {
|
||||
): BookSide {
|
||||
return new BookSide(
|
||||
client,
|
||||
perpMarket,
|
||||
|
@ -311,7 +337,6 @@ export class BookSide {
|
|||
public includeExpired = false,
|
||||
maxBookDelay?: number,
|
||||
) {
|
||||
// TODO why? Ask Daffy
|
||||
// Determine the maxTimestamp found on the book to use for tif
|
||||
// If maxBookDelay is not provided, use 3600 as a very large number
|
||||
maxBookDelay = maxBookDelay === undefined ? 3600 : maxBookDelay;
|
||||
|
@ -329,7 +354,7 @@ export class BookSide {
|
|||
this.now = maxTimestamp;
|
||||
}
|
||||
|
||||
static getPriceFromKey(key: BN) {
|
||||
static getPriceFromKey(key: BN): BN {
|
||||
return key.ushrn(64);
|
||||
}
|
||||
|
||||
|
@ -456,7 +481,7 @@ export class LeafNode {
|
|||
) {}
|
||||
}
|
||||
export class InnerNode {
|
||||
static from(obj: { children: [number] }) {
|
||||
static from(obj: { children: [number] }): InnerNode {
|
||||
return new InnerNode(obj.children);
|
||||
}
|
||||
|
||||
|
@ -477,7 +502,11 @@ export class PerpOrderType {
|
|||
}
|
||||
|
||||
export class PerpOrder {
|
||||
static from(perpMarket: PerpMarket, leafNode: LeafNode, type: BookSideType) {
|
||||
static from(
|
||||
perpMarket: PerpMarket,
|
||||
leafNode: LeafNode,
|
||||
type: BookSideType,
|
||||
): PerpOrder {
|
||||
const side =
|
||||
type == BookSideType.bids ? PerpOrderSide.bid : PerpOrderSide.ask;
|
||||
const price = BookSide.getPriceFromKey(leafNode.key);
|
||||
|
@ -555,7 +584,7 @@ export class PerpEventQueue {
|
|||
),
|
||||
);
|
||||
}
|
||||
throw new Error(`Unknown event with eventType ${event.eventType}`);
|
||||
throw new Error(`Unknown event with eventType ${event.eventType}!`);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -4,9 +4,13 @@ import { Cluster, PublicKey } from '@solana/web3.js';
|
|||
import BN from 'bn.js';
|
||||
import { MangoClient } from '../client';
|
||||
import { SERUM3_PROGRAM_ID } from '../constants';
|
||||
import { As } from '../utils';
|
||||
import { TokenIndex } from './bank';
|
||||
import { Group } from './group';
|
||||
import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from './I80F48';
|
||||
|
||||
export type MarketIndex = number & As<'market-index'>;
|
||||
|
||||
export class Serum3Market {
|
||||
public name: string;
|
||||
static from(
|
||||
|
@ -26,12 +30,12 @@ export class Serum3Market {
|
|||
return new Serum3Market(
|
||||
publicKey,
|
||||
obj.group,
|
||||
obj.baseTokenIndex,
|
||||
obj.quoteTokenIndex,
|
||||
obj.baseTokenIndex as TokenIndex,
|
||||
obj.quoteTokenIndex as TokenIndex,
|
||||
obj.name,
|
||||
obj.serumProgram,
|
||||
obj.serumMarketExternal,
|
||||
obj.marketIndex,
|
||||
obj.marketIndex as MarketIndex,
|
||||
obj.registrationTime,
|
||||
);
|
||||
}
|
||||
|
@ -39,12 +43,12 @@ export class Serum3Market {
|
|||
constructor(
|
||||
public publicKey: PublicKey,
|
||||
public group: PublicKey,
|
||||
public baseTokenIndex: number,
|
||||
public quoteTokenIndex: number,
|
||||
public baseTokenIndex: TokenIndex,
|
||||
public quoteTokenIndex: TokenIndex,
|
||||
name: number[],
|
||||
public serumProgram: PublicKey,
|
||||
public serumMarketExternal: PublicKey,
|
||||
public marketIndex: number,
|
||||
public marketIndex: MarketIndex,
|
||||
public registrationTime: BN,
|
||||
) {
|
||||
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
||||
|
@ -58,19 +62,7 @@ export class Serum3Market {
|
|||
*/
|
||||
maxBidLeverage(group: Group): number {
|
||||
const baseBank = group.getFirstBankByTokenIndex(this.baseTokenIndex);
|
||||
if (!baseBank) {
|
||||
throw new Error(
|
||||
`bank for base token with index ${this.baseTokenIndex} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
const quoteBank = group.getFirstBankByTokenIndex(this.quoteTokenIndex);
|
||||
if (!quoteBank) {
|
||||
throw new Error(
|
||||
`bank for quote token with index ${this.quoteTokenIndex} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
quoteBank.initLiabWeight.sub(baseBank.initAssetWeight).lte(ZERO_I80F48())
|
||||
) {
|
||||
|
@ -90,18 +82,7 @@ export class Serum3Market {
|
|||
*/
|
||||
maxAskLeverage(group: Group): number {
|
||||
const baseBank = group.getFirstBankByTokenIndex(this.baseTokenIndex);
|
||||
if (!baseBank) {
|
||||
throw new Error(
|
||||
`bank for base token with index ${this.baseTokenIndex} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
const quoteBank = group.getFirstBankByTokenIndex(this.quoteTokenIndex);
|
||||
if (!quoteBank) {
|
||||
throw new Error(
|
||||
`bank for quote token with index ${this.quoteTokenIndex} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
baseBank.initLiabWeight.sub(quoteBank.initAssetWeight).lte(ZERO_I80F48())
|
||||
|
@ -115,28 +96,18 @@ export class Serum3Market {
|
|||
}
|
||||
|
||||
public async loadBids(client: MangoClient, group: Group): Promise<Orderbook> {
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
this.serumMarketExternal.toBase58(),
|
||||
const serum3MarketExternal = group.getSerum3ExternalMarket(
|
||||
this.serumMarketExternal,
|
||||
);
|
||||
if (!serum3MarketExternal) {
|
||||
throw new Error(
|
||||
`Unable to find serum3MarketExternal for ${this.serumMarketExternal.toBase58()}`,
|
||||
);
|
||||
}
|
||||
return await serum3MarketExternal.loadBids(
|
||||
client.program.provider.connection,
|
||||
);
|
||||
}
|
||||
|
||||
public async loadAsks(client: MangoClient, group: Group): Promise<Orderbook> {
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
this.serumMarketExternal.toBase58(),
|
||||
const serum3MarketExternal = group.getSerum3ExternalMarket(
|
||||
this.serumMarketExternal,
|
||||
);
|
||||
if (!serum3MarketExternal) {
|
||||
throw new Error(
|
||||
`Unable to find serum3MarketExternal for ${this.serumMarketExternal.toBase58()}`,
|
||||
);
|
||||
}
|
||||
return await serum3MarketExternal.loadAsks(
|
||||
client.program.provider.connection,
|
||||
);
|
||||
|
|
|
@ -24,14 +24,13 @@ import {
|
|||
TransactionSignature,
|
||||
} from '@solana/web3.js';
|
||||
import bs58 from 'bs58';
|
||||
import { Bank, MintInfo } from './accounts/bank';
|
||||
import { Bank, MintInfo, TokenIndex } from './accounts/bank';
|
||||
import { Group } from './accounts/group';
|
||||
import { I80F48 } from './accounts/I80F48';
|
||||
import {
|
||||
MangoAccount,
|
||||
MangoAccountData,
|
||||
TokenPosition,
|
||||
PerpPosition,
|
||||
TokenPosition,
|
||||
} from './accounts/mangoAccount';
|
||||
import { StubOracle } from './accounts/oracle';
|
||||
import {
|
||||
|
@ -39,6 +38,7 @@ import {
|
|||
OutEvent,
|
||||
PerpEventQueue,
|
||||
PerpMarket,
|
||||
PerpMarketIndex,
|
||||
PerpOrderSide,
|
||||
PerpOrderType,
|
||||
} from './accounts/perp';
|
||||
|
@ -59,7 +59,6 @@ import {
|
|||
I64_MAX_BN,
|
||||
toNativeDecimals,
|
||||
} from './utils';
|
||||
import { simulate } from './utils/anchor';
|
||||
import { sendTransaction } from './utils/rpc';
|
||||
|
||||
enum AccountRetriever {
|
||||
|
@ -70,7 +69,6 @@ enum AccountRetriever {
|
|||
export type IdsSource = 'api' | 'static' | 'get-program-accounts';
|
||||
|
||||
// TODO: replace ui values with native as input wherever possible
|
||||
// TODO: replace token/market names with token or market indices
|
||||
export class MangoClient {
|
||||
private postSendTxCallback?: ({ txid }) => void;
|
||||
private prioritizationFee: number;
|
||||
|
@ -405,7 +403,7 @@ export class MangoClient {
|
|||
|
||||
public async getMintInfoForTokenIndex(
|
||||
group: Group,
|
||||
tokenIndex: number,
|
||||
tokenIndex: TokenIndex,
|
||||
): Promise<MintInfo[]> {
|
||||
const tokenIndexBuf = Buffer.alloc(2);
|
||||
tokenIndexBuf.writeUInt16LE(tokenIndex);
|
||||
|
@ -649,21 +647,27 @@ export class MangoClient {
|
|||
);
|
||||
}
|
||||
|
||||
public async getMangoAccount(mangoAccount: MangoAccount) {
|
||||
public async getMangoAccount(
|
||||
mangoAccount: MangoAccount,
|
||||
): Promise<MangoAccount> {
|
||||
return MangoAccount.from(
|
||||
mangoAccount.publicKey,
|
||||
await this.program.account.mangoAccount.fetch(mangoAccount.publicKey),
|
||||
);
|
||||
}
|
||||
|
||||
public async getMangoAccountForPublicKey(mangoAccountPk: PublicKey) {
|
||||
public async getMangoAccountForPublicKey(
|
||||
mangoAccountPk: PublicKey,
|
||||
): Promise<MangoAccount> {
|
||||
return MangoAccount.from(
|
||||
mangoAccountPk,
|
||||
await this.program.account.mangoAccount.fetch(mangoAccountPk),
|
||||
);
|
||||
}
|
||||
|
||||
public async getMangoAccountWithSlot(mangoAccountPk: PublicKey) {
|
||||
public async getMangoAccountWithSlot(
|
||||
mangoAccountPk: PublicKey,
|
||||
): Promise<{ slot: number; value: MangoAccount } | undefined> {
|
||||
const resp =
|
||||
await this.program.provider.connection.getAccountInfoAndContext(
|
||||
mangoAccountPk,
|
||||
|
@ -753,52 +757,6 @@ export class MangoClient {
|
|||
);
|
||||
}
|
||||
|
||||
public async computeAccountData(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
): Promise<MangoAccountData | undefined> {
|
||||
const healthRemainingAccounts: PublicKey[] =
|
||||
this.buildHealthRemainingAccounts(
|
||||
AccountRetriever.Fixed,
|
||||
group,
|
||||
[mangoAccount],
|
||||
[],
|
||||
[],
|
||||
);
|
||||
|
||||
// Use our custom simulate fn in utils/anchor.ts so signing the tx is not required
|
||||
this.program.provider.simulate = simulate;
|
||||
|
||||
const res = await this.program.methods
|
||||
.computeAccountData()
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
account: mangoAccount.publicKey,
|
||||
})
|
||||
.remainingAccounts(
|
||||
healthRemainingAccounts.map(
|
||||
(pk) =>
|
||||
({
|
||||
pubkey: pk,
|
||||
isWritable: false,
|
||||
isSigner: false,
|
||||
} as AccountMeta),
|
||||
),
|
||||
)
|
||||
.simulate();
|
||||
|
||||
if (res.events) {
|
||||
const accountDataEvent = res?.events.find(
|
||||
(event) => (event.name = 'MangoAccountData'),
|
||||
);
|
||||
return accountDataEvent
|
||||
? MangoAccountData.from(accountDataEvent.data as any)
|
||||
: undefined;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async tokenDeposit(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
|
@ -820,7 +778,7 @@ export class MangoClient {
|
|||
mangoAccount: MangoAccount,
|
||||
mintPk: PublicKey,
|
||||
nativeAmount: number,
|
||||
) {
|
||||
): Promise<TransactionSignature> {
|
||||
const bank = group.getFirstBankByMint(mintPk);
|
||||
|
||||
const tokenAccountPk = await getAssociatedTokenAddress(
|
||||
|
@ -1148,19 +1106,19 @@ export class MangoClient {
|
|||
orderType: Serum3OrderType,
|
||||
clientOrderId: number,
|
||||
limit: number,
|
||||
) {
|
||||
): Promise<TransactionSignature> {
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
if (!mangoAccount.findSerum3Account(serum3Market.marketIndex)) {
|
||||
if (!mangoAccount.getSerum3Account(serum3Market.marketIndex)) {
|
||||
await this.serum3CreateOpenOrders(
|
||||
group,
|
||||
mangoAccount,
|
||||
serum3Market.serumMarketExternal,
|
||||
);
|
||||
await mangoAccount.reload(this, group);
|
||||
await mangoAccount.reload(this);
|
||||
}
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
const serum3MarketExternal = group.serum3ExternalMarketsMap.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
const serum3MarketExternalVaultSigner =
|
||||
|
@ -1182,13 +1140,17 @@ export class MangoClient {
|
|||
const limitPrice = serum3MarketExternal.priceNumberToLots(price);
|
||||
const maxBaseQuantity = serum3MarketExternal.baseSizeNumberToLots(size);
|
||||
const maxQuoteQuantity = serum3MarketExternal.decoded.quoteLotSize
|
||||
.mul(new BN(1 + group.getFeeRate(orderType === Serum3OrderType.postOnly)))
|
||||
.mul(
|
||||
new BN(
|
||||
1 + group.getSerum3FeeRates(orderType === Serum3OrderType.postOnly),
|
||||
),
|
||||
)
|
||||
.mul(
|
||||
serum3MarketExternal
|
||||
.baseSizeNumberToLots(size)
|
||||
.mul(serum3MarketExternal.priceNumberToLots(price)),
|
||||
);
|
||||
const payerTokenIndex = (() => {
|
||||
const payerTokenIndex = ((): TokenIndex => {
|
||||
if (side == Serum3Side.bid) {
|
||||
return serum3Market.quoteTokenIndex;
|
||||
} else {
|
||||
|
@ -1211,7 +1173,7 @@ export class MangoClient {
|
|||
group: group.publicKey,
|
||||
account: mangoAccount.publicKey,
|
||||
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
openOrders: mangoAccount.findSerum3Account(serum3Market.marketIndex)
|
||||
openOrders: mangoAccount.getSerum3Account(serum3Market.marketIndex)
|
||||
?.openOrders,
|
||||
serumMarket: serum3Market.publicKey,
|
||||
serumProgram: SERUM3_PROGRAM_ID[this.cluster],
|
||||
|
@ -1249,12 +1211,12 @@ export class MangoClient {
|
|||
mangoAccount: MangoAccount,
|
||||
externalMarketPk: PublicKey,
|
||||
limit: number,
|
||||
) {
|
||||
): Promise<TransactionSignature> {
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
const serum3MarketExternal = group.serum3ExternalMarketsMap.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
|
||||
|
@ -1264,7 +1226,7 @@ export class MangoClient {
|
|||
group: group.publicKey,
|
||||
account: mangoAccount.publicKey,
|
||||
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
openOrders: mangoAccount.findSerum3Account(serum3Market.marketIndex)
|
||||
openOrders: mangoAccount.getSerum3Account(serum3Market.marketIndex)
|
||||
?.openOrders,
|
||||
serumMarket: serum3Market.publicKey,
|
||||
serumProgram: SERUM3_PROGRAM_ID[this.cluster],
|
||||
|
@ -1293,7 +1255,7 @@ export class MangoClient {
|
|||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
const serum3MarketExternal = group.serum3ExternalMarketsMap.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
const serum3MarketExternalVaultSigner =
|
||||
|
@ -1309,7 +1271,7 @@ export class MangoClient {
|
|||
group: group.publicKey,
|
||||
account: mangoAccount.publicKey,
|
||||
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
openOrders: mangoAccount.findSerum3Account(serum3Market.marketIndex)
|
||||
openOrders: mangoAccount.getSerum3Account(serum3Market.marketIndex)
|
||||
?.openOrders,
|
||||
serumMarket: serum3Market.publicKey,
|
||||
serumProgram: SERUM3_PROGRAM_ID[this.cluster],
|
||||
|
@ -1349,7 +1311,7 @@ export class MangoClient {
|
|||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
const serum3MarketExternal = group.serum3ExternalMarketsMap.get(
|
||||
externalMarketPk.toBase58(),
|
||||
)!;
|
||||
|
||||
|
@ -1358,7 +1320,7 @@ export class MangoClient {
|
|||
.accounts({
|
||||
group: group.publicKey,
|
||||
account: mangoAccount.publicKey,
|
||||
openOrders: mangoAccount.findSerum3Account(serum3Market.marketIndex)
|
||||
openOrders: mangoAccount.getSerum3Account(serum3Market.marketIndex)
|
||||
?.openOrders,
|
||||
serumMarket: serum3Market.publicKey,
|
||||
serumProgram: SERUM3_PROGRAM_ID[this.cluster],
|
||||
|
@ -1495,7 +1457,7 @@ export class MangoClient {
|
|||
|
||||
async perpEditMarket(
|
||||
group: Group,
|
||||
perpMarketName: string,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
oracle: PublicKey,
|
||||
oracleConfFilter: number,
|
||||
baseDecimals: number,
|
||||
|
@ -1516,7 +1478,7 @@ export class MangoClient {
|
|||
settleFeeAmountThreshold: number,
|
||||
settleFeeFractionLowHealth: number,
|
||||
): Promise<TransactionSignature> {
|
||||
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
|
||||
return await this.program.methods
|
||||
.perpEditMarket(
|
||||
|
@ -1554,9 +1516,9 @@ export class MangoClient {
|
|||
|
||||
async perpCloseMarket(
|
||||
group: Group,
|
||||
perpMarketName: string,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
): Promise<TransactionSignature> {
|
||||
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
|
||||
return await this.program.methods
|
||||
.perpCloseMarket()
|
||||
|
@ -1594,9 +1556,9 @@ export class MangoClient {
|
|||
async perpDeactivatePosition(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
perpMarketName: string,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
): Promise<TransactionSignature> {
|
||||
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
const healthRemainingAccounts: PublicKey[] =
|
||||
this.buildHealthRemainingAccounts(
|
||||
AccountRetriever.Fixed,
|
||||
|
@ -1625,7 +1587,7 @@ export class MangoClient {
|
|||
async perpPlaceOrder(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
perpMarketName: string,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
side: PerpOrderSide,
|
||||
price: number,
|
||||
quantity: number,
|
||||
|
@ -1635,14 +1597,14 @@ export class MangoClient {
|
|||
expiryTimestamp: number,
|
||||
limit: number,
|
||||
): Promise<TransactionSignature> {
|
||||
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
const healthRemainingAccounts: PublicKey[] =
|
||||
this.buildHealthRemainingAccounts(
|
||||
AccountRetriever.Fixed,
|
||||
group,
|
||||
[mangoAccount],
|
||||
// Settlement token bank, because a position for it may be created
|
||||
[group.getFirstBankByTokenIndex(0)],
|
||||
[group.getFirstBankByTokenIndex(0 as TokenIndex)],
|
||||
[perpMarket],
|
||||
);
|
||||
const ix = await this.program.methods
|
||||
|
@ -1689,10 +1651,10 @@ export class MangoClient {
|
|||
async perpCancelAllOrders(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
perpMarketName: string,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
limit: number,
|
||||
): Promise<TransactionSignature> {
|
||||
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
const ix = await this.program.methods
|
||||
.perpCancelAllOrders(limit)
|
||||
.accounts({
|
||||
|
@ -1717,11 +1679,11 @@ export class MangoClient {
|
|||
|
||||
async perpConsumeEvents(
|
||||
group: Group,
|
||||
perpMarketName: string,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
accounts: PublicKey[],
|
||||
limit: number,
|
||||
): Promise<TransactionSignature> {
|
||||
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
return await this.program.methods
|
||||
.perpConsumeEvents(new BN(limit))
|
||||
.accounts({
|
||||
|
@ -1740,10 +1702,10 @@ export class MangoClient {
|
|||
|
||||
async perpConsumeAllEvents(
|
||||
group: Group,
|
||||
perpMarketName: string,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
): Promise<void> {
|
||||
const limit = 8;
|
||||
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
const eventQueue = await perpMarket.loadEventQueue(this);
|
||||
const unconsumedEvents = eventQueue.getUnconsumedEvents();
|
||||
while (unconsumedEvents.length > 0) {
|
||||
|
@ -1762,12 +1724,12 @@ export class MangoClient {
|
|||
case PerpEventQueue.LIQUIDATE_EVENT_TYPE:
|
||||
return [];
|
||||
default:
|
||||
throw new Error(`Unknown event with eventType ${ev.eventType}`);
|
||||
throw new Error(`Unknown event with eventType ${ev.eventType}!`);
|
||||
}
|
||||
})
|
||||
.flat();
|
||||
|
||||
await this.perpConsumeEvents(group, perpMarketName, accounts, limit);
|
||||
await this.perpConsumeEvents(group, perpMarketIndex, accounts, limit);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1793,8 +1755,6 @@ export class MangoClient {
|
|||
const inputBank: Bank = group.getFirstBankByMint(inputMintPk);
|
||||
const outputBank: Bank = group.getFirstBankByMint(outputMintPk);
|
||||
|
||||
if (!inputBank || !outputBank) throw new Error('Invalid token');
|
||||
|
||||
const healthRemainingAccounts: PublicKey[] =
|
||||
this.buildHealthRemainingAccounts(
|
||||
AccountRetriever.Fixed,
|
||||
|
@ -1944,12 +1904,15 @@ export class MangoClient {
|
|||
);
|
||||
}
|
||||
|
||||
async updateIndexAndRate(group: Group, mintPk: PublicKey) {
|
||||
async updateIndexAndRate(
|
||||
group: Group,
|
||||
mintPk: PublicKey,
|
||||
): Promise<TransactionSignature> {
|
||||
// TODO: handle updating multiple banks
|
||||
const bank = group.getFirstBankByMint(mintPk);
|
||||
const mintInfo = group.mintInfosMapByMint.get(mintPk.toString())!;
|
||||
|
||||
await this.program.methods
|
||||
return await this.program.methods
|
||||
.tokenUpdateIndexAndRate()
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
|
@ -1976,7 +1939,7 @@ export class MangoClient {
|
|||
assetMintPk: PublicKey,
|
||||
liabMintPk: PublicKey,
|
||||
maxLiabTransfer: number,
|
||||
) {
|
||||
): Promise<TransactionSignature> {
|
||||
const assetBank: Bank = group.getFirstBankByMint(assetMintPk);
|
||||
const liabBank: Bank = group.getFirstBankByMint(liabMintPk);
|
||||
|
||||
|
@ -2024,7 +1987,11 @@ export class MangoClient {
|
|||
);
|
||||
}
|
||||
|
||||
async altSet(group: Group, addressLookupTable: PublicKey, index: number) {
|
||||
async altSet(
|
||||
group: Group,
|
||||
addressLookupTable: PublicKey,
|
||||
index: number,
|
||||
): Promise<TransactionSignature> {
|
||||
const ix = await this.program.methods
|
||||
.altSet(index)
|
||||
.accounts({
|
||||
|
@ -2049,7 +2016,7 @@ export class MangoClient {
|
|||
addressLookupTable: PublicKey,
|
||||
index: number,
|
||||
pks: PublicKey[],
|
||||
) {
|
||||
): Promise<TransactionSignature> {
|
||||
return await this.program.methods
|
||||
.altExtend(index, pks)
|
||||
.accounts({
|
||||
|
@ -2070,9 +2037,6 @@ export class MangoClient {
|
|||
opts: any = {},
|
||||
getIdsFromApi: IdsSource = 'api',
|
||||
): MangoClient {
|
||||
// TODO: use IDL on chain or in repository? decide...
|
||||
// Alternatively we could fetch IDL from chain.
|
||||
// const idl = await Program.fetchIdl(MANGO_V4_ID, provider);
|
||||
const idl = IDL;
|
||||
|
||||
return new MangoClient(
|
||||
|
@ -2108,8 +2072,7 @@ export class MangoClient {
|
|||
|
||||
/// private
|
||||
|
||||
// todo make private
|
||||
public buildHealthRemainingAccounts(
|
||||
private buildHealthRemainingAccounts(
|
||||
retriever: AccountRetriever,
|
||||
group: Group,
|
||||
mangoAccounts: MangoAccount[],
|
||||
|
@ -2133,8 +2096,7 @@ export class MangoClient {
|
|||
}
|
||||
}
|
||||
|
||||
// todo make private
|
||||
public buildFixedAccountRetrieverHealthAccounts(
|
||||
private buildFixedAccountRetrieverHealthAccounts(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
// Banks and perpMarkets for whom positions don't exist on mango account,
|
||||
|
@ -2207,8 +2169,7 @@ export class MangoClient {
|
|||
return healthRemainingAccounts;
|
||||
}
|
||||
|
||||
// todo make private
|
||||
public buildScanningAccountRetrieverHealthAccounts(
|
||||
private buildScanningAccountRetrieverHealthAccounts(
|
||||
group: Group,
|
||||
mangoAccounts: MangoAccount[],
|
||||
banks: Bank[],
|
||||
|
@ -2216,7 +2177,7 @@ export class MangoClient {
|
|||
): PublicKey[] {
|
||||
const healthRemainingAccounts: PublicKey[] = [];
|
||||
|
||||
let tokenIndices: number[] = [];
|
||||
let tokenIndices: TokenIndex[] = [];
|
||||
for (const mangoAccount of mangoAccounts) {
|
||||
tokenIndices.push(
|
||||
...mangoAccount.tokens
|
||||
|
@ -2241,7 +2202,7 @@ export class MangoClient {
|
|||
...mintInfos.map((mintInfo) => mintInfo.oracle),
|
||||
);
|
||||
|
||||
const perpIndices: number[] = [];
|
||||
const perpIndices: PerpMarketIndex[] = [];
|
||||
for (const mangoAccount of mangoAccounts) {
|
||||
perpIndices.push(
|
||||
...mangoAccount.perps
|
||||
|
|
|
@ -2,6 +2,7 @@ import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
|||
import { Cluster, Connection, Keypair } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { Group } from '../accounts/group';
|
||||
import { HealthCache } from '../accounts/healthCache';
|
||||
import { HealthType, MangoAccount } from '../accounts/mangoAccount';
|
||||
import { PerpMarket } from '../accounts/perp';
|
||||
import { Serum3Market } from '../accounts/serum3';
|
||||
|
@ -26,7 +27,7 @@ async function debugUser(
|
|||
) {
|
||||
console.log(mangoAccount.toString(group));
|
||||
|
||||
await mangoAccount.reload(client, group);
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
console.log(
|
||||
'buildFixedAccountRetrieverHealthAccounts ' +
|
||||
|
@ -45,42 +46,52 @@ async function debugUser(
|
|||
);
|
||||
console.log(
|
||||
'mangoAccount.getEquity() ' +
|
||||
toUiDecimalsForQuote(mangoAccount.getEquity()!.toNumber()),
|
||||
toUiDecimalsForQuote(mangoAccount.getEquity(group)!.toNumber()),
|
||||
);
|
||||
console.log(
|
||||
'mangoAccount.getHealth(HealthType.init) ' +
|
||||
toUiDecimalsForQuote(mangoAccount.getHealth(HealthType.init)!.toNumber()),
|
||||
toUiDecimalsForQuote(
|
||||
mangoAccount.getHealth(group, HealthType.init)!.toNumber(),
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
'HealthCache.fromMangoAccount(group,mangoAccount).health(HealthType.init) ' +
|
||||
toUiDecimalsForQuote(
|
||||
HealthCache.fromMangoAccount(group, mangoAccount)
|
||||
.health(HealthType.init)
|
||||
.toNumber(),
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
'mangoAccount.getHealthRatio(HealthType.init) ' +
|
||||
mangoAccount.getHealthRatio(HealthType.init)!.toNumber(),
|
||||
mangoAccount.getHealthRatio(group, HealthType.init)!.toNumber(),
|
||||
);
|
||||
console.log(
|
||||
'mangoAccount.getHealthRatioUi(HealthType.init) ' +
|
||||
mangoAccount.getHealthRatioUi(HealthType.init),
|
||||
mangoAccount.getHealthRatioUi(group, HealthType.init),
|
||||
);
|
||||
console.log(
|
||||
'mangoAccount.getHealthRatio(HealthType.maint) ' +
|
||||
mangoAccount.getHealthRatio(HealthType.maint)!.toNumber(),
|
||||
mangoAccount.getHealthRatio(group, HealthType.maint)!.toNumber(),
|
||||
);
|
||||
console.log(
|
||||
'mangoAccount.getHealthRatioUi(HealthType.maint) ' +
|
||||
mangoAccount.getHealthRatioUi(HealthType.maint),
|
||||
mangoAccount.getHealthRatioUi(group, HealthType.maint),
|
||||
);
|
||||
console.log(
|
||||
'mangoAccount.getCollateralValue() ' +
|
||||
toUiDecimalsForQuote(mangoAccount.getCollateralValue()!.toNumber()),
|
||||
toUiDecimalsForQuote(mangoAccount.getCollateralValue(group)!.toNumber()),
|
||||
);
|
||||
console.log(
|
||||
'mangoAccount.getAssetsValue() ' +
|
||||
toUiDecimalsForQuote(
|
||||
mangoAccount.getAssetsValue(HealthType.init)!.toNumber(),
|
||||
mangoAccount.getAssetsValue(group, HealthType.init)!.toNumber(),
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
'mangoAccount.getLiabsValue() ' +
|
||||
toUiDecimalsForQuote(
|
||||
mangoAccount.getLiabsValue(HealthType.init)!.toNumber(),
|
||||
mangoAccount.getLiabsValue(group, HealthType.init)!.toNumber(),
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -223,9 +234,9 @@ async function main() {
|
|||
|
||||
for (const mangoAccount of mangoAccounts) {
|
||||
console.log(`MangoAccount ${mangoAccount.publicKey}`);
|
||||
// if (mangoAccount.name === 'PnL Test') {
|
||||
await debugUser(client, group, mangoAccount);
|
||||
// }
|
||||
if (mangoAccount.name === 'PnL Test') {
|
||||
await debugUser(client, group, mangoAccount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ export class Id {
|
|||
|
||||
static fromIdsByName(name: string): Id {
|
||||
const groupConfig = ids.groups.find((id) => id['name'] === name);
|
||||
if (!groupConfig) throw new Error(`Unable to find group config ${name}`);
|
||||
if (!groupConfig) throw new Error(`No group config ${name} found in Ids!`);
|
||||
return new Id(
|
||||
groupConfig.cluster as Cluster,
|
||||
groupConfig.name,
|
||||
|
@ -71,7 +71,7 @@ export class Id {
|
|||
(id) => id['publicKey'] === groupPk.toString(),
|
||||
);
|
||||
if (!groupConfig)
|
||||
throw new Error(`Unable to find group config ${groupPk.toString()}`);
|
||||
throw new Error(`No group config ${groupPk.toString()} found in Ids!`);
|
||||
return new Id(
|
||||
groupConfig.cluster as Cluster,
|
||||
groupConfig.name,
|
||||
|
|
|
@ -567,7 +567,6 @@ async function main() {
|
|||
// );
|
||||
// console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
|
||||
// TODO decide on what keys should go in
|
||||
console.log(`ALT: extending manually with bank publick keys and oracles`);
|
||||
const extendIx = AddressLookupTableProgram.extendLookupTable({
|
||||
lookupTable: createIx[1],
|
||||
|
|
|
@ -78,10 +78,10 @@ async function main() {
|
|||
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString(group));
|
||||
|
||||
await mangoAccount.reload(client, group);
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
// set delegate, and change name
|
||||
if (false) {
|
||||
if (true) {
|
||||
console.log(`...changing mango account name, and setting a delegate`);
|
||||
const randomKey = new PublicKey(
|
||||
'4ZkS7ZZkxfsC3GtvvsHP3DFcUeByU9zzZELS4r8HCELo',
|
||||
|
@ -93,7 +93,7 @@ async function main() {
|
|||
'my_changed_name',
|
||||
randomKey,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
await mangoAccount.reload(client);
|
||||
console.log(mangoAccount.toString());
|
||||
|
||||
console.log(`...resetting mango account name, and re-setting a delegate`);
|
||||
|
@ -103,7 +103,7 @@ async function main() {
|
|||
'my_mango_account',
|
||||
PublicKey.default,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
await mangoAccount.reload(client);
|
||||
console.log(mangoAccount.toString());
|
||||
}
|
||||
|
||||
|
@ -113,7 +113,7 @@ async function main() {
|
|||
`...expanding mango account to have serum3 and perp position slots`,
|
||||
);
|
||||
await client.expandMangoAccount(group, mangoAccount, 8, 8, 8, 8);
|
||||
await mangoAccount.reload(client, group);
|
||||
await mangoAccount.reload(client);
|
||||
}
|
||||
|
||||
// deposit and withdraw
|
||||
|
@ -126,7 +126,7 @@ async function main() {
|
|||
new PublicKey(DEVNET_MINTS.get('USDC')!),
|
||||
50,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
|
@ -134,7 +134,7 @@ async function main() {
|
|||
new PublicKey(DEVNET_MINTS.get('SOL')!),
|
||||
1,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
await client.tokenDeposit(
|
||||
group,
|
||||
|
@ -142,7 +142,7 @@ async function main() {
|
|||
new PublicKey(DEVNET_MINTS.get('MNGO')!),
|
||||
1,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
console.log(`...withdrawing 1 USDC`);
|
||||
await client.tokenWithdraw(
|
||||
|
@ -152,7 +152,7 @@ async function main() {
|
|||
1,
|
||||
true,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
console.log(`...depositing 0.0005 BTC`);
|
||||
await client.tokenDeposit(
|
||||
|
@ -161,7 +161,7 @@ async function main() {
|
|||
new PublicKey(DEVNET_MINTS.get('BTC')!),
|
||||
0.0005,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
console.log(mangoAccount.toString(group));
|
||||
} catch (error) {
|
||||
|
@ -171,12 +171,6 @@ async function main() {
|
|||
|
||||
if (true) {
|
||||
// serum3
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')?.toBase58()!,
|
||||
);
|
||||
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')?.toBase58()!,
|
||||
);
|
||||
const asks = await group.loadSerum3AsksForMarket(
|
||||
client,
|
||||
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
|
||||
|
@ -205,7 +199,7 @@ async function main() {
|
|||
Date.now(),
|
||||
10,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
price = lowestAsk.price + lowestAsk.price / 2;
|
||||
qty = 0.0001;
|
||||
|
@ -224,7 +218,7 @@ async function main() {
|
|||
Date.now(),
|
||||
10,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
price = highestBid.price - highestBid.price / 2;
|
||||
qty = 0.0001;
|
||||
|
@ -291,33 +285,27 @@ async function main() {
|
|||
}
|
||||
|
||||
if (true) {
|
||||
await mangoAccount.reload(client, group);
|
||||
await mangoAccount.reload(client);
|
||||
console.log(
|
||||
'...mangoAccount.getEquity() ' +
|
||||
toUiDecimalsForQuote(mangoAccount.getEquity()!.toNumber()),
|
||||
toUiDecimalsForQuote(mangoAccount.getEquity(group)!.toNumber()),
|
||||
);
|
||||
console.log(
|
||||
'...mangoAccount.getCollateralValue() ' +
|
||||
toUiDecimalsForQuote(mangoAccount.getCollateralValue()!.toNumber()),
|
||||
);
|
||||
console.log(
|
||||
'...mangoAccount.accountData["healthCache"].health(HealthType.init) ' +
|
||||
toUiDecimalsForQuote(
|
||||
mangoAccount
|
||||
.accountData!['healthCache'].health(HealthType.init)
|
||||
.toNumber(),
|
||||
mangoAccount.getCollateralValue(group)!.toNumber(),
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
'...mangoAccount.getAssetsVal() ' +
|
||||
toUiDecimalsForQuote(
|
||||
mangoAccount.getAssetsValue(HealthType.init)!.toNumber(),
|
||||
mangoAccount.getAssetsValue(group, HealthType.init)!.toNumber(),
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
'...mangoAccount.getLiabsVal() ' +
|
||||
toUiDecimalsForQuote(
|
||||
mangoAccount.getLiabsValue(HealthType.init)!.toNumber(),
|
||||
mangoAccount.getLiabsValue(group, HealthType.init)!.toNumber(),
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
|
@ -400,10 +388,11 @@ async function main() {
|
|||
// perps
|
||||
if (true) {
|
||||
let sig;
|
||||
const perpMarket = group.getPerpMarketByName('BTC-PERP');
|
||||
const orders = await mangoAccount.loadPerpOpenOrdersForMarket(
|
||||
client,
|
||||
group,
|
||||
'BTC-PERP',
|
||||
perpMarket.perpMarketIndex,
|
||||
);
|
||||
for (const order of orders) {
|
||||
console.log(
|
||||
|
@ -411,7 +400,12 @@ async function main() {
|
|||
);
|
||||
}
|
||||
console.log(`...cancelling all perp orders`);
|
||||
sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
|
||||
sig = await client.perpCancelAllOrders(
|
||||
group,
|
||||
mangoAccount,
|
||||
perpMarket.perpMarketIndex,
|
||||
10,
|
||||
);
|
||||
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
|
||||
// scenario 1
|
||||
|
@ -423,7 +417,7 @@ async function main() {
|
|||
Math.floor(Math.random() * 100);
|
||||
const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi(
|
||||
group,
|
||||
'BTC-PERP',
|
||||
perpMarket.perpMarketIndex,
|
||||
1,
|
||||
);
|
||||
const baseQty = quoteQty / price;
|
||||
|
@ -433,7 +427,7 @@ async function main() {
|
|||
const sig = await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
'BTC-PERP',
|
||||
perpMarket.perpMarketIndex,
|
||||
PerpOrderSide.bid,
|
||||
price,
|
||||
baseQty,
|
||||
|
@ -448,7 +442,12 @@ async function main() {
|
|||
console.log(error);
|
||||
}
|
||||
console.log(`...cancelling all perp orders`);
|
||||
sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
|
||||
sig = await client.perpCancelAllOrders(
|
||||
group,
|
||||
mangoAccount,
|
||||
perpMarket.perpMarketIndex,
|
||||
10,
|
||||
);
|
||||
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
|
||||
// bid max perp + some
|
||||
|
@ -458,7 +457,11 @@ async function main() {
|
|||
group.banksMapByName.get('BTC')![0].uiPrice! -
|
||||
Math.floor(Math.random() * 100);
|
||||
const quoteQty =
|
||||
mangoAccount.getMaxQuoteForPerpBidUi(group, 'BTC-PERP', 1) * 1.02;
|
||||
mangoAccount.getMaxQuoteForPerpBidUi(
|
||||
group,
|
||||
perpMarket.perpMarketIndex,
|
||||
1,
|
||||
) * 1.02;
|
||||
const baseQty = quoteQty / price;
|
||||
console.log(
|
||||
`...placing max qty * 1.02 perp bid clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
|
||||
|
@ -466,7 +469,7 @@ async function main() {
|
|||
const sig = await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
'BTC-PERP',
|
||||
perpMarket.perpMarketIndex,
|
||||
PerpOrderSide.bid,
|
||||
price,
|
||||
baseQty,
|
||||
|
@ -487,7 +490,11 @@ async function main() {
|
|||
const price =
|
||||
group.banksMapByName.get('BTC')![0].uiPrice! +
|
||||
Math.floor(Math.random() * 100);
|
||||
const baseQty = mangoAccount.getMaxBaseForPerpAskUi(group, 'BTC-PERP', 1);
|
||||
const baseQty = mangoAccount.getMaxBaseForPerpAskUi(
|
||||
group,
|
||||
perpMarket.perpMarketIndex,
|
||||
1,
|
||||
);
|
||||
const quoteQty = baseQty * price;
|
||||
console.log(
|
||||
`...placing max qty perp ask clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
|
||||
|
@ -495,7 +502,7 @@ async function main() {
|
|||
const sig = await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
'BTC-PERP',
|
||||
perpMarket.perpMarketIndex,
|
||||
PerpOrderSide.ask,
|
||||
price,
|
||||
baseQty,
|
||||
|
@ -517,7 +524,11 @@ async function main() {
|
|||
group.banksMapByName.get('BTC')![0].uiPrice! +
|
||||
Math.floor(Math.random() * 100);
|
||||
const baseQty =
|
||||
mangoAccount.getMaxBaseForPerpAskUi(group, 'BTC-PERP', 1) * 1.02;
|
||||
mangoAccount.getMaxBaseForPerpAskUi(
|
||||
group,
|
||||
perpMarket.perpMarketIndex,
|
||||
1,
|
||||
) * 1.02;
|
||||
const quoteQty = baseQty * price;
|
||||
console.log(
|
||||
`...placing max qty perp ask * 1.02 clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
|
||||
|
@ -525,7 +536,7 @@ async function main() {
|
|||
const sig = await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
'BTC-PERP',
|
||||
perpMarket.perpMarketIndex,
|
||||
PerpOrderSide.ask,
|
||||
price,
|
||||
baseQty,
|
||||
|
@ -541,7 +552,12 @@ async function main() {
|
|||
}
|
||||
|
||||
console.log(`...cancelling all perp orders`);
|
||||
sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
|
||||
sig = await client.perpCancelAllOrders(
|
||||
group,
|
||||
mangoAccount,
|
||||
perpMarket.perpMarketIndex,
|
||||
10,
|
||||
);
|
||||
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
|
||||
// // scenario 2
|
||||
|
@ -553,8 +569,8 @@ async function main() {
|
|||
// const sig = await client.perpPlaceOrder(
|
||||
// group,
|
||||
// mangoAccount,
|
||||
// 'BTC-PERP',
|
||||
// PerpOrderSide.bid,
|
||||
// perpMarket.perpMarketIndex,
|
||||
// PerpOrderSide.bid,
|
||||
// price,
|
||||
// 0.01,
|
||||
// price * 0.01,
|
||||
|
@ -574,8 +590,8 @@ async function main() {
|
|||
// const sig = await client.perpPlaceOrder(
|
||||
// group,
|
||||
// mangoAccount,
|
||||
// 'BTC-PERP',
|
||||
// PerpOrderSide.ask,
|
||||
// perpMarket.perpMarketIndex,
|
||||
// PerpOrderSide.ask,
|
||||
// price,
|
||||
// 0.01,
|
||||
// price * 0.011,
|
||||
|
@ -590,11 +606,9 @@ async function main() {
|
|||
// }
|
||||
// // // should be able to cancel them : know bug
|
||||
// // console.log(`...cancelling all perp orders`);
|
||||
// // sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
|
||||
// // sig = await client.perpCancelAllOrders(group, mangoAccount, perpMarket.perpMarketIndex, 10);
|
||||
// // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
|
||||
const perpMarket = group.perpMarketsMap.get('BTC-PERP')!;
|
||||
|
||||
const bids: BookSide = await perpMarket?.loadBids(client)!;
|
||||
console.log(`bids - ${Array.from(bids.items())}`);
|
||||
const asks: BookSide = await perpMarket?.loadAsks(client)!;
|
||||
|
@ -615,7 +629,7 @@ async function main() {
|
|||
|
||||
// make+take orders should have cancelled each other, and if keeper has already cranked, then should not appear in position
|
||||
await group.reloadAll(client);
|
||||
await mangoAccount.reload(client, group);
|
||||
await mangoAccount.reload(client);
|
||||
console.log(`${mangoAccount.toString(group)}`);
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,13 @@ import { PerpMarket } from './accounts/perp';
|
|||
export const U64_MAX_BN = new BN('18446744073709551615');
|
||||
export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64);
|
||||
|
||||
export function debugAccountMetas(ams: AccountMeta[]) {
|
||||
// https://stackoverflow.com/questions/70261755/user-defined-type-guard-function-and-type-narrowing-to-more-specific-type/70262876#70262876
|
||||
export declare abstract class As<Tag extends keyof never> {
|
||||
private static readonly $as$: unique symbol;
|
||||
private [As.$as$]: Record<Tag, true>;
|
||||
}
|
||||
|
||||
export function debugAccountMetas(ams: AccountMeta[]): void {
|
||||
for (const am of ams) {
|
||||
console.log(
|
||||
`${am.pubkey.toBase58()}, isSigner: ${am.isSigner
|
||||
|
@ -39,7 +45,7 @@ export function debugHealthAccounts(
|
|||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
publicKeys: PublicKey[],
|
||||
) {
|
||||
): void {
|
||||
const banks = new Map(
|
||||
Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [
|
||||
banks[0].publicKey.toBase58(),
|
||||
|
@ -66,10 +72,12 @@ export function debugHealthAccounts(
|
|||
}),
|
||||
);
|
||||
const perps = new Map(
|
||||
Array.from(group.perpMarketsMap.values()).map((perpMarket: PerpMarket) => [
|
||||
perpMarket.publicKey.toBase58(),
|
||||
`${perpMarket.name} perp market`,
|
||||
]),
|
||||
Array.from(group.perpMarketsMapByName.values()).map(
|
||||
(perpMarket: PerpMarket) => [
|
||||
perpMarket.publicKey.toBase58(),
|
||||
`${perpMarket.name} perp market`,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
publicKeys.map((pk) => {
|
||||
|
@ -126,7 +134,7 @@ export async function getAssociatedTokenAddress(
|
|||
associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
): Promise<PublicKey> {
|
||||
if (!allowOwnerOffCurve && !PublicKey.isOnCurve(owner.toBuffer()))
|
||||
throw new Error('TokenOwnerOffCurve');
|
||||
throw new Error('TokenOwnerOffCurve!');
|
||||
|
||||
const [address] = await PublicKey.findProgramAddress(
|
||||
[owner.toBuffer(), programId.toBuffer(), mint.toBuffer()],
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -10,7 +10,7 @@ export async function sendTransaction(
|
|||
ixs: TransactionInstruction[],
|
||||
alts: AddressLookupTableAccount[],
|
||||
opts: any = {},
|
||||
) {
|
||||
): Promise<string> {
|
||||
const connection = provider.connection;
|
||||
const latestBlockhash = await connection.getLatestBlockhash(
|
||||
opts.preflightCommitment,
|
||||
|
|
Loading…
Reference in New Issue