2022-09-29 06:51:09 -07:00
import { BN } from '@project-serum/anchor' ;
import { OpenOrders } from '@project-serum/serum' ;
2022-08-17 23:48:45 -07:00
import { PublicKey } from '@solana/web3.js' ;
2022-08-11 08:44:12 -07:00
import _ from 'lodash' ;
2022-09-29 06:51:09 -07:00
import { Bank , BankForHealth , TokenIndex } from './bank' ;
2022-08-11 08:44:12 -07:00
import { Group } from './group' ;
import {
HUNDRED_I80F48 ,
I80F48 ,
I80F48Dto ,
MAX_I80F48 ,
ZERO_I80F48 ,
} from './I80F48' ;
2022-09-29 06:51:09 -07:00
import { HealthType , MangoAccount , PerpPosition } from './mangoAccount' ;
2022-09-23 02:43:26 -07:00
import { PerpMarket , PerpOrderSide } from './perp' ;
2022-09-29 06:51:09 -07:00
import { MarketIndex , Serum3Market , Serum3Side } from './serum3' ;
2022-07-13 10:18:55 -07:00
// ░░░░
//
// ██
// ██░░██
// ░░ ░░ ██░░░░░░██ ░░░░
// ██░░░░░░░░░░██
// ██░░░░░░░░░░██
// ██░░░░░░░░░░░░░░██
// ██░░░░░░██████░░░░░░██
// ██░░░░░░██████░░░░░░██
// ██░░░░░░░░██████░░░░░░░░██
// ██░░░░░░░░██████░░░░░░░░██
// ██░░░░░░░░░░██████░░░░░░░░░░██
// ██░░░░░░░░░░░░██████░░░░░░░░░░░░██
// ██░░░░░░░░░░░░██████░░░░░░░░░░░░██
// ██░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░██
// ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██
// ██░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░██
// ██░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░██
// ██░░░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░░░██
// ░░ ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██
// ██████████████████████████████████████████
// warning: this code is copy pasta from rust, keep in sync with health.rs
export class HealthCache {
2022-08-31 02:36:44 -07:00
constructor (
public tokenInfos : TokenInfo [ ] ,
public serum3Infos : Serum3Info [ ] ,
public perpInfos : PerpInfo [ ] ,
) { }
2022-09-29 06:51:09 -07:00
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 {
2022-08-31 02:36:44 -07:00
return new HealthCache (
dto . tokenInfos . map ( ( dto ) = > TokenInfo . fromDto ( dto ) ) ,
dto . serum3Infos . map ( ( dto ) = > Serum3Info . fromDto ( dto ) ) ,
2022-09-23 02:43:26 -07:00
dto . perpInfos . map ( ( dto ) = > PerpInfo . fromDto ( dto ) ) ,
2022-08-31 02:36:44 -07:00
) ;
2022-07-13 10:18:55 -07:00
}
public health ( healthType : HealthType ) : I80F48 {
2022-09-01 00:48:23 -07:00
const health = ZERO_I80F48 ( ) ;
2022-07-13 10:18:55 -07:00
for ( const tokenInfo of this . tokenInfos ) {
2022-08-04 10:42:41 -07:00
const contrib = tokenInfo . healthContribution ( healthType ) ;
2022-09-29 06:51:09 -07:00
// console.log(` - ti ${contrib}`);
2022-09-01 00:48:23 -07:00
health . iadd ( contrib ) ;
2022-07-13 10:18:55 -07:00
}
for ( const serum3Info of this . serum3Infos ) {
2022-08-04 10:42:41 -07:00
const contrib = serum3Info . healthContribution (
healthType ,
this . tokenInfos ,
) ;
2022-09-29 06:51:09 -07:00
// console.log(` - si ${contrib}`);
2022-09-01 00:48:23 -07:00
health . iadd ( contrib ) ;
2022-07-13 10:18:55 -07:00
}
for ( const perpInfo of this . perpInfos ) {
2022-08-04 10:42:41 -07:00
const contrib = perpInfo . healthContribution ( healthType ) ;
2022-09-29 06:51:09 -07:00
// console.log(` - pi ${contrib}`);
2022-09-01 00:48:23 -07:00
health . iadd ( contrib ) ;
2022-07-13 10:18:55 -07:00
}
return health ;
}
2022-08-11 08:44:12 -07:00
public assets ( healthType : HealthType ) : I80F48 {
2022-09-01 00:48:23 -07:00
const assets = ZERO_I80F48 ( ) ;
2022-08-11 08:44:12 -07:00
for ( const tokenInfo of this . tokenInfos ) {
const contrib = tokenInfo . healthContribution ( healthType ) ;
if ( contrib . isPos ( ) ) {
2022-09-01 00:48:23 -07:00
assets . iadd ( contrib ) ;
2022-08-11 08:44:12 -07:00
}
}
for ( const serum3Info of this . serum3Infos ) {
const contrib = serum3Info . healthContribution (
healthType ,
this . tokenInfos ,
) ;
if ( contrib . isPos ( ) ) {
2022-09-01 00:48:23 -07:00
assets . iadd ( contrib ) ;
2022-08-11 08:44:12 -07:00
}
}
for ( const perpInfo of this . perpInfos ) {
const contrib = perpInfo . healthContribution ( healthType ) ;
if ( contrib . isPos ( ) ) {
2022-09-01 00:48:23 -07:00
assets . iadd ( contrib ) ;
2022-08-11 08:44:12 -07:00
}
}
return assets ;
}
public liabs ( healthType : HealthType ) : I80F48 {
2022-09-01 00:48:23 -07:00
const liabs = ZERO_I80F48 ( ) ;
2022-08-11 08:44:12 -07:00
for ( const tokenInfo of this . tokenInfos ) {
const contrib = tokenInfo . healthContribution ( healthType ) ;
if ( contrib . isNeg ( ) ) {
2022-09-01 00:48:23 -07:00
liabs . isub ( contrib ) ;
2022-08-11 08:44:12 -07:00
}
}
for ( const serum3Info of this . serum3Infos ) {
const contrib = serum3Info . healthContribution (
healthType ,
this . tokenInfos ,
) ;
if ( contrib . isNeg ( ) ) {
2022-09-01 00:48:23 -07:00
liabs . isub ( contrib ) ;
2022-08-11 08:44:12 -07:00
}
}
for ( const perpInfo of this . perpInfos ) {
const contrib = perpInfo . healthContribution ( healthType ) ;
if ( contrib . isNeg ( ) ) {
2022-09-01 00:48:23 -07:00
liabs . isub ( contrib ) ;
2022-08-11 08:44:12 -07:00
}
}
return liabs ;
}
public healthRatio ( healthType : HealthType ) : I80F48 {
2022-09-01 00:48:23 -07:00
const assets = ZERO_I80F48 ( ) ;
const liabs = ZERO_I80F48 ( ) ;
2022-08-11 08:44:12 -07:00
for ( const tokenInfo of this . tokenInfos ) {
const contrib = tokenInfo . healthContribution ( healthType ) ;
if ( contrib . isPos ( ) ) {
2022-09-01 00:48:23 -07:00
assets . iadd ( contrib ) ;
2022-08-11 08:44:12 -07:00
} else {
2022-09-01 00:48:23 -07:00
liabs . isub ( contrib ) ;
2022-08-11 08:44:12 -07:00
}
}
for ( const serum3Info of this . serum3Infos ) {
const contrib = serum3Info . healthContribution (
healthType ,
this . tokenInfos ,
) ;
if ( contrib . isPos ( ) ) {
2022-09-01 00:48:23 -07:00
assets . iadd ( contrib ) ;
2022-08-11 08:44:12 -07:00
} else {
2022-09-01 00:48:23 -07:00
liabs . isub ( contrib ) ;
2022-08-11 08:44:12 -07:00
}
}
for ( const perpInfo of this . perpInfos ) {
const contrib = perpInfo . healthContribution ( healthType ) ;
if ( contrib . isPos ( ) ) {
2022-09-01 00:48:23 -07:00
assets . iadd ( contrib ) ;
2022-08-11 08:44:12 -07:00
} else {
2022-09-01 00:48:23 -07:00
liabs . isub ( contrib ) ;
2022-08-11 08:44:12 -07:00
}
}
2022-09-27 08:33:51 -07:00
if ( liabs . gt ( I80F48 . fromNumber ( 0.001 ) ) ) {
2022-09-01 00:48:23 -07:00
return HUNDRED_I80F48 ( ) . mul ( assets . sub ( liabs ) . div ( liabs ) ) ;
2022-08-11 08:44:12 -07:00
} else {
2022-09-01 00:48:23 -07:00
return MAX_I80F48 ( ) ;
2022-08-11 08:44:12 -07:00
}
}
2022-09-29 06:51:09 -07:00
findTokenInfoIndex ( tokenIndex : TokenIndex ) : number {
2022-08-11 08:44:12 -07:00
return this . tokenInfos . findIndex (
2022-09-29 06:51:09 -07:00
( tokenInfo ) = > tokenInfo . tokenIndex === tokenIndex ,
2022-08-11 08:44:12 -07:00
) ;
}
2022-09-23 02:43:26 -07:00
getOrCreateTokenInfoIndex ( bank : BankForHealth ) : number {
2022-08-12 02:05:39 -07:00
const index = this . findTokenInfoIndex ( bank . tokenIndex ) ;
if ( index == - 1 ) {
2022-09-29 06:51:09 -07:00
this . tokenInfos . push ( TokenInfo . fromBank ( bank ) ) ;
2022-08-12 02:05:39 -07:00
}
return this . findTokenInfoIndex ( bank . tokenIndex ) ;
}
2022-09-29 06:51:09 -07:00
findSerum3InfoIndex ( marketIndex : MarketIndex ) : number {
2022-09-23 00:34:08 -07:00
return this . serum3Infos . findIndex (
( serum3Info ) = > serum3Info . marketIndex === marketIndex ,
) ;
}
2022-09-29 06:51:09 -07:00
getOrCreateSerum3InfoIndex (
baseBank : BankForHealth ,
quoteBank : BankForHealth ,
serum3Market : Serum3Market ,
) : number {
2022-09-23 00:34:08 -07:00
const index = this . findSerum3InfoIndex ( serum3Market . marketIndex ) ;
const baseEntryIndex = this . getOrCreateTokenInfoIndex ( baseBank ) ;
const quoteEntryIndex = this . getOrCreateTokenInfoIndex ( quoteBank ) ;
if ( index == - 1 ) {
this . serum3Infos . push (
Serum3Info . emptyFromSerum3Market (
serum3Market ,
baseEntryIndex ,
quoteEntryIndex ,
) ,
) ;
}
return this . findSerum3InfoIndex ( serum3Market . marketIndex ) ;
}
2022-08-31 02:36:44 -07:00
adjustSerum3Reserved (
// todo change indices to types from numbers
2022-09-29 06:51:09 -07:00
baseBank : BankForHealth ,
quoteBank : BankForHealth ,
2022-09-23 00:34:08 -07:00
serum3Market : Serum3Market ,
2022-08-31 02:36:44 -07:00
reservedBaseChange : I80F48 ,
freeBaseChange : I80F48 ,
reservedQuoteChange : I80F48 ,
freeQuoteChange : I80F48 ,
2022-09-29 06:51:09 -07:00
) : void {
2022-09-23 00:34:08 -07:00
const baseEntryIndex = this . getOrCreateTokenInfoIndex ( baseBank ) ;
const quoteEntryIndex = this . getOrCreateTokenInfoIndex ( quoteBank ) ;
2022-08-31 02:36:44 -07:00
const baseEntry = this . tokenInfos [ baseEntryIndex ] ;
2022-09-01 00:48:23 -07:00
const reservedAmount = reservedBaseChange . mul ( baseEntry . oraclePrice ) ;
2022-08-31 02:36:44 -07:00
const quoteEntry = this . tokenInfos [ quoteEntryIndex ] ;
2022-09-01 00:48:23 -07:00
reservedAmount . iadd ( reservedQuoteChange . mul ( quoteEntry . oraclePrice ) ) ;
2022-08-31 02:36:44 -07:00
// Apply it to the tokens
2022-09-01 00:48:23 -07:00
baseEntry . serum3MaxReserved . iadd ( reservedAmount ) ;
baseEntry . balance . iadd ( freeBaseChange . mul ( baseEntry . oraclePrice ) ) ;
quoteEntry . serum3MaxReserved . iadd ( reservedAmount ) ;
quoteEntry . balance . iadd ( freeQuoteChange . mul ( quoteEntry . oraclePrice ) ) ;
2022-08-31 02:36:44 -07:00
// Apply it to the serum3 info
2022-09-29 06:51:09 -07:00
const index = this . getOrCreateSerum3InfoIndex (
baseBank ,
quoteBank ,
serum3Market ,
) ;
2022-09-23 00:34:08 -07:00
const serum3Info = this . serum3Infos [ index ] ;
2022-08-31 02:36:44 -07:00
serum3Info . reserved = serum3Info . reserved . add ( reservedAmount ) ;
}
2022-09-23 02:43:26 -07:00
findPerpInfoIndex ( perpMarketIndex : number ) : number {
return this . perpInfos . findIndex (
( perpInfo ) = > perpInfo . perpMarketIndex === perpMarketIndex ,
) ;
}
getOrCreatePerpInfoIndex ( perpMarket : PerpMarket ) : number {
const index = this . findPerpInfoIndex ( perpMarket . perpMarketIndex ) ;
if ( index == - 1 ) {
this . perpInfos . push ( PerpInfo . emptyFromPerpMarket ( perpMarket ) ) ;
}
return this . findPerpInfoIndex ( perpMarket . perpMarketIndex ) ;
}
2022-09-29 06:51:09 -07:00
public static logHealthCache ( debug : string , healthCache : HealthCache ) : void {
2022-08-31 02:36:44 -07:00
if ( debug ) console . log ( debug ) ;
2022-08-23 02:43:25 -07:00
for ( const token of healthCache . tokenInfos ) {
2022-08-31 02:55:54 -07:00
console . log ( ` ${ token . toString ( ) } ` ) ;
2022-08-31 02:36:44 -07:00
}
for ( const serum3Info of healthCache . serum3Infos ) {
2022-08-31 02:55:54 -07:00
console . log ( ` ${ serum3Info . toString ( healthCache . tokenInfos ) } ` ) ;
2022-08-23 00:42:00 -07:00
}
console . log (
2022-08-23 02:43:25 -07:00
` assets ${ healthCache . assets (
2022-08-23 00:42:00 -07:00
HealthType . init ,
2022-08-23 02:43:25 -07:00
) } , liabs $ { healthCache . liabs ( HealthType . init ) } , ` ,
) ;
console . log (
` health(HealthType.init) ${ healthCache . health ( HealthType . init ) } ` ,
) ;
console . log (
` healthRatio(HealthType.init) ${ healthCache . healthRatio (
HealthType . init ,
) } ` ,
2022-08-23 00:42:00 -07:00
) ;
}
2022-08-11 23:30:13 -07:00
simHealthRatioWithTokenPositionChanges (
group : Group ,
2022-08-22 23:34:44 -07:00
nativeTokenChanges : {
nativeTokenAmount : I80F48 ;
2022-08-17 23:48:45 -07:00
mintPk : PublicKey ;
} [ ] ,
2022-08-11 23:30:13 -07:00
healthType : HealthType = HealthType . init ,
) : I80F48 {
const adjustedCache : HealthCache = _ . cloneDeep ( this ) ;
2022-08-23 00:42:00 -07:00
// HealthCache.logHealthCache('beforeChange', adjustedCache);
2022-08-22 23:34:44 -07:00
for ( const change of nativeTokenChanges ) {
2022-08-17 23:48:45 -07:00
const bank : Bank = group . getFirstBankByMint ( change . mintPk ) ;
2022-08-12 02:05:39 -07:00
const changeIndex = adjustedCache . getOrCreateTokenInfoIndex ( bank ) ;
2022-09-01 00:48:23 -07:00
adjustedCache . tokenInfos [ changeIndex ] . balance . iadd (
change . nativeTokenAmount . mul ( bank . price ) ,
) ;
2022-08-11 23:30:13 -07:00
}
2022-08-23 00:42:00 -07:00
// HealthCache.logHealthCache('afterChange', adjustedCache);
2022-08-11 23:30:13 -07:00
return adjustedCache . healthRatio ( healthType ) ;
}
2022-08-31 02:36:44 -07:00
simHealthRatioWithSerum3BidChanges (
2022-09-29 06:51:09 -07:00
baseBank : BankForHealth ,
quoteBank : BankForHealth ,
2022-08-31 02:36:44 -07:00
bidNativeQuoteAmount : I80F48 ,
serum3Market : Serum3Market ,
healthType : HealthType = HealthType . init ,
) : I80F48 {
const adjustedCache : HealthCache = _ . cloneDeep ( this ) ;
2022-08-31 05:37:45 -07:00
const quoteIndex = adjustedCache . getOrCreateTokenInfoIndex ( quoteBank ) ;
2022-08-31 02:36:44 -07:00
const quote = adjustedCache . tokenInfos [ quoteIndex ] ;
// Move token balance to reserved funds in open orders,
// essentially simulating a place order
// Reduce token balance for quote
2022-09-01 00:48:23 -07:00
adjustedCache . tokenInfos [ quoteIndex ] . balance . isub (
bidNativeQuoteAmount . mul ( quote . oraclePrice ) ,
) ;
2022-08-31 02:36:44 -07:00
// Increase reserved in Serum3Info for quote
adjustedCache . adjustSerum3Reserved (
2022-09-29 06:51:09 -07:00
baseBank ,
quoteBank ,
2022-09-23 00:34:08 -07:00
serum3Market ,
2022-09-01 00:48:23 -07:00
ZERO_I80F48 ( ) ,
ZERO_I80F48 ( ) ,
2022-08-31 02:36:44 -07:00
bidNativeQuoteAmount ,
2022-09-01 00:48:23 -07:00
ZERO_I80F48 ( ) ,
2022-08-31 02:36:44 -07:00
) ;
return adjustedCache . healthRatio ( healthType ) ;
}
simHealthRatioWithSerum3AskChanges (
2022-09-29 06:51:09 -07:00
baseBank : BankForHealth ,
quoteBank : BankForHealth ,
2022-08-31 02:36:44 -07:00
askNativeBaseAmount : I80F48 ,
serum3Market : Serum3Market ,
healthType : HealthType = HealthType . init ,
) : I80F48 {
const adjustedCache : HealthCache = _ . cloneDeep ( this ) ;
2022-08-31 05:37:45 -07:00
const baseIndex = adjustedCache . getOrCreateTokenInfoIndex ( baseBank ) ;
2022-08-31 02:36:44 -07:00
const base = adjustedCache . tokenInfos [ baseIndex ] ;
// Move token balance to reserved funds in open orders,
// essentially simulating a place order
// Reduce token balance for base
2022-09-01 00:48:23 -07:00
adjustedCache . tokenInfos [ baseIndex ] . balance . isub (
askNativeBaseAmount . mul ( base . oraclePrice ) ,
) ;
2022-08-31 02:36:44 -07:00
// Increase reserved in Serum3Info for base
adjustedCache . adjustSerum3Reserved (
2022-09-29 06:51:09 -07:00
baseBank ,
quoteBank ,
2022-09-23 00:34:08 -07:00
serum3Market ,
2022-08-31 02:36:44 -07:00
askNativeBaseAmount ,
2022-09-01 00:48:23 -07:00
ZERO_I80F48 ( ) ,
ZERO_I80F48 ( ) ,
ZERO_I80F48 ( ) ,
2022-08-31 02:36:44 -07:00
) ;
return adjustedCache . healthRatio ( healthType ) ;
}
private static binaryApproximationSearch (
left : I80F48 ,
leftRatio : I80F48 ,
right : I80F48 ,
rightRatio : I80F48 ,
targetRatio : I80F48 ,
healthRatioAfterActionFn : ( I80F48 ) = > I80F48 ,
2022-09-29 06:51:09 -07:00
) : I80F48 {
2022-08-31 02:36:44 -07:00
const maxIterations = 40 ;
// TODO: make relative to health ratio decimals? Might be over engineering
const targetError = I80F48 . fromNumber ( 0.001 ) ;
if (
( leftRatio . sub ( targetRatio ) . isPos ( ) &&
rightRatio . sub ( targetRatio ) . isPos ( ) ) ||
( leftRatio . sub ( targetRatio ) . isNeg ( ) &&
rightRatio . sub ( targetRatio ) . isNeg ( ) )
) {
throw new Error (
2022-09-29 06:51:09 -07:00
` 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! ` ,
2022-08-31 02:36:44 -07:00
) ;
}
let newAmount ;
2022-09-29 06:51:09 -07:00
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2022-08-31 02:36:44 -07:00
for ( const key of Array ( maxIterations ) . fill ( 0 ) . keys ( ) ) {
newAmount = left . add ( right ) . mul ( I80F48 . fromNumber ( 0.5 ) ) ;
const newAmountRatio = healthRatioAfterActionFn ( newAmount ) ;
const error = newAmountRatio . sub ( targetRatio ) ;
if ( error . isPos ( ) && error . lt ( targetError ) ) {
return newAmount ;
}
if ( newAmountRatio . gt ( targetRatio ) != rightRatio . gt ( targetRatio ) ) {
left = newAmount ;
} else {
right = newAmount ;
rightRatio = newAmountRatio ;
}
}
console . error (
` Unable to get targetRatio within ${ maxIterations } iterations ` ,
) ;
return newAmount ;
}
2022-08-11 08:44:12 -07:00
getMaxSourceForTokenSwap (
2022-09-23 02:43:26 -07:00
sourceBank : BankForHealth ,
targetBank : BankForHealth ,
2022-08-11 08:44:12 -07:00
minRatio : I80F48 ,
2022-09-23 02:43:26 -07:00
priceFactor : I80F48 ,
2022-08-11 08:44:12 -07:00
) : I80F48 {
2022-08-15 11:23:51 -07:00
if (
sourceBank . initLiabWeight
. sub ( targetBank . initAssetWeight )
. abs ( )
2022-09-01 00:48:23 -07:00
. lte ( ZERO_I80F48 ( ) )
2022-08-15 11:23:51 -07:00
) {
2022-09-01 00:48:23 -07:00
return ZERO_I80F48 ( ) ;
2022-08-15 11:23:51 -07:00
}
2022-08-11 08:44:12 -07:00
// The health_ratio is a nonlinear based on swap amount.
// For large swap amounts the slope is guaranteed to be negative, but small amounts
// can have positive slope (e.g. using source deposits to pay back target borrows).
//
// That means:
// - even if the initial ratio is < minRatio it can be useful to swap to *increase* health
// - be careful about finding the minRatio point: the function isn't convex
const initialRatio = this . healthRatio ( HealthType . init ) ;
2022-09-29 06:51:09 -07:00
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2022-09-23 02:43:26 -07:00
const initialHealth = this . health ( HealthType . init ) ;
2022-09-01 00:48:23 -07:00
if ( initialRatio . lte ( ZERO_I80F48 ( ) ) ) {
return ZERO_I80F48 ( ) ;
2022-08-11 08:44:12 -07:00
}
2022-09-23 02:43:26 -07:00
// If the price is sufficiently good, then health will just increase from swapping:
// once we've swapped enough, swapping x reduces health by x * source_liab_weight and
// increases it by x * target_asset_weight * price_factor.
const finalHealthSlope = sourceBank . initLiabWeight
. neg ( )
. add ( targetBank . initAssetWeight . mul ( priceFactor ) ) ;
if ( finalHealthSlope . gte ( ZERO_I80F48 ( ) ) ) {
return MAX_I80F48 ( ) ;
}
2022-08-12 02:05:39 -07:00
const healthCacheClone : HealthCache = _ . cloneDeep ( this ) ;
const sourceIndex = healthCacheClone . getOrCreateTokenInfoIndex ( sourceBank ) ;
const targetIndex = healthCacheClone . getOrCreateTokenInfoIndex ( targetBank ) ;
const source = healthCacheClone . tokenInfos [ sourceIndex ] ;
const target = healthCacheClone . tokenInfos [ targetIndex ] ;
2022-08-11 08:44:12 -07:00
// There are two key slope changes: Assume source.balance > 0 and target.balance < 0. Then
// initially health ratio goes up. When one of balances flips sign, the health ratio slope
// may be positive or negative for a bit, until both balances have flipped and the slope is
// negative.
// The maximum will be at one of these points (ignoring serum3 effects).
2022-08-12 02:05:39 -07:00
2022-09-29 06:51:09 -07:00
function cacheAfterSwap ( amount : I80F48 ) : HealthCache {
2022-08-12 02:05:39 -07:00
const adjustedCache : HealthCache = _ . cloneDeep ( healthCacheClone ) ;
2022-08-23 02:43:25 -07:00
// HealthCache.logHealthCache('beforeSwap', adjustedCache);
2022-09-01 00:48:23 -07:00
adjustedCache . tokenInfos [ sourceIndex ] . balance . isub ( amount ) ;
2022-09-23 02:43:26 -07:00
adjustedCache . tokenInfos [ targetIndex ] . balance . iadd (
amount . mul ( priceFactor ) ,
) ;
2022-08-23 02:43:25 -07:00
// HealthCache.logHealthCache('afterSwap', adjustedCache);
2022-08-11 08:44:12 -07:00
return adjustedCache ;
}
function healthRatioAfterSwap ( amount : I80F48 ) : I80F48 {
return cacheAfterSwap ( amount ) . healthRatio ( HealthType . init ) ;
}
2022-09-23 02:43:26 -07:00
// There are two key slope changes: Assume source.balance > 0 and target.balance < 0.
// When these values flip sign, the health slope decreases, but could still be positive.
// After point1 it's definitely negative (due to finalHealthSlope check above).
// The maximum health ratio will be at 0 or at one of these points (ignoring serum3 effects).
const sourceForZeroTargetBalance = target . balance . neg ( ) . div ( priceFactor ) ;
2022-08-11 08:44:12 -07:00
const point0Amount = source . balance
2022-09-23 02:43:26 -07:00
. min ( sourceForZeroTargetBalance )
2022-09-01 00:48:23 -07:00
. max ( ZERO_I80F48 ( ) ) ;
2022-08-11 08:44:12 -07:00
const point1Amount = source . balance
2022-09-23 02:43:26 -07:00
. max ( sourceForZeroTargetBalance )
2022-09-01 00:48:23 -07:00
. max ( ZERO_I80F48 ( ) ) ;
2022-08-23 02:43:25 -07:00
const cache0 = cacheAfterSwap ( point0Amount ) ;
const point0Ratio = cache0 . healthRatio ( HealthType . init ) ;
const cache1 = cacheAfterSwap ( point1Amount ) ;
const point1Ratio = cache1 . healthRatio ( HealthType . init ) ;
const point1Health = cache1 . health ( HealthType . init ) ;
2022-08-11 08:44:12 -07:00
let amount : I80F48 ;
if (
initialRatio . lte ( minRatio ) &&
point0Ratio . lt ( minRatio ) &&
point1Ratio . lt ( minRatio )
) {
// If we have to stay below the target ratio, pick the highest one
if ( point0Ratio . gt ( initialRatio ) ) {
if ( point1Ratio . gt ( point0Ratio ) ) {
amount = point1Amount ;
} else {
amount = point0Amount ;
}
} else if ( point1Ratio . gt ( initialRatio ) ) {
amount = point1Amount ;
} else {
2022-09-01 00:48:23 -07:00
amount = ZERO_I80F48 ( ) ;
2022-08-11 08:44:12 -07:00
}
} else if ( point1Ratio . gte ( minRatio ) ) {
// If point1Ratio is still bigger than minRatio, the target amount must be >point1Amount
// search to the right of point1Amount: but how far?
// At point1, source.balance < 0 and target.balance > 0, so use a simple estimation for
2022-09-23 02:43:26 -07:00
// zero health: health - source_liab_weight * a + target_asset_weight * a * priceFactor = 0.
2022-09-01 00:48:23 -07:00
if ( point1Health . lte ( ZERO_I80F48 ( ) ) ) {
return ZERO_I80F48 ( ) ;
2022-08-11 08:44:12 -07:00
}
2022-09-23 02:43:26 -07:00
const zeroHealthAmount = point1Amount . sub (
point1Health . div ( finalHealthSlope ) ,
2022-08-11 08:44:12 -07:00
) ;
const zeroHealthRatio = healthRatioAfterSwap ( zeroHealthAmount ) ;
2022-08-31 02:36:44 -07:00
amount = HealthCache . binaryApproximationSearch (
2022-08-11 08:44:12 -07:00
point1Amount ,
point1Ratio ,
zeroHealthAmount ,
zeroHealthRatio ,
minRatio ,
2022-08-31 02:36:44 -07:00
healthRatioAfterSwap ,
2022-08-11 08:44:12 -07:00
) ;
} else if ( point0Ratio . gte ( minRatio ) ) {
// Must be between point0Amount and point1Amount.
2022-08-31 02:36:44 -07:00
amount = HealthCache . binaryApproximationSearch (
2022-08-11 08:44:12 -07:00
point0Amount ,
point0Ratio ,
point1Amount ,
point1Ratio ,
minRatio ,
2022-08-31 02:36:44 -07:00
healthRatioAfterSwap ,
2022-08-11 08:44:12 -07:00
) ;
} else {
2022-09-23 02:43:26 -07:00
// Must be between 0 and point0_amount
amount = HealthCache . binaryApproximationSearch (
ZERO_I80F48 ( ) ,
initialRatio ,
point0Amount ,
point0Ratio ,
minRatio ,
healthRatioAfterSwap ,
2022-08-11 08:44:12 -07:00
) ;
}
2022-09-23 02:43:26 -07:00
return amount . div ( source . oraclePrice ) ;
2022-08-11 08:44:12 -07:00
}
2022-08-31 02:36:44 -07:00
2022-09-23 02:43:26 -07:00
getMaxSerum3OrderForHealthRatio (
2022-09-29 06:51:09 -07:00
baseBank : BankForHealth ,
quoteBank : BankForHealth ,
2022-08-31 02:36:44 -07:00
serum3Market : Serum3Market ,
side : Serum3Side ,
minRatio : I80F48 ,
2022-09-29 06:51:09 -07:00
) : I80F48 {
2022-08-31 02:36:44 -07:00
const healthCacheClone : HealthCache = _ . cloneDeep ( this ) ;
2022-08-31 05:37:45 -07:00
const baseIndex = healthCacheClone . getOrCreateTokenInfoIndex ( baseBank ) ;
2022-09-01 00:48:23 -07:00
const quoteIndex = healthCacheClone . getOrCreateTokenInfoIndex ( quoteBank ) ;
2022-08-31 02:36:44 -07:00
const base = healthCacheClone . tokenInfos [ baseIndex ] ;
const quote = healthCacheClone . tokenInfos [ quoteIndex ] ;
// Binary search between current health (0 sized new order) and
// an amount to trade which will bring health to 0.
// Current health and amount i.e. 0
2022-09-01 00:48:23 -07:00
const initialAmount = ZERO_I80F48 ( ) ;
2022-08-31 02:36:44 -07:00
const initialHealth = this . health ( HealthType . init ) ;
const initialRatio = this . healthRatio ( HealthType . init ) ;
2022-09-01 00:48:23 -07:00
if ( initialRatio . lte ( ZERO_I80F48 ( ) ) ) {
return ZERO_I80F48 ( ) ;
2022-08-31 02:36:44 -07:00
}
// Amount which would bring health to 0
2022-09-23 00:34:08 -07:00
// where M = max(A_deposits, B_borrows)
// amount = M + (init_health + M * (B_init_liab - A_init_asset)) / (A_init_liab - B_init_asset);
2022-08-31 02:36:44 -07:00
// A is what we would be essentially swapping for B
// So when its an ask, then base->quote,
// and when its a bid, then quote->bid
let zeroAmount ;
if ( side == Serum3Side . ask ) {
2022-09-01 00:48:23 -07:00
const quoteBorrows = quote . balance . lt ( ZERO_I80F48 ( ) )
2022-08-31 02:36:44 -07:00
? quote . balance . abs ( )
2022-09-01 00:48:23 -07:00
: ZERO_I80F48 ( ) ;
2022-09-23 00:34:08 -07:00
const max = base . balance . max ( quoteBorrows ) ;
zeroAmount = max . add (
initialHealth
. add ( max . mul ( quote . initLiabWeight . sub ( base . initAssetWeight ) ) )
. div (
2022-08-31 02:36:44 -07:00
base
. liabWeight ( HealthType . init )
. sub ( quote . assetWeight ( HealthType . init ) ) ,
) ,
2022-09-23 00:34:08 -07:00
) ;
2022-08-31 02:36:44 -07:00
} else {
2022-09-01 00:48:23 -07:00
const baseBorrows = base . balance . lt ( ZERO_I80F48 ( ) )
2022-08-31 02:36:44 -07:00
? base . balance . abs ( )
2022-09-01 00:48:23 -07:00
: ZERO_I80F48 ( ) ;
2022-09-23 00:34:08 -07:00
const max = quote . balance . max ( baseBorrows ) ;
zeroAmount = max . add (
initialHealth
. add ( max . mul ( base . initLiabWeight . sub ( quote . initAssetWeight ) ) )
. div (
2022-08-31 02:36:44 -07:00
quote
. liabWeight ( HealthType . init )
. sub ( base . assetWeight ( HealthType . init ) ) ,
) ,
2022-09-23 00:34:08 -07:00
) ;
2022-08-31 02:36:44 -07:00
}
2022-09-23 00:34:08 -07:00
2022-08-31 02:36:44 -07:00
const cache = cacheAfterPlacingOrder ( zeroAmount ) ;
2022-09-29 06:51:09 -07:00
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2022-09-23 00:34:08 -07:00
const zeroAmountHealth = cache . health ( HealthType . init ) ;
2022-08-31 02:36:44 -07:00
const zeroAmountRatio = cache . healthRatio ( HealthType . init ) ;
2022-09-29 06:51:09 -07:00
function cacheAfterPlacingOrder ( amount : I80F48 ) : HealthCache {
2022-08-31 02:36:44 -07:00
const adjustedCache : HealthCache = _ . cloneDeep ( healthCacheClone ) ;
side === Serum3Side . ask
2022-09-01 00:48:23 -07:00
? adjustedCache . tokenInfos [ baseIndex ] . balance . isub ( amount )
: adjustedCache . tokenInfos [ quoteIndex ] . balance . isub ( amount ) ;
2022-08-31 02:36:44 -07:00
adjustedCache . adjustSerum3Reserved (
2022-09-29 06:51:09 -07:00
baseBank ,
quoteBank ,
2022-09-23 00:34:08 -07:00
serum3Market ,
2022-09-01 00:48:23 -07:00
side === Serum3Side . ask ? amount . div ( base . oraclePrice ) : ZERO_I80F48 ( ) ,
ZERO_I80F48 ( ) ,
side === Serum3Side . bid ? amount . div ( quote . oraclePrice ) : ZERO_I80F48 ( ) ,
ZERO_I80F48 ( ) ,
2022-08-31 02:36:44 -07:00
) ;
return adjustedCache ;
}
function healthRatioAfterPlacingOrder ( amount : I80F48 ) : I80F48 {
return cacheAfterPlacingOrder ( amount ) . healthRatio ( HealthType . init ) ;
}
const amount = HealthCache . binaryApproximationSearch (
initialAmount ,
initialRatio ,
zeroAmount ,
zeroAmountRatio ,
minRatio ,
healthRatioAfterPlacingOrder ,
) ;
2022-09-29 06:51:09 -07:00
return amount ;
2022-08-31 02:36:44 -07:00
}
2022-09-23 02:43:26 -07:00
getMaxPerpForHealthRatio (
perpMarket : PerpMarket ,
side : PerpOrderSide ,
minRatio : I80F48 ,
price : I80F48 ,
) : I80F48 {
const healthCacheClone : HealthCache = _ . cloneDeep ( this ) ;
const initialRatio = this . healthRatio ( HealthType . init ) ;
if ( initialRatio . lt ( ZERO_I80F48 ( ) ) ) {
return ZERO_I80F48 ( ) ;
}
const direction = side == PerpOrderSide . bid ? 1 : - 1 ;
const perpInfoIndex = this . getOrCreatePerpInfoIndex ( perpMarket ) ;
const perpInfo = this . perpInfos [ perpInfoIndex ] ;
const oraclePrice = perpInfo . oraclePrice ;
const baseLotSize = I80F48 . fromString ( perpMarket . baseLotSize . toString ( ) ) ;
// If the price is sufficiently good then health will just increase from trading
const finalHealthSlope =
direction == 1
? perpInfo . initAssetWeight . mul ( oraclePrice ) . sub ( price )
: price . sub ( perpInfo . initLiabWeight . mul ( oraclePrice ) ) ;
if ( finalHealthSlope . gte ( ZERO_I80F48 ( ) ) ) {
return MAX_I80F48 ( ) ;
}
function cacheAfterTrade ( baseLots : I80F48 ) : HealthCache {
const adjustedCache : HealthCache = _ . cloneDeep ( healthCacheClone ) ;
const d = I80F48 . fromNumber ( direction ) ;
adjustedCache . perpInfos [ perpInfoIndex ] . base . iadd (
d . mul ( baseLots . mul ( baseLotSize . mul ( oraclePrice ) ) ) ,
) ;
adjustedCache . perpInfos [ perpInfoIndex ] . quote . isub (
d . mul ( baseLots . mul ( baseLotSize . mul ( price ) ) ) ,
) ;
return adjustedCache ;
}
function healthAfterTrade ( baseLots : I80F48 ) : I80F48 {
return cacheAfterTrade ( baseLots ) . health ( HealthType . init ) ;
}
function healthRatioAfterTrade ( baseLots : I80F48 ) : I80F48 {
return cacheAfterTrade ( baseLots ) . healthRatio ( HealthType . init ) ;
}
const initialBaseLots = perpInfo . base
. div ( perpInfo . oraclePrice )
. div ( baseLotSize ) ;
// There are two cases:
// 1. We are increasing abs(baseLots)
// 2. We are bringing the base position to 0, and then going to case 1.
const hasCase2 =
( initialBaseLots . gt ( ZERO_I80F48 ( ) ) && direction == - 1 ) ||
( initialBaseLots . lt ( ZERO_I80F48 ( ) ) && direction == 1 ) ;
let case1Start : I80F48 , case1StartRatio : I80F48 ;
if ( hasCase2 ) {
case1Start = initialBaseLots . abs ( ) ;
case1StartRatio = healthRatioAfterTrade ( case1Start ) ;
} else {
case1Start = ZERO_I80F48 ( ) ;
case1StartRatio = initialRatio ;
}
// If we start out below minRatio and can't go above, pick the best case
let baseLots : I80F48 ;
if ( initialRatio . lte ( minRatio ) && case1StartRatio . lt ( minRatio ) ) {
if ( case1StartRatio . gte ( initialRatio ) ) {
baseLots = case1Start ;
} else {
baseLots = ZERO_I80F48 ( ) ;
}
} else if ( case1StartRatio . gte ( minRatio ) ) {
// Must reach minRatio to the right of case1Start
const case1StartHealth = healthAfterTrade ( case1Start ) ;
if ( case1StartHealth . lte ( ZERO_I80F48 ( ) ) ) {
return ZERO_I80F48 ( ) ;
}
const zeroHealthAmount = case1Start . sub (
case1StartHealth . div ( finalHealthSlope ) . div ( baseLotSize ) ,
) ;
const zeroHealthRatio = healthRatioAfterTrade ( zeroHealthAmount ) ;
baseLots = HealthCache . binaryApproximationSearch (
case1Start ,
case1StartRatio ,
zeroHealthAmount ,
zeroHealthRatio ,
minRatio ,
healthRatioAfterTrade ,
) ;
} else {
// Between 0 and case1Start
baseLots = HealthCache . binaryApproximationSearch (
ZERO_I80F48 ( ) ,
initialRatio ,
case1Start ,
case1StartRatio ,
minRatio ,
healthRatioAfterTrade ,
) ;
}
return baseLots . floor ( ) ;
}
2022-07-13 10:18:55 -07:00
}
export class TokenInfo {
2022-08-12 02:05:39 -07:00
constructor (
2022-09-29 06:51:09 -07:00
public tokenIndex : TokenIndex ,
2022-08-12 02:05:39 -07:00
public maintAssetWeight : I80F48 ,
public initAssetWeight : I80F48 ,
public maintLiabWeight : I80F48 ,
public initLiabWeight : I80F48 ,
// native/native
public oraclePrice : I80F48 ,
// in health-reference-token native units
public balance : I80F48 ,
// in health-reference-token native units
public serum3MaxReserved : I80F48 ,
) { }
static fromDto ( dto : TokenInfoDto ) : TokenInfo {
return new TokenInfo (
2022-09-29 06:51:09 -07:00
dto . tokenIndex as TokenIndex ,
2022-08-12 02:05:39 -07:00
I80F48 . from ( dto . maintAssetWeight ) ,
I80F48 . from ( dto . initAssetWeight ) ,
I80F48 . from ( dto . maintLiabWeight ) ,
I80F48 . from ( dto . initLiabWeight ) ,
I80F48 . from ( dto . oraclePrice ) ,
I80F48 . from ( dto . balance ) ,
I80F48 . from ( dto . serum3MaxReserved ) ,
) ;
2022-07-13 10:18:55 -07:00
}
2022-09-29 06:51:09 -07:00
static fromBank (
bank : BankForHealth ,
nativeBalance? : I80F48 ,
serum3MaxReserved? : I80F48 ,
) : TokenInfo {
2022-08-12 02:05:39 -07:00
return new TokenInfo (
bank . tokenIndex ,
bank . maintAssetWeight ,
bank . initAssetWeight ,
bank . maintLiabWeight ,
bank . initLiabWeight ,
bank . price ,
2022-09-29 06:51:09 -07:00
nativeBalance ? nativeBalance . mul ( bank . price ) : ZERO_I80F48 ( ) ,
serum3MaxReserved ? serum3MaxReserved : ZERO_I80F48 ( ) ,
2022-08-12 02:05:39 -07:00
) ;
}
2022-07-13 10:18:55 -07:00
assetWeight ( healthType : HealthType ) : I80F48 {
return healthType == HealthType . init
? this . initAssetWeight
: this . maintAssetWeight ;
}
liabWeight ( healthType : HealthType ) : I80F48 {
return healthType == HealthType . init
? this . initLiabWeight
: this . maintLiabWeight ;
}
healthContribution ( healthType : HealthType ) : I80F48 {
return (
this . balance . isNeg ( )
? this . liabWeight ( healthType )
: this . assetWeight ( healthType )
) . mul ( this . balance ) ;
}
2022-08-23 00:42:00 -07:00
2022-09-29 06:51:09 -07:00
toString ( ) : string {
2022-08-31 02:36:44 -07:00
return ` tokenIndex: ${ this . tokenIndex } , balance: ${
this . balance
} , serum3MaxReserved : $ {
this . serum3MaxReserved
} , initHealth $ { this . healthContribution ( HealthType . init ) } ` ;
2022-08-23 00:42:00 -07:00
}
2022-07-13 10:18:55 -07:00
}
export class Serum3Info {
2022-08-31 02:36:44 -07:00
constructor (
public reserved : I80F48 ,
public baseIndex : number ,
public quoteIndex : number ,
2022-09-29 06:51:09 -07:00
public marketIndex : MarketIndex ,
2022-08-31 02:36:44 -07:00
) { }
2022-07-13 10:18:55 -07:00
2022-09-29 06:51:09 -07:00
static fromDto ( dto : Serum3InfoDto ) : Serum3Info {
2022-08-31 02:36:44 -07:00
return new Serum3Info (
I80F48 . from ( dto . reserved ) ,
dto . baseIndex ,
dto . quoteIndex ,
2022-09-29 06:51:09 -07:00
dto . marketIndex as MarketIndex ,
2022-08-31 02:36:44 -07:00
) ;
}
2022-07-13 10:18:55 -07:00
2022-09-23 00:34:08 -07:00
static emptyFromSerum3Market (
serum3Market : Serum3Market ,
baseEntryIndex : number ,
quoteEntryIndex : number ,
2022-09-29 06:51:09 -07:00
) : Serum3Info {
2022-09-23 00:34:08 -07:00
return new Serum3Info (
ZERO_I80F48 ( ) ,
baseEntryIndex ,
quoteEntryIndex ,
serum3Market . marketIndex ,
) ;
}
2022-09-29 06:51:09 -07:00
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 ) ;
}
2022-07-13 10:18:55 -07:00
healthContribution ( healthType : HealthType , tokenInfos : TokenInfo [ ] ) : I80F48 {
2022-08-04 10:42:41 -07:00
const baseInfo = tokenInfos [ this . baseIndex ] ;
const quoteInfo = tokenInfos [ this . quoteIndex ] ;
const reserved = this . reserved ;
2022-09-29 06:51:09 -07:00
// console.log(` - reserved ${reserved}`);
// console.log(` - this.baseIndex ${this.baseIndex}`);
// console.log(` - this.quoteIndex ${this.quoteIndex}`);
2022-07-13 10:18:55 -07:00
if ( reserved . isZero ( ) ) {
2022-09-01 00:48:23 -07:00
return ZERO_I80F48 ( ) ;
2022-07-13 10:18:55 -07:00
}
// How much the health would increase if the reserved balance were applied to the passed
// token info?
2022-09-29 06:51:09 -07:00
const computeHealthEffect = function ( tokenInfo : TokenInfo ) : I80F48 {
2022-07-13 10:18:55 -07:00
// 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`.
2022-08-04 10:42:41 -07:00
const maxBalance = tokenInfo . balance . add ( tokenInfo . serum3MaxReserved ) ;
2022-07-13 10:18:55 -07:00
// Assuming `reserved` was added to `max_balance` last (because that gives the smallest
// health effects): how much did health change because of it?
let assetPart , liabPart ;
if ( maxBalance . gte ( reserved ) ) {
assetPart = reserved ;
2022-09-01 00:48:23 -07:00
liabPart = ZERO_I80F48 ( ) ;
2022-07-13 10:18:55 -07:00
} else if ( maxBalance . isNeg ( ) ) {
2022-09-01 00:48:23 -07:00
assetPart = ZERO_I80F48 ( ) ;
2022-07-13 10:18:55 -07:00
liabPart = reserved ;
} else {
assetPart = maxBalance ;
liabPart = reserved . sub ( maxBalance ) ;
}
2022-08-04 10:42:41 -07:00
const assetWeight = tokenInfo . assetWeight ( healthType ) ;
const liabWeight = tokenInfo . liabWeight ( healthType ) ;
2022-09-29 06:51:09 -07:00
// 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}`);
2022-07-13 10:18:55 -07:00
return assetWeight . mul ( assetPart ) . add ( liabWeight . mul ( liabPart ) ) ;
} ;
2022-08-04 10:42:41 -07:00
const reservedAsBase = computeHealthEffect ( baseInfo ) ;
const reservedAsQuote = computeHealthEffect ( quoteInfo ) ;
2022-09-29 06:51:09 -07:00
// console.log(` - reservedAsBase ${reservedAsBase}`);
// console.log(` - reservedAsQuote ${reservedAsQuote}`);
2022-07-13 10:18:55 -07:00
return reservedAsBase . min ( reservedAsQuote ) ;
}
2022-08-31 02:36:44 -07:00
2022-09-29 06:51:09 -07:00
toString ( tokenInfos : TokenInfo [ ] ) : string {
2022-08-31 02:36:44 -07:00
return ` marketIndex: ${ this . marketIndex } , baseIndex: ${
this . baseIndex
} , quoteIndex : $ { this . quoteIndex } , reserved : $ {
this . reserved
} , initHealth $ { this . healthContribution ( HealthType . init , tokenInfos ) } ` ;
}
2022-07-13 10:18:55 -07:00
}
export class PerpInfo {
2022-09-23 02:43:26 -07:00
constructor (
public perpMarketIndex : number ,
public maintAssetWeight : I80F48 ,
public initAssetWeight : I80F48 ,
public maintLiabWeight : I80F48 ,
public initLiabWeight : I80F48 ,
public base : I80F48 ,
public quote : I80F48 ,
public oraclePrice : I80F48 ,
public hasOpenOrders : boolean ,
2022-09-29 06:51:09 -07:00
public trustedMarket : boolean ,
2022-09-23 02:43:26 -07:00
) { }
2022-09-29 06:51:09 -07:00
static fromDto ( dto : PerpInfoDto ) : PerpInfo {
2022-09-23 02:43:26 -07:00
return new PerpInfo (
dto . perpMarketIndex ,
I80F48 . from ( dto . maintAssetWeight ) ,
I80F48 . from ( dto . initAssetWeight ) ,
I80F48 . from ( dto . maintLiabWeight ) ,
I80F48 . from ( dto . initLiabWeight ) ,
I80F48 . from ( dto . base ) ,
I80F48 . from ( dto . quote ) ,
I80F48 . from ( dto . oraclePrice ) ,
dto . hasOpenOrders ,
2022-09-29 06:51:09 -07:00
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 ,
2022-09-23 02:43:26 -07:00
) ;
}
2022-07-13 10:18:55 -07:00
healthContribution ( healthType : HealthType ) : I80F48 {
let weight ;
if ( healthType == HealthType . init && this . base . isNeg ( ) ) {
weight = this . initLiabWeight ;
} else if ( healthType == HealthType . init && ! this . base . isNeg ( ) ) {
weight = this . initAssetWeight ;
}
if ( healthType == HealthType . maint && this . base . isNeg ( ) ) {
weight = this . maintLiabWeight ;
}
if ( healthType == HealthType . maint && ! this . base . isNeg ( ) ) {
weight = this . maintAssetWeight ;
}
2022-09-29 06:51:09 -07:00
// console.log(`initLiabWeight ${this.initLiabWeight}`);
// console.log(`initAssetWeight ${this.initAssetWeight}`);
// console.log(`weight ${weight}`);
// console.log(`this.quote ${this.quote}`);
// console.log(`this.base ${this.base}`);
const uncappedHealthContribution = this . quote . add ( weight . mul ( this . base ) ) ;
if ( this . trustedMarket ) {
return uncappedHealthContribution ;
} else {
return uncappedHealthContribution . min ( ZERO_I80F48 ( ) ) ;
}
2022-07-13 10:18:55 -07:00
}
2022-09-23 02:43:26 -07:00
static emptyFromPerpMarket ( perpMarket : PerpMarket ) : PerpInfo {
return new PerpInfo (
perpMarket . perpMarketIndex ,
perpMarket . maintAssetWeight ,
perpMarket . initAssetWeight ,
perpMarket . maintLiabWeight ,
perpMarket . initLiabWeight ,
ZERO_I80F48 ( ) ,
ZERO_I80F48 ( ) ,
2022-09-29 06:51:09 -07:00
perpMarket . price ,
2022-09-23 02:43:26 -07:00
false ,
2022-09-29 06:51:09 -07:00
perpMarket . trustedMarket ,
2022-09-23 02:43:26 -07:00
) ;
}
2022-09-29 06:51:09 -07:00
toString ( ) : string {
return ` perpMarketIndex: ${ this . perpMarketIndex } , base: ${
this . base
} , quote : $ { this . quote } , oraclePrice : $ {
this . oraclePrice
} , initHealth $ { this . healthContribution ( HealthType . init ) } ` ;
}
2022-07-13 10:18:55 -07:00
}
export class HealthCacheDto {
tokenInfos : TokenInfoDto [ ] ;
serum3Infos : Serum3InfoDto [ ] ;
perpInfos : PerpInfoDto [ ] ;
}
export class TokenInfoDto {
tokenIndex : number ;
maintAssetWeight : I80F48Dto ;
initAssetWeight : I80F48Dto ;
maintLiabWeight : I80F48Dto ;
initLiabWeight : I80F48Dto ;
oraclePrice : I80F48Dto ; // native/native
// in health-reference-token native units
balance : I80F48Dto ;
// in health-reference-token native units
serum3MaxReserved : I80F48Dto ;
2022-08-31 02:36:44 -07:00
constructor (
tokenIndex : number ,
maintAssetWeight : I80F48Dto ,
initAssetWeight : I80F48Dto ,
maintLiabWeight : I80F48Dto ,
initLiabWeight : I80F48Dto ,
oraclePrice : I80F48Dto ,
balance : I80F48Dto ,
serum3MaxReserved : I80F48Dto ,
) {
this . tokenIndex = tokenIndex ;
this . maintAssetWeight = maintAssetWeight ;
this . initAssetWeight = initAssetWeight ;
this . maintLiabWeight = maintLiabWeight ;
this . initLiabWeight = initLiabWeight ;
this . oraclePrice = oraclePrice ;
this . balance = balance ;
this . serum3MaxReserved = serum3MaxReserved ;
}
2022-07-13 10:18:55 -07:00
}
export class Serum3InfoDto {
reserved : I80F48Dto ;
baseIndex : number ;
quoteIndex : number ;
2022-08-31 02:36:44 -07:00
marketIndex : number ;
constructor ( reserved : I80F48Dto , baseIndex : number , quoteIndex : number ) {
this . reserved = reserved ;
this . baseIndex = baseIndex ;
this . quoteIndex = quoteIndex ;
}
2022-07-13 10:18:55 -07:00
}
export class PerpInfoDto {
2022-09-23 02:43:26 -07:00
perpMarketIndex : number ;
2022-07-13 10:18:55 -07:00
maintAssetWeight : I80F48Dto ;
initAssetWeight : I80F48Dto ;
maintLiabWeight : I80F48Dto ;
initLiabWeight : I80F48Dto ;
base : I80F48Dto ;
quote : I80F48Dto ;
2022-09-23 02:43:26 -07:00
oraclePrice : I80F48Dto ;
hasOpenOrders : boolean ;
2022-09-29 06:51:09 -07:00
trustedMarket : boolean ;
2022-07-13 10:18:55 -07:00
}