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