import { PublicKey } from '@solana/web3.js' ; import BN from 'bn.js' ; import { EventEmitter } from 'events' ; import StrictEventEmitter from 'strict-event-emitter-types' ; import { ClearingHouse } from './clearingHouse' ; import { UserAccount , UserPosition , UserPositionsAccount } from './types' ; import { calculateEntryPrice } from './math/position' ; import { MARK_PRICE_PRECISION , AMM_TO_QUOTE_PRECISION_RATIO , ZERO , TEN_THOUSAND , BN_MAX , PARTIAL_LIQUIDATION_RATIO , FULL_LIQUIDATION_RATIO , QUOTE_PRECISION , AMM_RESERVE_PRECISION , PRICE_TO_QUOTE_PRECISION , } from './constants/numericConstants' ; import { UserAccountSubscriber , UserAccountEvents } from './accounts/types' ; import { DefaultUserAccountSubscriber } from './accounts/defaultUserAccountSubscriber' ; import { calculateMarkPrice , calculateBaseAssetValue , calculatePositionFundingPNL , calculatePositionPNL , PositionDirection , calculateTradeSlippage , } from '.' ; import { getUserAccountPublicKey } from './addresses' ; export class ClearingHouseUser { clearingHouse : ClearingHouse ; authority : PublicKey ; accountSubscriber : UserAccountSubscriber ; userAccountPublicKey ? : PublicKey ; isSubscribed = false ; eventEmitter : StrictEventEmitter < EventEmitter , UserAccountEvents > ; public static from ( clearingHouse , authority ) { const accountSubscriber = new DefaultUserAccountSubscriber ( clearingHouse . program , authority ) ; return new ClearingHouseUser ( clearingHouse , authority , accountSubscriber ) ; } public constructor ( clearingHouse , authority , accountSubscriber ) { this . clearingHouse = clearingHouse ; this . authority = authority ; this . accountSubscriber = accountSubscriber ; this . eventEmitter = this . accountSubscriber . eventEmitter ; } returns public async subscribe ( ) { await this . clearingHouse . subscribe ( ) ; this . isSubscribed = await this . accountSubscriber . subscribe ( ) ; return this . isSubscribed ; } /**
     *	Forces the accountSubscriber to fetch account updates from rpc
     */ public async fetchAccounts ( ) { await this . accountSubscriber . fetch ( ) ; } public async unsubscribe ( ) { await this . accountSubscriber . unsubscribe ( ) ; this . isSubscribed = false ; } public getUserAccount ( ) { return this . accountSubscriber . getUserAccount ( ) ; } public getUserPositionsAccount ( ) { return this . accountSubscriber . getUserPositionsAccount ( ) ; } param marketIndex returns public getUserPosition ( marketIndex ) { return this . getUserPositionsAccount ( ) . positions . find ( ( position ) => position . marketIndex . eq ( marketIndex ) ) ; } public getEmptyPosition ( marketIndex ) { return { baseAssetAmount : ZERO , lastCumulativeFundingRate : ZERO , marketIndex , quoteAssetAmount : ZERO , } ; } public async getUserAccountPublicKey ( ) { if ( this . userAccountPublicKey ) { return this . userAccountPublicKey ; } this . userAccountPublicKey = await getUserAccountPublicKey ( this . clearingHouse . program . programId , this . authority ) ; return this . userAccountPublicKey ; } public async exists ( ) { const userAccountPublicKey = await this . getUserAccountPublicKey ( ) ; const userAccountRPCResponse = await this . clearingHouse . connection . getParsedAccountInfo ( userAccountPublicKey ) ; return userAccountRPCResponse . value !== null ; } returns public getBuyingPower ( ) { return this . getFreeCollateral ( ) . mul ( this . getMaxLeverage ( 'Initial' ) ) . div ( TEN_THOUSAND ) ; } returns public getFreeCollateral ( ) { return this . getTotalCollateral ( ) . sub ( this . getTotalPositionValue ( ) . mul ( TEN_THOUSAND ) . div ( this . getMaxLeverage ( 'Initial' ) ) ) ; } returns public getUnrealizedPNL ( withFunding ? , marketIndex ? ) { return this . getUserPositionsAccount ( ) . positions . filter ( ( pos ) => marketIndex ? pos . marketIndex === marketIndex : true ) . reduce ( ( pnl , marketPosition ) => { const market = this . clearingHouse . getMarket ( marketPosition . marketIndex ) ; return pnl . add ( calculatePositionPNL ( market , marketPosition , withFunding ) ) ; } , ZERO ) ; } returns public getUnrealizedFundingPNL ( marketIndex ? ) { return this . getUserPositionsAccount ( ) . positions . filter ( ( pos ) => marketIndex ? pos . marketIndex === marketIndex : true ) . reduce ( ( pnl , marketPosition ) => { const market = this . clearingHouse . getMarket ( marketPosition . marketIndex ) ; return pnl . add ( calculatePositionFundingPNL ( market , marketPosition ) ) ; } , ZERO ) ; } returns public getTotalCollateral ( ) { return ( this . getUserAccount ( ) . collateral . add ( this . getUnrealizedPNL ( true ) ) ?? new BN ( 0 ) ) ; } returns getTotalPositionValue ( ) { return this . getUserPositionsAccount ( ) . positions . reduce ( ( positionValue , marketPosition ) => { const market = this . clearingHouse . getMarket ( marketPosition . marketIndex ) ; return positionValue . add ( calculateBaseAssetValue ( market , marketPosition ) ) ; } , ZERO ) ; } returns public getPositionValue ( marketIndex ) { const userPosition = this . getUserPosition ( marketIndex ) || this . getEmptyPosition ( marketIndex ) ; const market = this . clearingHouse . getMarket ( userPosition . marketIndex ) ; return calculateBaseAssetValue ( market , userPosition ) ; } public getPositionSide ( currentPosition ) { if ( currentPosition . baseAssetAmount . gt ( ZERO ) ) { return PositionDirection . LONG ; } else if ( currentPosition . baseAssetAmount . lt ( ZERO ) ) { return PositionDirection . SHORT ; } else { return undefined ; } } returns public getPositionEstimatedExitPriceAndPnl ( position , amountToClose ? ) { const market = this . clearingHouse . getMarket ( position . marketIndex ) ; const entryPrice = calculateEntryPrice ( position ) ; if ( amountToClose ) { if ( amountToClose . eq ( ZERO ) ) { return [ calculateMarkPrice ( market ) , ZERO ] ; } position = { baseAssetAmount : amountToClose , lastCumulativeFundingRate : position . lastCumulativeFundingRate , marketIndex : position . marketIndex , quoteAssetAmount : position . quoteAssetAmount , } as UserPosition ; } const baseAssetValue = calculateBaseAssetValue ( market , position ) ; if ( position . baseAssetAmount . eq ( ZERO ) ) { return [ ZERO , ZERO ] ; } const exitPrice = baseAssetValue . mul ( AMM_TO_QUOTE_PRECISION_RATIO ) . mul ( MARK_PRICE_PRECISION ) . div ( position . baseAssetAmount . abs ( ) ) ; const pnlPerBase = exitPrice . sub ( entryPrice ) ; const pnl = pnlPerBase . mul ( position . baseAssetAmount ) . div ( MARK_PRICE_PRECISION ) . div ( AMM_TO_QUOTE_PRECISION_RATIO ) ; return [ exitPrice , pnl ] ; } returns public getLeverage ( ) { const totalCollateral = this . getTotalCollateral ( ) ; const totalPositionValue = this . getTotalPositionValue ( ) ; if ( totalPositionValue . eq ( ZERO ) && totalCollateral . eq ( ZERO ) ) { return ZERO ; } return totalPositionValue . mul ( TEN_THOUSAND ) . div ( totalCollateral ) ; } params returns public getMaxLeverage ( category ? ) { const chState = this . clearingHouse . getStateAccount ( ) ; let marginRatioCategory ; switch ( category ) { case 'Initial' : marginRatioCategory = chState . marginRatioInitial ; break ; case 'Maintenance' : marginRatioCategory = chState . marginRatioMaintenance ; break ; case 'Partial' : marginRatioCategory = chState . marginRatioPartial ; break ; default : marginRatioCategory = chState . marginRatioInitial ; break ; } const maxLeverage = TEN_THOUSAND . mul ( TEN_THOUSAND ) . div ( marginRatioCategory ) ; return maxLeverage ; } returns public getMarginRatio ( ) { const totalPositionValue = this . getTotalPositionValue ( ) ; if ( totalPositionValue . eq ( ZERO ) ) { return BN_MAX ; } return this . getTotalCollateral ( ) . mul ( TEN_THOUSAND ) . div ( totalPositionValue ) ; } public canBeLiquidated ( ) { const marginRatio = this . getMarginRatio ( ) ; const canLiquidate = marginRatio . lte ( PARTIAL_LIQUIDATION_RATIO ) ; return [ canLiquidate , marginRatio ] ; } returns public needsToSettleFundingPayment ( ) { const marketsAccount = this . clearingHouse . getMarketsAccount ( ) ; for ( const userPosition of this . getUserPositionsAccount ( ) . positions ) { if ( userPosition . baseAssetAmount . eq ( ZERO ) ) { continue ; } const market = marketsAccount . markets [ userPosition . marketIndex . toNumber ( ) ] ; if ( market . amm . cumulativeFundingRateLong . eq ( userPosition . lastCumulativeFundingRate ) || market . amm . cumulativeFundingRateShort . eq ( userPosition . lastCumulativeFundingRate ) ) { continue ; } return true ; } return false ; } param targetMarket param positionBaseSizeChange param partial returns public liquidationPriceOld ( targetMarket , positionBaseSizeChange = ZERO , partial = false ) { const currentPrice = calculateMarkPrice ( this . clearingHouse . getMarket ( targetMarket . marketIndex ) ) ; const totalCollateralUSDC = this . getTotalCollateral ( ) ; const totalCurrentPositionValueIgnoringTargetUSDC = this . getTotalPositionValueExcludingMarket ( targetMarket . marketIndex ) ; const currentMarketPosition = this . getUserPosition ( targetMarket . marketIndex ) || this . getEmptyPosition ( targetMarket . marketIndex ) ; const currentMarketPositionBaseSize = currentMarketPosition . baseAssetAmount ; const proposedMarketPosition = { marketIndex : targetMarket . marketIndex , baseAssetAmount : currentMarketPositionBaseSize . add ( positionBaseSizeChange ) , lastCumulativeFundingRate : new BN ( 0 ) , quoteAssetAmount : new BN ( 0 ) , } ; const market = this . clearingHouse . getMarket ( proposedMarketPosition . marketIndex ) ; const proposedMarketPositionValueUSDC = calculateBaseAssetValue ( market , proposedMarketPosition ) ; const targetTotalPositionValueUSDC = totalCurrentPositionValueIgnoringTargetUSDC . add ( proposedMarketPositionValueUSDC ) ; let totalFreeCollateralUSDC = this . getTotalCollateral ( ) . sub ( this . getTotalPositionValue ( ) . mul ( TEN_THOUSAND ) . div ( this . getMaxLeverage ( 'Maintenance' ) ) ) ; if ( partial ) { totalFreeCollateralUSDC = this . getTotalCollateral ( ) . sub ( this . getTotalPositionValue ( ) . mul ( TEN_THOUSAND ) . div ( this . getMaxLeverage ( 'Partial' ) ) ) ; } if ( targetTotalPositionValueUSDC . lte ( totalFreeCollateralUSDC ) && proposedMarketPosition . baseAssetAmount . gt ( ZERO ) ) { return new BN ( - 1 ) ; } let marginRatio ; if ( proposedMarketPositionValueUSDC . eq ( ZERO ) ) { marginRatio = BN_MAX ; } else { marginRatio = totalCollateralUSDC . mul ( TEN_THOUSAND ) . div ( proposedMarketPositionValueUSDC ) ; } let liqRatio = FULL_LIQUIDATION_RATIO ; if ( partial ) { liqRatio = PARTIAL_LIQUIDATION_RATIO ; } const baseAssetSignIsNeg = proposedMarketPosition . baseAssetAmount . isNeg ( ) ; let pctChange = marginRatio . abs ( ) . sub ( liqRatio ) ; if ( baseAssetSignIsNeg ) { pctChange = pctChange . add ( TEN_THOUSAND ) ; } else { if ( TEN_THOUSAND . lte ( pctChange ) ) { return new BN ( - 1 ) ; } pctChange = TEN_THOUSAND . sub ( pctChange ) ; } const liqPrice = currentPrice . mul ( pctChange ) . div ( TEN_THOUSAND ) ; return liqPrice ; } param targetMarket param positionBaseSizeChange param partial returns public liquidationPrice ( targetMarket , positionBaseSizeChange = ZERO , partial = false ) { const tc = this . getTotalCollateral ( ) ; const tpv = this . getTotalPositionValue ( ) ; const partialLev = 16 ; const maintLev = 20 ; const thisLev = partial ? new BN ( partialLev ) : new BN ( maintLev ) ; const totalCurrentPositionValueIgnoringTargetUSDC = this . getTotalPositionValueExcludingMarket ( targetMarket . marketIndex ) ; const currentMarketPosition = this . getUserPosition ( targetMarket . marketIndex ) || this . getEmptyPosition ( targetMarket . marketIndex ) ; const currentMarketPositionBaseSize = currentMarketPosition . baseAssetAmount ; const proposedBaseAssetAmount = currentMarketPositionBaseSize . add ( positionBaseSizeChange ) ; const proposedMarketPosition = { marketIndex : targetMarket . marketIndex , baseAssetAmount : proposedBaseAssetAmount , lastCumulativeFundingRate : currentMarketPosition . lastCumulativeFundingRate , quoteAssetAmount : new BN ( 0 ) , } ; const market = this . clearingHouse . getMarket ( proposedMarketPosition . marketIndex ) ; const proposedMarketPositionValueUSDC = calculateBaseAssetValue ( market , proposedMarketPosition ) ; const targetTotalPositionValueUSDC = totalCurrentPositionValueIgnoringTargetUSDC . add ( proposedMarketPositionValueUSDC ) ; let totalFreeCollateralUSDC = tc . sub ( totalCurrentPositionValueIgnoringTargetUSDC . mul ( TEN_THOUSAND ) . div ( this . getMaxLeverage ( 'Maintenance' ) ) ) ; if ( partial ) { totalFreeCollateralUSDC = tc . sub ( totalCurrentPositionValueIgnoringTargetUSDC . mul ( TEN_THOUSAND ) . div ( this . getMaxLeverage ( 'Partial' ) ) ) ; } let priceDelt ; if ( proposedBaseAssetAmount . lt ( ZERO ) ) { priceDelt = ( tc . mul ( thisLev ) . sub ( tpv ) ) . mul ( PRICE_TO_QUOTE_PRECISION ) . div ( thisLev . add ( new BN ( 1 ) ) ) ; } else { priceDelt = ( tc . mul ( thisLev ) . sub ( tpv ) ) . mul ( PRICE_TO_QUOTE_PRECISION ) . div ( thisLev . sub ( new BN ( 1 ) ) ) ; } let currentPrice ; if ( positionBaseSizeChange . eq ( ZERO ) ) { currentPrice = calculateMarkPrice ( this . clearingHouse . getMarket ( targetMarket . marketIndex ) ) ; } else { const direction = positionBaseSizeChange . gt ( ZERO ) ? PositionDirection . LONG : PositionDirection . SHORT ; currentPrice = calculateTradeSlippage ( direction , positionBaseSizeChange . abs ( ) , this . clearingHouse . getMarket ( targetMarket . marketIndex ) , 'base' ) [ 3 ] ; } if ( targetTotalPositionValueUSDC . lte ( totalFreeCollateralUSDC ) && proposedMarketPosition . baseAssetAmount . gt ( ZERO ) ) { return new BN ( - 1 ) ; } if ( proposedBaseAssetAmount . eq ( ZERO ) ) return new BN ( - 1 ) ; const eatMargin2 = priceDelt . mul ( AMM_RESERVE_PRECISION ) . div ( proposedBaseAssetAmount ) ; if ( eatMargin2 . gt ( currentPrice ) ) { return new BN ( - 1 ) ; } const liqPrice = currentPrice . sub ( eatMargin2 ) ; return liqPrice ; } param positionMarketIndex param closeQuoteAmount returns public liquidationPriceAfterClose ( positionMarketIndex , closeQuoteAmount ) { const currentPosition = this . getUserPosition ( positionMarketIndex ) || this . getEmptyPosition ( positionMarketIndex ) ; const closeBaseAmount = currentPosition . baseAssetAmount . mul ( closeQuoteAmount ) . div ( currentPosition . quoteAssetAmount ) . add ( currentPosition . baseAssetAmount . mul ( closeQuoteAmount ) . mod ( currentPosition . quoteAssetAmount ) ) . neg ( ) ; return this . liquidationPrice ( { marketIndex : positionMarketIndex , } , closeBaseAmount ) ; } param marketIndex param tradeSide param userMaxLeverageSetting returns public getMaxTradeSizeUSDC ( targetMarketIndex , tradeSide , userMaxLeverageSetting ) { const currentPosition = this . getUserPosition ( targetMarketIndex ) || this . getEmptyPosition ( targetMarketIndex ) ; const targetSide = tradeSide === PositionDirection . SHORT ? 'short' : 'long' ; const currentPositionSide = currentPosition ?. baseAssetAmount . isNeg ( ) ? 'short' : 'long' ; const targettingSameSide = ! currentPosition ? true : targetSide === currentPositionSide ; const oppositeSizeValueUSDC = targettingSameSide ? ZERO : this . getPositionValue ( targetMarketIndex ) ; const currentLeverage = this . getLeverage ( ) ; const remainingLeverage = BN . max ( userMaxLeverageSetting . sub ( currentLeverage ) , ZERO ) ; const totalCollateral = this . getTotalCollateral ( ) ; let maxPositionSize = remainingLeverage . mul ( totalCollateral ) . div ( TEN_THOUSAND ) ; if ( userMaxLeverageSetting . sub ( currentLeverage ) . gte ( ZERO ) ) { if ( oppositeSizeValueUSDC . eq ( ZERO ) ) { } else { maxPositionSize = maxPositionSize . add ( oppositeSizeValueUSDC . mul ( new BN ( 2 ) ) ) ; } } else { if ( ! targettingSameSide ) { const currentPositionQuoteSize = this . getPositionValue ( targetMarketIndex ) ; const currentTotalQuoteSize = currentLeverage . mul ( totalCollateral ) . div ( TEN_THOUSAND ) ; const otherPositionsTotalQuoteSize = currentTotalQuoteSize . sub ( currentPositionQuoteSize ) ; const quoteValueOfMaxLeverage = userMaxLeverageSetting . mul ( totalCollateral ) . div ( TEN_THOUSAND ) ; if ( otherPositionsTotalQuoteSize . sub ( currentPositionQuoteSize ) . gte ( quoteValueOfMaxLeverage ) ) { maxPositionSize = currentPositionQuoteSize ; } else { const allowedQuoteSizeAfterClosingCurrentPosition = quoteValueOfMaxLeverage . sub ( otherPositionsTotalQuoteSize ) ; maxPositionSize = currentPositionQuoteSize . add ( allowedQuoteSizeAfterClosingCurrentPosition ) ; } } else { } } const oneMilli = maxPositionSize . div ( QUOTE_PRECISION ) ; return maxPositionSize . sub ( oneMilli ) ; } param targetMarketIndex param positionMarketIndex param tradeQuoteAmount returns public accountLeverageRatioAfterTrade ( targetMarketIndex , tradeQuoteAmount , tradeSide ) { const currentPosition = this . getUserPosition ( targetMarketIndex ) || this . getEmptyPosition ( targetMarketIndex ) ; let currentPositionQuoteAmount = this . getPositionValue ( targetMarketIndex ) ; const currentSide = currentPosition && currentPosition . baseAssetAmount . isNeg ( ) ? PositionDirection . SHORT : PositionDirection . LONG ; if ( currentSide === PositionDirection . SHORT ) currentPositionQuoteAmount = currentPositionQuoteAmount . neg ( ) ; if ( tradeSide === PositionDirection . SHORT ) tradeQuoteAmount = tradeQuoteAmount . neg ( ) ; const currentMarketPositionAfterTrade = currentPositionQuoteAmount . add ( tradeQuoteAmount ) . abs ( ) ; const totalPositionAfterTradeExcludingTargetMarket = this . getTotalPositionValueExcludingMarket ( targetMarketIndex ) ; const totalCollateral = this . getTotalCollateral ( ) ; if ( totalCollateral . gt ( ZERO ) ) { const newLeverage = currentMarketPositionAfterTrade . add ( totalPositionAfterTradeExcludingTargetMarket ) . abs ( ) . mul ( TEN_THOUSAND ) . div ( totalCollateral ) ; return newLeverage ; } else { return new BN ( 0 ) ; } } param quoteAmount returns public calculateFeeForQuoteAmount ( quoteAmount ) { const feeStructure = this . clearingHouse . getStateAccount ( ) . feeStructure ; return quoteAmount . mul ( feeStructure . feeNumerator ) . div ( feeStructure . feeDenominator ) ; } param marketToIgnore returns private getTotalPositionValueExcludingMarket ( marketToIgnore ) { const currentMarketPosition = this . getUserPosition ( marketToIgnore ) || this . getEmptyPosition ( marketToIgnore ) ; let currentMarketPositionValueUSDC = ZERO ; if ( currentMarketPosition ) { currentMarketPositionValueUSDC = this . getPositionValue ( marketToIgnore ) ; } return this . getTotalPositionValue ( ) . sub ( currentMarketPositionValueUSDC ) ; } }