From 9b916a34cea644b8f77304132d42081597439c36 Mon Sep 17 00:00:00 2001 From: filipzeta <103913117+filipzeta@users.noreply.github.com> Date: Wed, 23 Aug 2023 10:44:51 +1000 Subject: [PATCH] Post-trade calcs (#257) --- CHANGELOG.md | 4 + package.json | 2 +- src/risk-utils.ts | 212 +++++++++++++++++++++++++++++- src/risk.ts | 319 +++++++++++++++++++++------------------------- src/types.ts | 9 ++ src/utils.ts | 7 + 6 files changed, 379 insertions(+), 174 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7db821429..bd3fe2165 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ Version changes are pinned to SDK releases. ## Unreleased +## [1.7.0] 2023-08-21 + +- Add post-trade calculations to risk.ts, allowing for accurate simulations of account metrics from a hypothetical order/trade. ([#257](https://github.com/zetamarkets/sdk/pull/257)) + ## [1.6.1] 2023-08-09 - getMaxTradeSize() now simulates all positions and orders correctly. ([#256](https://github.com/zetamarkets/sdk/pull/256)) diff --git a/package.json b/package.json index b43a362b3..4dde8dbc4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@zetamarkets/sdk", "repository": "https://github.com/zetamarkets/sdk/", - "version": "1.6.1", + "version": "1.7.0", "description": "Zeta SDK", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/risk-utils.ts b/src/risk-utils.ts index 9b60e0efa..f2083bb98 100644 --- a/src/risk-utils.ts +++ b/src/risk-utils.ts @@ -1,7 +1,14 @@ import { types, Exchange, constants, assets } from "."; import { Asset } from "./constants"; -import { MarginAccount } from "./program-types"; -import { convertNativeBNToDecimal } from "./utils"; +import { CrossMarginAccount, MarginAccount } from "./program-types"; +import { + convertDecimalToNativeInteger, + convertDecimalToNativeLotSize, + convertNativeBNToDecimal, + convertNativeLotSizeToDecimal, +} from "./utils"; +import cloneDeep from "lodash.clonedeep"; +import * as anchor from "@zetamarkets/anchor"; /** * Assemble a collected risk state Map, describing important values on a per-asset basis. @@ -135,3 +142,204 @@ export function checkMarginAccountMarginRequirement( let buffer = marginAccount.balance.toNumber() + pnl - totalMaintenanceMargin; return buffer > 0; } + +/** + * Simulate adding an extra position/order into an existing CrossMarginAccount. + * This will change the account! Therefore do a deep clone first if you want a new account to simulate. + * @param marginAccount The CrossMarginAccount itself + * @param isTaker Whether or not the order crosses the orderbook in full and becomes a position + * @param asset The market on which we're trading + * @param side Bid or ask + * @param price The trade price, in decimal USDC + * @param size The trade size, in decimal USDC + */ +export function addFakeTradeToAccount( + marginAccount: CrossMarginAccount, + isTaker: boolean, + asset: constants.Asset, + side: types.Side, + price: number, + size: number +) { + let assetIndex = assets.assetToIndex(asset); + let editedPosition = marginAccount.productLedgers[assetIndex].position; + let editedOrderState = marginAccount.productLedgers[assetIndex].orderState; + let markPrice = Exchange.getMarkPrice(asset); + + let fee = isTaker + ? (convertNativeBNToDecimal(Exchange.state.nativeD1TradeFeePercentage) / + 100) * + price + : 0; + + let sizeNative = convertDecimalToNativeLotSize(size); + let currentSizeBN = editedPosition.size; + let currentSize = currentSizeBN.toNumber(); + // Fake the new position, moving both editedPosition and editedOrderState + if (isTaker) { + editedPosition.size = editedPosition.size.add( + new anchor.BN(side == types.Side.BID ? sizeNative : -sizeNative) + ); + marginAccount.balance = marginAccount.balance.sub( + new anchor.BN(convertDecimalToNativeInteger(fee * size, 1)) + ); + + // If we're just adding to costOfTrades + if ( + (side == types.Side.BID && currentSize > 0) || + (side == types.Side.ASK && currentSize < 0) + ) { + editedPosition.costOfTrades = editedPosition.costOfTrades.add( + new anchor.BN(size * convertDecimalToNativeInteger(price, 1)) + ); + + let openIndex = side == types.Side.BID ? 1 : 0; + let diff = anchor.BN.min( + editedOrderState.openingOrders[openIndex], + new anchor.BN(sizeNative) + ); + editedOrderState.closingOrders = editedOrderState.closingOrders.add(diff); + editedOrderState.openingOrders[openIndex] = + editedOrderState.openingOrders[openIndex].sub(diff); + } + // If we're just reducing the current position + else if (sizeNative < Math.abs(currentSize)) { + let entryPrice = new anchor.BN( + editedPosition.costOfTrades.toNumber() / + convertNativeLotSizeToDecimal(Math.abs(currentSize)) + ); + let priceDiff = entryPrice.sub( + new anchor.BN(convertDecimalToNativeInteger(price, 1)) + ); + marginAccount.balance = marginAccount.balance.add( + new anchor.BN(side == types.Side.BID ? size : -size).mul(priceDiff) + ); + + editedPosition.costOfTrades = editedPosition.costOfTrades.sub( + editedPosition.costOfTrades + .mul(new anchor.BN(sizeNative)) + .div(currentSizeBN.abs()) + ); + + let openIndex = side == types.Side.BID ? 0 : 1; + let diff = anchor.BN.min( + editedOrderState.closingOrders, + new anchor.BN(sizeNative) + ); + editedOrderState.closingOrders = editedOrderState.closingOrders.sub(diff); + editedOrderState.openingOrders[openIndex] = + editedOrderState.openingOrders[openIndex].add(diff); + } + // If we're zeroing out the current position and opening a position on the other side + else { + if (Math.abs(currentSize) > 0) { + let entryPrice = new anchor.BN( + editedPosition.costOfTrades.toNumber() / + convertNativeLotSizeToDecimal(Math.abs(currentSize)) + ); + let priceDiff = entryPrice.sub( + new anchor.BN(convertDecimalToNativeInteger(price, 1)) + ); + marginAccount.balance = marginAccount.balance.add( + new anchor.BN( + side == types.Side.BID + ? convertNativeLotSizeToDecimal(currentSizeBN.abs()) + : -convertNativeLotSizeToDecimal(currentSizeBN.abs()) + ).mul(priceDiff) + ); + } + + editedPosition.costOfTrades = new anchor.BN( + convertNativeLotSizeToDecimal( + Math.abs(editedPosition.size.toNumber()) + ) * convertDecimalToNativeInteger(price, 1) + ); + + let sameSide = side == types.Side.BID ? 0 : 1; + let otherSide = side == types.Side.BID ? 1 : 0; + editedOrderState.openingOrders[sameSide] = editedOrderState.openingOrders[ + sameSide + ].add(editedOrderState.closingOrders); + + editedOrderState.closingOrders = anchor.BN.max( + editedOrderState.openingOrders[otherSide].sub( + editedPosition.size.abs() + ), + new anchor.BN(0) + ); + + editedOrderState.openingOrders[otherSide] = + editedOrderState.openingOrders[otherSide].sub( + editedOrderState.closingOrders + ); + } + } + // Fake the new order. editedPosition is untouched + else { + // Any non-filled trades have an extra PnL adjustment + // Only negative PnL is used + let pnlAdjustment = size * (markPrice - price); + pnlAdjustment = Math.min( + 0, + side == types.Side.BID ? pnlAdjustment : -pnlAdjustment + ); + marginAccount.balance = marginAccount.balance.add( + new anchor.BN(convertDecimalToNativeInteger(pnlAdjustment, 1)) + ); + + // If we're just adding an extra order on the same side as the existing position + if ( + (side == types.Side.BID && currentSize > 0) || + (side == types.Side.ASK && currentSize < 0) + ) { + let i = side == types.Side.BID ? 0 : 1; + editedOrderState.openingOrders[i] = editedOrderState.openingOrders[i].add( + new anchor.BN(sizeNative) + ); + } + + // If we're adding to the opposite side then both openingOrders and closingOrders change + else { + let i = side == types.Side.BID ? 0 : 1; + let newOrderSize = editedOrderState.closingOrders + .add(editedOrderState.openingOrders[i]) + .add(new anchor.BN(sizeNative)); + editedOrderState.closingOrders = anchor.BN.min( + newOrderSize, + editedPosition.size.abs() + ); + editedOrderState.openingOrders[i] = newOrderSize.sub( + editedOrderState.closingOrders + ); + } + } + + marginAccount.productLedgers[assetIndex].orderState = editedOrderState; + marginAccount.productLedgers[assetIndex].position = editedPosition; +} + +/** + * Simulate adding an extra position/order into an existing CrossMarginAccount, but deep copy the account first and return that deep copied account + * @param marginAccount the CrossMarginAccount itself, untouched if clone = true + * @param clone Whether to deep-copy the marginAccount as part of the function. You can speed up execution by providing your own already deep-copied marginAccount if calling this multiple times. + * @param executionInfo A hypothetical trade. Object containing: asset (Asset), price (decimal USDC), size (signed decimal), isTaker (whether or not it trades for full size) + * @returns The edited CrossMarginAccount with an added trade/order + */ +export function fakeTrade( + marginAccount: CrossMarginAccount, + clone: boolean, + executionInfo: types.ExecutionInfo +): CrossMarginAccount { + let account = clone + ? (cloneDeep(marginAccount) as CrossMarginAccount) + : marginAccount; + addFakeTradeToAccount( + account, + executionInfo.isTaker, + executionInfo.asset, + executionInfo.size > 0 ? types.Side.BID : types.Side.ASK, + executionInfo.price, + executionInfo.size + ); + return account; +} diff --git a/src/risk.ts b/src/risk.ts index 64d98a03c..1a4f5648d 100644 --- a/src/risk.ts +++ b/src/risk.ts @@ -12,8 +12,10 @@ import { assetToIndex, fromProgramAsset } from "./assets"; import { Asset } from "./constants"; import { assets, Decimal, programTypes } from "."; import { + addFakeTradeToAccount, calculateLiquidationPrice, calculateProductMargin, + fakeTrade, collectRiskMaps, } from "./risk-utils"; @@ -66,7 +68,7 @@ export class RiskCalculator { /** * Returns the margin requirement for a given market and size. * @param asset underlying asset type. - * @param size signed size for margin requirements (short orders should be negative), in lots. + * @param size signed size for margin requirements (short orders should be negative), in decimal USDC lots. * @param marginType type of margin calculation. */ public getPerpMarginRequirement( @@ -213,12 +215,7 @@ export class RiskCalculator { account: any, accountType: types.ProgramAccountType = types.ProgramAccountType .MarginAccount, - executionInfo: { - asset: Asset; - price: number; - size: number; - addTakerFees: boolean; - } + executionInfo: types.ExecutionInfo ): number { // Number for MarginAccount, Map for CrossMarginAccount if (accountType == types.ProgramAccountType.CrossMarginAccount) { @@ -274,24 +271,18 @@ export class RiskCalculator { private calculatePnl( account: MarginAccount, accountType: types.ProgramAccountType, - executionInfo: - | { asset: Asset; price: number; size: number; addTakerFees: boolean } - | undefined + executionInfo: types.ExecutionInfo | undefined ): number; private calculatePnl( account: CrossMarginAccount, accountType: types.ProgramAccountType, - executionInfo: - | { asset: Asset; price: number; size: number; addTakerFees: boolean } - | undefined + executionInfo: types.ExecutionInfo | undefined ): Map; private calculatePnl( account: any, accountType: types.ProgramAccountType = types.ProgramAccountType .MarginAccount, - executionInfo: - | { asset: Asset; price: number; size: number; addTakerFees: boolean } - | undefined = undefined + executionInfo: types.ExecutionInfo | undefined = undefined ): any { let pnl = 0; @@ -372,7 +363,7 @@ export class RiskCalculator { if ( executionInfo && executionInfo.asset == asset && - executionInfo.addTakerFees + executionInfo.isTaker ) { assetPnl -= convertNativeLotSizeToDecimal(Math.abs(size)) * @@ -723,6 +714,7 @@ export class RiskCalculator { maintenanceMarginIncludingOrders.values() ).reduce((a, b) => a + b, 0); + let equity: number = balance + upnlTotal + unpaidFundingTotal; let availableBalanceInitial: number = balance + upnlTotal + unpaidFundingTotal - imTotal; let availableBalanceWithdrawable: number = @@ -736,6 +728,7 @@ export class RiskCalculator { balance + upnlTotal + unpaidFundingTotal - mmioTotal; return { balance, + equity, availableBalanceInitial, availableBalanceMaintenance, availableBalanceMaintenanceIncludingOrders, @@ -787,6 +780,7 @@ export class RiskCalculator { marginAccount, types.ProgramAccountType.MarginAccount ) as number; + let equity: number = balance + unrealizedPnl + unpaidFunding; let availableBalanceInitial: number = balance + unrealizedPnl + unpaidFunding - initialMargin; let availableBalanceWithdrawable: number = @@ -795,6 +789,7 @@ export class RiskCalculator { balance + unrealizedPnl + unpaidFunding - maintenanceMargin; return { balance, + equity, initialMargin, initialMarginSkipConcession, maintenanceMargin, @@ -850,7 +845,7 @@ export class RiskCalculator { asset: tradeAsset, price: tradePrice, size: undefined, - addTakerFees: isTaker, + isTaker: isTaker, } ).values() ).reduce((a, b) => a + b, 0) + @@ -960,159 +955,14 @@ export class RiskCalculator { cloneDeep(currentOrderState); editedAccount.balance = currentBalance; - let editedPosition = editedAccount.productLedgers[assetIndex].position; - let editedOrderState = - editedAccount.productLedgers[assetIndex].orderState; - - let sizeNative = convertDecimalToNativeLotSize(size); - let currentSizeBN = editedPosition.size; - let currentSize = currentSizeBN.toNumber(); - // Fake the new position, moving both editedPosition and editedOrderState - if (isTaker) { - editedPosition.size = editedPosition.size.add( - new anchor.BN(tradeSide == types.Side.BID ? sizeNative : -sizeNative) - ); - editedAccount.balance = editedAccount.balance.sub( - new anchor.BN(convertDecimalToNativeInteger(fee * size, 1)) - ); - - // If we're just adding to costOfTrades - if ( - (tradeSide == types.Side.BID && currentSize > 0) || - (tradeSide == types.Side.ASK && currentSize < 0) - ) { - editedPosition.costOfTrades = editedPosition.costOfTrades.add( - new anchor.BN(size * convertDecimalToNativeInteger(tradePrice, 1)) - ); - - let openIndex = tradeSide == types.Side.BID ? 1 : 0; - let diff = anchor.BN.min( - editedOrderState.openingOrders[openIndex], - new anchor.BN(sizeNative) - ); - editedOrderState.closingOrders = - editedOrderState.closingOrders.add(diff); - editedOrderState.openingOrders[openIndex] = - editedOrderState.openingOrders[openIndex].sub(diff); - } - // If we're just reducing the current position - else if (sizeNative < Math.abs(currentSize)) { - let entryPrice = new anchor.BN( - editedPosition.costOfTrades.toNumber() / - convertNativeLotSizeToDecimal(Math.abs(currentSize)) - ); - let priceDiff = entryPrice.sub( - new anchor.BN(convertDecimalToNativeInteger(tradePrice, 1)) - ); - editedAccount.balance = editedAccount.balance.add( - new anchor.BN(tradeSide == types.Side.BID ? size : -size).mul( - priceDiff - ) - ); - - editedPosition.costOfTrades = editedPosition.costOfTrades.sub( - editedPosition.costOfTrades - .mul(new anchor.BN(sizeNative)) - .div(currentSizeBN.abs()) - ); - - let openIndex = tradeSide == types.Side.BID ? 0 : 1; - let diff = anchor.BN.min( - editedOrderState.closingOrders, - new anchor.BN(sizeNative) - ); - editedOrderState.closingOrders = - editedOrderState.closingOrders.sub(diff); - editedOrderState.openingOrders[openIndex] = - editedOrderState.openingOrders[openIndex].add(diff); - } - // If we're zeroing out the current position and opening a position on the other side - else { - if (Math.abs(currentSize) > 0) { - let entryPrice = new anchor.BN( - editedPosition.costOfTrades.toNumber() / - convertNativeLotSizeToDecimal(Math.abs(currentSize)) - ); - let priceDiff = entryPrice.sub( - new anchor.BN(convertDecimalToNativeInteger(tradePrice, 1)) - ); - editedAccount.balance = editedAccount.balance.add( - new anchor.BN( - tradeSide == types.Side.BID - ? convertNativeLotSizeToDecimal(currentSizeBN.abs()) - : -convertNativeLotSizeToDecimal(currentSizeBN.abs()) - ).mul(priceDiff) - ); - } - - editedPosition.costOfTrades = new anchor.BN( - convertNativeLotSizeToDecimal( - Math.abs(editedPosition.size.toNumber()) - ) * convertDecimalToNativeInteger(tradePrice, 1) - ); - - let sameSide = tradeSide == types.Side.BID ? 0 : 1; - let otherSide = tradeSide == types.Side.BID ? 1 : 0; - editedOrderState.openingOrders[sameSide] = - editedOrderState.openingOrders[sameSide].add( - editedOrderState.closingOrders - ); - - editedOrderState.closingOrders = anchor.BN.max( - editedOrderState.openingOrders[otherSide].sub( - editedPosition.size.abs() - ), - new anchor.BN(0) - ); - - editedOrderState.openingOrders[otherSide] = - editedOrderState.openingOrders[otherSide].sub( - editedOrderState.closingOrders - ); - } - } - // Fake the new order. editedPosition is untouched - else { - // Any non-filled trades have an extra PnL adjustment - // Only negative PnL is used - let pnlAdjustment = size * (markPrice - tradePrice); - pnlAdjustment = Math.min( - 0, - tradeSide == types.Side.BID ? pnlAdjustment : -pnlAdjustment - ); - editedAccount.balance = editedAccount.balance.add( - new anchor.BN(convertDecimalToNativeInteger(pnlAdjustment, 1)) - ); - - // If we're just adding an extra order on the same side as the existing position - if ( - (tradeSide == types.Side.BID && currentSize > 0) || - (tradeSide == types.Side.ASK && currentSize < 0) - ) { - let i = tradeSide == types.Side.BID ? 0 : 1; - editedOrderState.openingOrders[i] = editedOrderState.openingOrders[ - i - ].add(new anchor.BN(sizeNative)); - } - - // If we're adding to the opposite side then both openingOrders and closingOrders change - else { - let i = tradeSide == types.Side.BID ? 0 : 1; - let newOrderSize = editedOrderState.closingOrders - .add(editedOrderState.openingOrders[i]) - .add(new anchor.BN(sizeNative)); - editedOrderState.closingOrders = anchor.BN.min( - newOrderSize, - editedPosition.size.abs() - ); - editedOrderState.openingOrders[i] = newOrderSize.sub( - editedOrderState.closingOrders - ); - } - } - - editedAccount.productLedgers[assetIndex].orderState = editedOrderState; - editedAccount.productLedgers[assetIndex].position = editedPosition; + addFakeTradeToAccount( + editedAccount, + isTaker, + tradeAsset, + tradeSide, + tradePrice, + size + ); // TODO if this is slow then do only the necessary calcs manually, there's a bunch of extra calcs in here // that aren't needed in getMaxTradeSize() @@ -1165,4 +1015,131 @@ export class RiskCalculator { signedPosition ); } + + /** + * Get an account's equity, which is the balance including unrealized PnL and unpaid funding. + * You can optionally provide executionInfo to mimick a fake order/trade, which will return the account's equity after that order/trade occurs. + * @param marginAccount The CrossMarginAccount itself, edited in-place if executionInfo is provided + * @param executionInfo (Optional) A hypothetical trade. Object containing: asset (Asset), price (decimal USDC), size (signed decimal), isTaker (whether or not it trades for full size) + * @param clone Whether to deep-copy the marginAccount as part of the function. You can speed up execution by providing your own already deep-copied marginAccount if calling multiple risk.ts functions. + * @returns A decimal USDC representing the account equity + */ + public getEquity( + marginAccount: CrossMarginAccount, + executionInfo?: types.ExecutionInfo, + clone: boolean = true + ): number { + let account = marginAccount; + if (executionInfo) { + account = fakeTrade(marginAccount, clone, executionInfo); + } + return this.getCrossMarginAccountState(account).equity; + } + + /** + * Get an account's buying power, which is the position size you can get additional exposure to. + * You can optionally provide executionInfo to mimick a fake order/trade, which will return the account's buying power after that order/trade occurs. + * @param marginAccount The CrossMarginAccount itself, edited in-place if executionInfo is provided + * @param asset The underlying for which we're estimating buying power + * @param executionInfo (Optional) A hypothetical trade. Object containing: asset (Asset), price (decimal USDC), size (signed decimal), isTaker (whether or not it trades for full size) + * @param clone Whether to deep-copy the marginAccount as part of the function. You can speed up execution by providing your own already deep-copied marginAccount if calling multiple risk.ts functions. + * @returns A decimal USDC representing the buying power + */ + public getBuyingPower( + marginAccount: CrossMarginAccount, + asset: Asset, + executionInfo?: types.ExecutionInfo, + clone: boolean = true + ): number { + let account = marginAccount; + let markPrice = Exchange.getMarkPrice(asset); + let initialMarginPerLot = this.getPerpMarginRequirement( + asset, + 1, + types.MarginType.INITIAL + ); + + if (executionInfo) { + account = fakeTrade(marginAccount, clone, executionInfo); + } + let state = this.getCrossMarginAccountState(account); + let freeCollateral = state.availableBalanceInitial; + + return freeCollateral * (markPrice / initialMarginPerLot); + } + + /** + * Get an account's margin usage, which is a decimal percentage from 0 to 100 representing the percentage of equity used in maintenance margin. + * You can optionally provide executionInfo to mimick a fake order/trade, which will return the account's margin usage after that order/trade occurs. + * @param marginAccount The CrossMarginAccount itself, edited in-place if executionInfo is provided + * @param executionInfo (Optional) A hypothetical trade. Object containing: asset (Asset), price (decimal USDC), size (signed decimal), isTaker (whether or not it trades for full size) + * @param clone Whether to deep-copy the marginAccount as part of the function. You can speed up execution by providing your own already deep-copied marginAccount if calling multiple risk.ts functions. + * @returns A decimal percentage representing margin usage. + */ + public getMarginUsagePercent( + marginAccount: CrossMarginAccount, + executionInfo?: types.ExecutionInfo, + clone: boolean = true + ): number { + let account = marginAccount; + if (executionInfo) { + account = fakeTrade(marginAccount, clone, executionInfo); + } + let state = this.getCrossMarginAccountState(account); + return 100 * (state.maintenanceMarginTotal / state.equity); + } + + /** + * Get an account's free collateral, which is the amount of available collateral the account has for trading. Equivalent to 'availableBalanceInitial' + * You can optionally provide executionInfo to mimick a fake order/trade, which will return the account's free collateral after that order/trade occurs. + * @param marginAccount The CrossMarginAccount itself, edited in-place if executionInfo is provided + * @param executionInfo (Optional) A hypothetical trade. Object containing: asset (Asset), price (decimal USDC), size (signed decimal), isTaker (whether or not it trades for full size) + * @param clone Whether to deep-copy the marginAccount as part of the function. You can speed up execution by providing your own already deep-copied marginAccount if calling multiple risk.ts functions. + * @returns A decimal USDC representing the available collateral. + */ + public getFreeCollateral( + marginAccount: CrossMarginAccount, + executionInfo?: types.ExecutionInfo, + clone: boolean = true + ): number { + let account = marginAccount; + if (executionInfo) { + account = fakeTrade(marginAccount, clone, executionInfo); + } + return this.getCrossMarginAccountState(account).availableBalanceInitial; + } + + /** + * Get an account's current leverage + * You can optionally provide executionInfo to mimick a fake order/trade, which will return the account's current leverage after that order/trade occurs. + * @param marginAccount The CrossMarginAccount itself, edited in-place if executionInfo is provided + * @param executionInfo (Optional) A hypothetical trade. Object containing: asset (Asset), price (decimal USDC), size (signed decimal), isTaker (whether or not it trades for full size) + * @param clone Whether to deep-copy the marginAccount as part of the function. You can speed up execution by providing your own already deep-copied marginAccount if calling multiple risk.ts functions. + * @returns A decimal value representing the current leverage. + */ + public getLeverage( + marginAccount: CrossMarginAccount, + executionInfo?: types.ExecutionInfo, + clone: boolean = true + ) { + let account = marginAccount; + if (executionInfo) { + account = fakeTrade(marginAccount, clone, executionInfo); + } + + // Sum up all the positions in the account + let positionValue = 0; + for (var asset of Exchange.assets) { + positionValue += + Math.abs( + convertNativeLotSizeToDecimal( + account.productLedgers[ + assets.assetToIndex(asset) + ].position.size.toNumber() + ) + ) * Exchange.getMarkPrice(asset); + } + + return positionValue / this.getCrossMarginAccountState(account).equity; + } } diff --git a/src/types.ts b/src/types.ts index da0022db7..d1214eba9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -205,6 +205,7 @@ export interface MarginRequirement { export interface MarginAccountState { balance: number; + equity: number; initialMargin: number; initialMarginSkipConcession: number; maintenanceMargin: number; @@ -226,6 +227,7 @@ export interface AssetRiskState { export interface CrossMarginAccountState { balance: number; + equity: number; availableBalanceInitial: number; availableBalanceMaintenance: number; availableBalanceMaintenanceIncludingOrders: number; @@ -404,3 +406,10 @@ export function defaultLoadExchangeConfig( loadFromStore, }; } + +export interface ExecutionInfo { + asset: Asset; + price: number; + size: number; + isTaker: boolean; +} diff --git a/src/utils.ts b/src/utils.ts index c708f1bd6..8b14fd9ce 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -47,6 +47,7 @@ import { Decimal } from "./decimal"; import { readBigInt64LE } from "./oracle-utils"; import { assets } from "."; import { Network } from "./network"; +import cloneDeep from "lodash.clonedeep"; export function getState(programId: PublicKey): [PublicKey, number] { return anchor.web3.PublicKey.findProgramAddressSync( @@ -1789,3 +1790,9 @@ const checkWithinSlippageTolerance = ( return Math.abs(price - markPrice) <= Math.abs(maxSlippage); }; + +export function deepCloneCrossMarginAccount( + marginAccount: CrossMarginAccount +): CrossMarginAccount { + return cloneDeep(marginAccount) as CrossMarginAccount; +}