2173 lines
88 KiB
TypeScript
2173 lines
88 KiB
TypeScript
import { ChevronUpIcon, RefreshIcon } from '@heroicons/react/outline'
|
|
import { Table, Thead, Tbody, Tr, Th, Td } from 'react-super-responsive-table'
|
|
import { Disclosure } from '@headlessui/react'
|
|
import useMangoStore, { serumProgramId } from '../stores/useMangoStore'
|
|
import PageBodyContainer from '../components/PageBodyContainer'
|
|
import TopBar from '../components/TopBar'
|
|
import Button, { LinkButton } from '../components/Button'
|
|
import Input from '../components/Input'
|
|
import { useState, useEffect } from 'react'
|
|
import Tooltip from '../components/Tooltip'
|
|
import {
|
|
floorToDecimal,
|
|
tokenPrecision,
|
|
perpContractPrecision,
|
|
roundToDecimal,
|
|
} from '../utils/index'
|
|
import { formatUsdValue, usdFormatter } from '../utils'
|
|
import {
|
|
getMarketIndexBySymbol,
|
|
getTokenBySymbol,
|
|
getMarketByPublicKey,
|
|
QUOTE_INDEX,
|
|
} from '@blockworks-foundation/mango-client'
|
|
import Switch from '../components/Switch'
|
|
import Slider from 'rc-slider'
|
|
import 'rc-slider/assets/index.css'
|
|
import { AnchorIcon } from '../components/icons'
|
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
|
import { useTranslation } from 'next-i18next'
|
|
import { useRouter } from 'next/router'
|
|
import { PublicKey } from '@solana/web3.js'
|
|
|
|
export async function getStaticProps({ locale }) {
|
|
return {
|
|
props: {
|
|
...(await serverSideTranslations(locale, ['common', 'calculator'])),
|
|
},
|
|
}
|
|
}
|
|
|
|
interface CalculatorRow {
|
|
symbolName: string
|
|
oracleIndex: number
|
|
hasMarketSpot: boolean
|
|
hasMarketPerp: boolean
|
|
price: number
|
|
spotNet: number
|
|
spotDeposit: number
|
|
spotBorrow: number
|
|
spotInOrders: number
|
|
spotBaseTokenFree: number
|
|
spotBaseTokenLocked: number
|
|
spotQuoteTokenFree: number
|
|
spotQuoteTokenLocked: number
|
|
spotMarketIndex: number
|
|
spotPublicKey: number
|
|
perpBasePosition: number
|
|
perpInOrders: number
|
|
perpBids: number
|
|
perpAsks: number
|
|
perpAvgEntryPrice: number
|
|
perpScenarioPnL: number
|
|
perpPositionPnL: number
|
|
perpUnsettledFunding: number
|
|
perpPositionSide: string
|
|
perpBaseLotSize: number
|
|
perpQuoteLotSize: number
|
|
perpMarketIndex: number
|
|
perpPublicKey: number
|
|
initAssetWeightSpot: number
|
|
initLiabWeightSpot: number
|
|
maintAssetWeightSpot: number
|
|
maintLiabWeightSpot: number
|
|
initAssetWeightPerp: number
|
|
initLiabWeightPerp: number
|
|
maintAssetWeightPerp: number
|
|
maintLiabWeightPerp: number
|
|
precision: number
|
|
priceDisabled: boolean
|
|
}
|
|
|
|
interface ScenarioCalculator {
|
|
rowData: CalculatorRow[]
|
|
}
|
|
|
|
export default function RiskCalculator() {
|
|
const { t } = useTranslation(['common', 'calculator'])
|
|
const riskRanks = [
|
|
t('calculator:great'),
|
|
t('calculator:ok'),
|
|
t('calculator:poor'),
|
|
t('calculator:very-poor'),
|
|
t('calculator:rekt'),
|
|
]
|
|
|
|
// Get mango account data
|
|
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
|
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
|
|
const mangoConfig = useMangoStore((s) => s.selectedMangoGroup.config)
|
|
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
|
|
const connected = useMangoStore((s) => s.wallet.connected)
|
|
const setMangoStore = useMangoStore((s) => s.set)
|
|
const router = useRouter()
|
|
const { pubkey } = router.query
|
|
|
|
// Set default state variables
|
|
const [sliderPercentage, setSliderPercentage] = useState(0)
|
|
const [scenarioInitialized, setScenarioInitialized] = useState(false)
|
|
const [blankScenarioInitialized, setBlankScenarioInitialized] =
|
|
useState(false)
|
|
const [scenarioBars, setScenarioBars] = useState<ScenarioCalculator>()
|
|
const [accountConnected, setAccountConnected] = useState(false)
|
|
const [showZeroBalances, setShowZeroBalances] = useState(true)
|
|
const [interimValue, setInterimValue] = useState(new Map())
|
|
const [ordersAsBalance, toggleOrdersAsBalance] = useState(false)
|
|
const [resetOnLeave, setResetOnLeave] = useState(false)
|
|
const defaultSliderVal = 1
|
|
|
|
useEffect(() => {
|
|
if (connected) {
|
|
router.push('/risk-calculator')
|
|
setScenarioInitialized(false)
|
|
}
|
|
}, [connected])
|
|
|
|
useEffect(() => {
|
|
async function loadUnownedMangoAccount() {
|
|
try {
|
|
const unownedMangoAccountPubkey = new PublicKey(pubkey)
|
|
const mangoClient = useMangoStore.getState().connection.client
|
|
if (mangoGroup) {
|
|
const unOwnedMangoAccount = await mangoClient.getMangoAccount(
|
|
unownedMangoAccountPubkey,
|
|
serumProgramId
|
|
)
|
|
setMangoStore((state) => {
|
|
state.selectedMangoAccount.current = unOwnedMangoAccount
|
|
})
|
|
setScenarioInitialized(false)
|
|
setResetOnLeave(true)
|
|
}
|
|
} catch (error) {
|
|
router.push('/risk-calculator')
|
|
}
|
|
}
|
|
|
|
if (pubkey) {
|
|
loadUnownedMangoAccount()
|
|
}
|
|
}, [pubkey, mangoGroup])
|
|
|
|
useEffect(() => {
|
|
const handleRouteChange = () => {
|
|
if (resetOnLeave) {
|
|
setMangoStore((state) => {
|
|
state.selectedMangoAccount.current = undefined
|
|
})
|
|
}
|
|
}
|
|
router.events.on('routeChangeStart', handleRouteChange)
|
|
return () => {
|
|
router.events.off('routeChangeStart', handleRouteChange)
|
|
}
|
|
}, [resetOnLeave])
|
|
|
|
// Set rules for updating the scenario
|
|
useEffect(() => {
|
|
if (mangoGroup && mangoCache) {
|
|
if (mangoAccount && !scenarioInitialized) {
|
|
setSliderPercentage(defaultSliderVal)
|
|
createScenario('account')
|
|
setScenarioInitialized(true)
|
|
setAccountConnected(true)
|
|
setBlankScenarioInitialized(false)
|
|
} else if (
|
|
!mangoAccount &&
|
|
!scenarioInitialized &&
|
|
!blankScenarioInitialized
|
|
) {
|
|
setSliderPercentage(defaultSliderVal)
|
|
createScenario('blank')
|
|
setBlankScenarioInitialized(true)
|
|
}
|
|
}
|
|
}, [connected, mangoAccount, scenarioInitialized, mangoGroup, mangoCache])
|
|
|
|
// Handle toggling open order inclusion or order cancelling for heatlh calculation
|
|
useEffect(() => {
|
|
if (mangoGroup && mangoCache && mangoAccount) {
|
|
setScenarioInitialized(!scenarioInitialized)
|
|
createScenario('account')
|
|
}
|
|
}, [ordersAsBalance])
|
|
|
|
// Retrieve the data to create the scenario table
|
|
const createScenario = (type) => {
|
|
const rowData = []
|
|
let calculatorRowData
|
|
for (let i = -1; i < mangoGroup.numOracles; i++) {
|
|
// Get market configuration data
|
|
const spotMarketConfig =
|
|
i < 0
|
|
? null
|
|
: getMarketByPublicKey(
|
|
mangoConfig,
|
|
mangoGroup.spotMarkets[i].spotMarket
|
|
)
|
|
const perpMarketConfig =
|
|
i < 0
|
|
? null
|
|
: getMarketByPublicKey(
|
|
mangoConfig,
|
|
mangoGroup.perpMarkets[i].perpMarket
|
|
)
|
|
const symbol =
|
|
i < 0
|
|
? 'USDC'
|
|
: spotMarketConfig?.baseSymbol
|
|
? spotMarketConfig?.baseSymbol
|
|
: perpMarketConfig?.baseSymbol
|
|
|
|
// Retrieve spot balances if present
|
|
const spotDeposit =
|
|
Number(
|
|
mangoAccount && spotMarketConfig
|
|
? mangoAccount.getUiDeposit(
|
|
mangoCache.rootBankCache[spotMarketConfig.marketIndex],
|
|
mangoGroup,
|
|
spotMarketConfig.marketIndex
|
|
)
|
|
: mangoAccount && symbol === 'USDC'
|
|
? mangoAccount.getUiDeposit(
|
|
mangoCache.rootBankCache[QUOTE_INDEX],
|
|
mangoGroup,
|
|
QUOTE_INDEX
|
|
)
|
|
: 0
|
|
) || 0
|
|
const spotBorrow =
|
|
Number(
|
|
mangoAccount && spotMarketConfig
|
|
? mangoAccount.getUiBorrow(
|
|
mangoCache.rootBankCache[spotMarketConfig.marketIndex],
|
|
mangoGroup,
|
|
spotMarketConfig.marketIndex
|
|
)
|
|
: mangoAccount && symbol === 'USDC'
|
|
? mangoAccount.getUiBorrow(
|
|
mangoCache.rootBankCache[QUOTE_INDEX],
|
|
mangoGroup,
|
|
QUOTE_INDEX
|
|
)
|
|
: 0
|
|
) || 0
|
|
const spotBaseTokenLocked =
|
|
mangoAccount && spotMarketConfig
|
|
? Number(
|
|
mangoAccount.spotOpenOrdersAccounts[i]?.baseTokenTotal.sub(
|
|
mangoAccount.spotOpenOrdersAccounts[i]?.baseTokenFree
|
|
)
|
|
) / Math.pow(10, spotMarketConfig.baseDecimals) || 0
|
|
: 0
|
|
const spotQuoteTokenLocked =
|
|
mangoAccount && spotMarketConfig
|
|
? Number(
|
|
mangoAccount.spotOpenOrdersAccounts[i]?.quoteTokenTotal.sub(
|
|
mangoAccount.spotOpenOrdersAccounts[i]?.quoteTokenFree
|
|
)
|
|
) / Math.pow(10, 6) || 0
|
|
: 0
|
|
const spotBaseTokenFree =
|
|
mangoAccount && spotMarketConfig
|
|
? Number(mangoAccount.spotOpenOrdersAccounts[i]?.baseTokenFree) /
|
|
Math.pow(10, spotMarketConfig.baseDecimals) || 0
|
|
: 0
|
|
const spotQuoteTokenFree =
|
|
mangoAccount && spotMarketConfig
|
|
? Number(mangoAccount.spotOpenOrdersAccounts[i]?.quoteTokenFree) /
|
|
Math.pow(10, 6) || 0
|
|
: 0
|
|
let inOrders = 0
|
|
if (symbol === 'USDC' && ordersAsBalance) {
|
|
for (let j = 0; j < mangoGroup.tokens.length; j++) {
|
|
const inOrder =
|
|
j !== QUOTE_INDEX &&
|
|
mangoConfig.spotMarkets[j]?.publicKey &&
|
|
mangoAccount?.spotOpenOrdersAccounts[j]?.quoteTokenTotal
|
|
? mangoAccount.spotOpenOrdersAccounts[j].quoteTokenTotal
|
|
: 0
|
|
inOrders += Number(inOrder) / Math.pow(10, 6)
|
|
}
|
|
} else {
|
|
inOrders =
|
|
spotMarketConfig &&
|
|
mangoAccount?.spotOpenOrdersAccounts[i]?.baseTokenTotal
|
|
? Number(mangoAccount.spotOpenOrdersAccounts[i].baseTokenTotal) /
|
|
Math.pow(10, spotMarketConfig.baseDecimals)
|
|
: 0
|
|
}
|
|
|
|
// Retrieve perp positions if present
|
|
const perpPosition =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? mangoAccount?.perpAccounts[i]
|
|
: null
|
|
const perpMarketIndex =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? getMarketIndexBySymbol(mangoConfig, symbol)
|
|
: null
|
|
const perpAccount =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? mangoAccount?.perpAccounts[perpMarketIndex]
|
|
: null
|
|
const perpMarketCache =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? mangoCache?.perpMarketCache[perpMarketIndex]
|
|
: null
|
|
const perpMarketInfo =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? mangoGroup?.perpMarkets[perpMarketIndex]
|
|
: null
|
|
const basePosition =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? Number(perpAccount?.basePosition) /
|
|
Math.pow(10, perpContractPrecision[symbol]) || 0
|
|
: 0
|
|
const unsettledFunding =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? (Number(perpAccount?.getUnsettledFunding(perpMarketCache)) *
|
|
basePosition) /
|
|
Math.pow(10, 6) || 0
|
|
: 0
|
|
const positionPnL =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? Number(
|
|
perpAccount?.getPnl(
|
|
perpMarketInfo,
|
|
perpMarketCache,
|
|
mangoCache.priceCache[perpMarketIndex].price
|
|
)
|
|
) / Math.pow(10, 6) || 0
|
|
: 0
|
|
const perpBids =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? Number(perpPosition?.bidsQuantity) /
|
|
Math.pow(10, perpContractPrecision[symbol]) || 0
|
|
: Number(0)
|
|
const perpAsks =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? Number(perpPosition?.asksQuantity) /
|
|
Math.pow(10, perpContractPrecision[symbol]) || 0
|
|
: Number(0)
|
|
|
|
if (
|
|
spotMarketConfig?.publicKey ||
|
|
perpMarketConfig?.publicKey ||
|
|
symbol === 'USDC'
|
|
) {
|
|
calculatorRowData = {
|
|
symbolName: symbol,
|
|
oracleIndex: symbol === 'USDC' ? null : i,
|
|
hasMarketSpot: spotMarketConfig?.publicKey ? true : false,
|
|
hasMarketPerp: perpMarketConfig?.publicKey ? true : false,
|
|
priceDisabled: symbol === 'USDC' || symbol === 'USDT' ? true : false,
|
|
price: floorToDecimal(
|
|
Number(
|
|
mangoGroup.getPrice(
|
|
i < 0
|
|
? mangoGroup.getTokenIndex(
|
|
getTokenBySymbol(mangoConfig, 'USDC').mintKey
|
|
)
|
|
: i,
|
|
mangoCache
|
|
)
|
|
),
|
|
6
|
|
),
|
|
spotNet:
|
|
type === 'account'
|
|
? Number(
|
|
floorToDecimal(
|
|
spotDeposit - spotBorrow + (ordersAsBalance ? inOrders : 0),
|
|
spotMarketConfig?.baseDecimals || 6
|
|
)
|
|
)
|
|
: Number(0),
|
|
spotDeposit:
|
|
type === 'account'
|
|
? Number(
|
|
floorToDecimal(
|
|
spotDeposit,
|
|
spotMarketConfig?.baseDecimals || 6
|
|
)
|
|
)
|
|
: Number(0),
|
|
spotBorrow:
|
|
type === 'account'
|
|
? Number(
|
|
floorToDecimal(
|
|
spotBorrow,
|
|
spotMarketConfig?.baseDecimals || 6
|
|
)
|
|
)
|
|
: Number(0),
|
|
spotInOrders:
|
|
type === 'account'
|
|
? Number(floorToDecimal(inOrders, 6))
|
|
: Number(0),
|
|
spotBaseTokenFree:
|
|
type === 'account'
|
|
? Number(floorToDecimal(spotBaseTokenFree, 6))
|
|
: Number(0),
|
|
spotBaseTokenLocked:
|
|
type === 'account'
|
|
? Number(floorToDecimal(spotBaseTokenLocked, 6))
|
|
: Number(0),
|
|
spotQuoteTokenFree:
|
|
type === 'account'
|
|
? Number(floorToDecimal(spotQuoteTokenFree, 6))
|
|
: Number(0),
|
|
spotQuoteTokenLocked:
|
|
type === 'account'
|
|
? Number(floorToDecimal(spotQuoteTokenLocked, 6))
|
|
: Number(0),
|
|
spotMarketIndex: mangoGroup.spotMarkets[i]?.spotMarket
|
|
? spotMarketConfig?.marketIndex
|
|
: null,
|
|
spotPublicKey: mangoGroup.spotMarkets[i]?.spotMarket
|
|
? mangoGroup.spotMarkets[i]?.spotMarket
|
|
: null,
|
|
perpAvgEntryPrice: floorToDecimal(
|
|
Number(
|
|
mangoGroup.getPrice(
|
|
i < 0
|
|
? mangoGroup.getTokenIndex(
|
|
getTokenBySymbol(mangoConfig, 'USDC').mintKey
|
|
)
|
|
: i,
|
|
mangoCache
|
|
)
|
|
),
|
|
6
|
|
),
|
|
perpBasePosition:
|
|
type === 'account' && perpMarketConfig?.publicKey
|
|
? Number(basePosition)
|
|
: Number(0),
|
|
perpScenarioPnL: 0,
|
|
perpPositionPnL:
|
|
type === 'account' && perpMarketConfig?.publicKey
|
|
? Number(floorToDecimal(positionPnL, 6))
|
|
: Number(0),
|
|
perpUnsettledFunding:
|
|
type === 'account' && perpMarketConfig?.publicKey
|
|
? Number(floorToDecimal(unsettledFunding, 6))
|
|
: Number(0),
|
|
perpInOrders:
|
|
type === 'account' && perpMarketConfig?.publicKey
|
|
? perpBids > Math.abs(perpAsks)
|
|
? perpBids
|
|
: perpAsks
|
|
: Number(0),
|
|
perpBids:
|
|
type === 'account' && perpMarketConfig?.publicKey
|
|
? perpBids || 0
|
|
: Number(0),
|
|
perpAsks:
|
|
type === 'account' && perpMarketConfig?.publicKey
|
|
? perpAsks || 0
|
|
: Number(0),
|
|
perpPositionSide:
|
|
type === 'account' &&
|
|
perpMarketConfig?.publicKey &&
|
|
basePosition < 0
|
|
? 'short'
|
|
: 'long',
|
|
perpBaseLotSize: mangoGroup.perpMarkets[i]?.perpMarket
|
|
? Number(mangoGroup.perpMarkets[i]?.baseLotSize)
|
|
: null,
|
|
perpQuoteLotSize: mangoGroup.perpMarkets[i]?.perpMarket
|
|
? Number(mangoGroup.perpMarkets[i]?.quoteLotSize)
|
|
: null,
|
|
perpMarketIndex: mangoGroup.perpMarkets[i]?.perpMarket
|
|
? mangoGroup.perpMarkets[i]?.perpMarket
|
|
: null,
|
|
perpPublicKey: mangoGroup.perpMarkets[i]?.perpMarket
|
|
? mangoGroup.perpMarkets[i]?.perpMarket
|
|
: null,
|
|
initAssetWeightSpot:
|
|
symbol === 'USDC'
|
|
? 1
|
|
: mangoGroup.spotMarkets[i]?.spotMarket
|
|
? roundToDecimal(
|
|
Number(mangoGroup.spotMarkets[i]?.initAssetWeight),
|
|
2
|
|
)
|
|
: 1,
|
|
initLiabWeightSpot:
|
|
symbol === 'USDC'
|
|
? 1
|
|
: mangoGroup.spotMarkets[i]?.spotMarket
|
|
? roundToDecimal(
|
|
Number(mangoGroup.spotMarkets[i]?.initLiabWeight),
|
|
2
|
|
)
|
|
: 1,
|
|
maintAssetWeightSpot:
|
|
symbol === 'USDC'
|
|
? 1
|
|
: mangoGroup.spotMarkets[i]?.spotMarket
|
|
? roundToDecimal(
|
|
Number(mangoGroup.spotMarkets[i]?.maintAssetWeight),
|
|
2
|
|
)
|
|
: 1,
|
|
maintLiabWeightSpot:
|
|
symbol === 'USDC'
|
|
? 1
|
|
: mangoGroup.spotMarkets[i]?.spotMarket
|
|
? roundToDecimal(
|
|
Number(mangoGroup.spotMarkets[i]?.maintLiabWeight),
|
|
2
|
|
)
|
|
: 1,
|
|
initAssetWeightPerp:
|
|
symbol === 'USDC'
|
|
? 1
|
|
: mangoGroup.perpMarkets[i]?.perpMarket
|
|
? roundToDecimal(
|
|
Number(mangoGroup.perpMarkets[i]?.initAssetWeight),
|
|
2
|
|
)
|
|
: 1,
|
|
initLiabWeightPerp:
|
|
symbol === 'USDC'
|
|
? 1
|
|
: mangoGroup.perpMarkets[i]?.perpMarket
|
|
? roundToDecimal(
|
|
Number(mangoGroup.perpMarkets[i]?.initLiabWeight),
|
|
2
|
|
)
|
|
: 1,
|
|
maintAssetWeightPerp:
|
|
symbol === 'USDC'
|
|
? 1
|
|
: mangoGroup.perpMarkets[i]?.perpMarket
|
|
? roundToDecimal(
|
|
Number(mangoGroup.perpMarkets[i]?.maintAssetWeight),
|
|
2
|
|
)
|
|
: 1,
|
|
maintLiabWeightPerp:
|
|
symbol === 'USDC'
|
|
? 1
|
|
: mangoGroup.perpMarkets[i]?.perpMarket
|
|
? roundToDecimal(
|
|
Number(mangoGroup.perpMarkets[i]?.maintLiabWeight),
|
|
2
|
|
)
|
|
: 1,
|
|
precision:
|
|
symbol === 'USDC'
|
|
? 4
|
|
: mangoGroup.spotMarkets[i]?.spotMarket
|
|
? tokenPrecision[spotMarketConfig?.baseSymbol]
|
|
: tokenPrecision[perpMarketConfig?.baseSymbol] || 6,
|
|
}
|
|
|
|
rowData.push(calculatorRowData)
|
|
}
|
|
}
|
|
|
|
const calcData = updateCalculator(rowData)
|
|
setScenarioBars(calcData)
|
|
}
|
|
|
|
// Update the rows for the scenario
|
|
const updateCalculator = (rowData: CalculatorRow[]) => {
|
|
return {
|
|
rowData: rowData,
|
|
} as ScenarioCalculator
|
|
}
|
|
|
|
// Reset column details
|
|
const resetScenarioColumn = (column) => {
|
|
let resetRowData
|
|
mangoGroup
|
|
? (resetRowData = scenarioBars.rowData.map((asset) => {
|
|
let resetValue: number
|
|
let resetDeposit: number
|
|
let resetBorrow: number
|
|
let resetInOrders: number
|
|
let resetPositionSide: string
|
|
let resetPerpPositionPnL: number
|
|
let resetPerpUnsettledFunding: number
|
|
let resetPerpInOrders: number
|
|
|
|
switch (column) {
|
|
case 'price':
|
|
setSliderPercentage(defaultSliderVal)
|
|
resetValue =
|
|
floorToDecimal(
|
|
Number(
|
|
mangoGroup?.getPrice(
|
|
asset.oracleIndex === null
|
|
? mangoGroup.getTokenIndex(
|
|
getTokenBySymbol(mangoConfig, 'USDC').mintKey
|
|
)
|
|
: asset.oracleIndex,
|
|
mangoCache
|
|
)
|
|
),
|
|
6
|
|
) || 0
|
|
break
|
|
case 'perpAvgEntryPrice':
|
|
setSliderPercentage(defaultSliderVal)
|
|
resetValue =
|
|
floorToDecimal(
|
|
Number(
|
|
mangoGroup?.getPrice(
|
|
asset.oracleIndex === null
|
|
? mangoGroup.getTokenIndex(
|
|
getTokenBySymbol(mangoConfig, 'USDC').mintKey
|
|
)
|
|
: asset.oracleIndex,
|
|
mangoCache
|
|
)
|
|
),
|
|
6
|
|
) || 0
|
|
break
|
|
case 'spotNet':
|
|
{
|
|
// Get market configuration data if present
|
|
const spotMarketConfig =
|
|
asset.oracleIndex === null
|
|
? null
|
|
: getMarketByPublicKey(
|
|
mangoConfig,
|
|
mangoGroup.spotMarkets[asset.oracleIndex].spotMarket
|
|
)
|
|
|
|
// Retrieve spot balances if present
|
|
resetDeposit =
|
|
Number(
|
|
mangoAccount && spotMarketConfig
|
|
? mangoAccount.getUiDeposit(
|
|
mangoCache.rootBankCache[
|
|
spotMarketConfig.marketIndex
|
|
],
|
|
mangoGroup,
|
|
spotMarketConfig.marketIndex
|
|
)
|
|
: mangoAccount && asset.symbolName === 'USDC'
|
|
? mangoAccount.getUiDeposit(
|
|
mangoCache.rootBankCache[QUOTE_INDEX],
|
|
mangoGroup,
|
|
QUOTE_INDEX
|
|
)
|
|
: 0
|
|
) || 0
|
|
resetBorrow =
|
|
Number(
|
|
mangoAccount && spotMarketConfig
|
|
? mangoAccount.getUiBorrow(
|
|
mangoCache.rootBankCache[
|
|
spotMarketConfig.marketIndex
|
|
],
|
|
mangoGroup,
|
|
spotMarketConfig.marketIndex
|
|
)
|
|
: mangoAccount && asset.symbolName === 'USDC'
|
|
? mangoAccount.getUiBorrow(
|
|
mangoCache.rootBankCache[QUOTE_INDEX],
|
|
mangoGroup,
|
|
QUOTE_INDEX
|
|
)
|
|
: 0
|
|
) || 0
|
|
resetInOrders = 0
|
|
|
|
if (asset.symbolName === 'USDC' && ordersAsBalance) {
|
|
for (let j = 0; j < mangoGroup.tokens.length; j++) {
|
|
const inOrder =
|
|
j !== QUOTE_INDEX &&
|
|
mangoConfig.spotMarkets[j]?.publicKey &&
|
|
mangoAccount?.spotOpenOrdersAccounts[j]?.quoteTokenTotal
|
|
? mangoAccount.spotOpenOrdersAccounts[j].quoteTokenTotal
|
|
: 0
|
|
resetInOrders += Number(inOrder) / Math.pow(10, 6)
|
|
}
|
|
} else {
|
|
resetInOrders =
|
|
spotMarketConfig &&
|
|
mangoAccount?.spotOpenOrdersAccounts[asset.oracleIndex]
|
|
?.baseTokenTotal
|
|
? Number(
|
|
mangoAccount.spotOpenOrdersAccounts[asset.oracleIndex]
|
|
.baseTokenTotal
|
|
) / Math.pow(10, spotMarketConfig.baseDecimals)
|
|
: 0
|
|
}
|
|
resetValue = floorToDecimal(
|
|
resetDeposit -
|
|
resetBorrow +
|
|
(ordersAsBalance ? resetInOrders : 0),
|
|
spotMarketConfig?.baseDecimals || 6
|
|
)
|
|
}
|
|
break
|
|
case 'perpBasePosition':
|
|
{
|
|
// Get market configuration data
|
|
const symbol = asset.symbolName
|
|
const perpMarketConfig =
|
|
asset.oracleIndex === null
|
|
? null
|
|
: getMarketByPublicKey(
|
|
mangoConfig,
|
|
mangoGroup.perpMarkets[asset.oracleIndex].perpMarket
|
|
)
|
|
|
|
// Retrieve perp positions if present
|
|
const perpPosition =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? mangoAccount?.perpAccounts[asset.oracleIndex]
|
|
: null
|
|
const perpMarketIndex =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? getMarketIndexBySymbol(mangoConfig, symbol)
|
|
: null
|
|
const perpAccount =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? mangoAccount?.perpAccounts[perpMarketIndex]
|
|
: null
|
|
const perpMarketCache =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? mangoCache?.perpMarketCache[perpMarketIndex]
|
|
: null
|
|
const perpMarketInfo =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? mangoGroup?.perpMarkets[perpMarketIndex]
|
|
: null
|
|
const basePosition =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? Number(perpAccount?.basePosition) /
|
|
Math.pow(10, perpContractPrecision[symbol])
|
|
: 0
|
|
const unsettledFunding =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? (Number(
|
|
perpAccount?.getUnsettledFunding(perpMarketCache)
|
|
) *
|
|
basePosition) /
|
|
Math.pow(10, 6)
|
|
: 0
|
|
const positionPnL =
|
|
perpMarketConfig?.publicKey && mangoAccount
|
|
? Number(
|
|
perpAccount?.getPnl(
|
|
perpMarketInfo,
|
|
perpMarketCache,
|
|
mangoCache.priceCache[perpMarketIndex].price
|
|
)
|
|
) / Math.pow(10, 6)
|
|
: 0
|
|
const perpInOrders = perpMarketConfig?.publicKey
|
|
? Number(perpPosition?.bidsQuantity) >
|
|
Math.abs(Number(perpPosition?.asksQuantity))
|
|
? floorToDecimal(
|
|
Number(perpPosition?.bidsQuantity),
|
|
tokenPrecision[perpMarketConfig?.baseSymbol] || 6
|
|
)
|
|
: floorToDecimal(
|
|
-1 * Number(perpPosition?.asksQuantity),
|
|
tokenPrecision[perpMarketConfig?.baseSymbol] || 6
|
|
)
|
|
: 0
|
|
|
|
resetValue = basePosition
|
|
resetPositionSide = resetValue < 0 ? 'short' : 'long'
|
|
resetPerpPositionPnL = positionPnL
|
|
resetPerpUnsettledFunding = unsettledFunding
|
|
resetPerpInOrders = perpInOrders
|
|
}
|
|
break
|
|
}
|
|
|
|
if (column === 'spotNet') {
|
|
return {
|
|
...asset,
|
|
[column]: resetValue,
|
|
['spotDeposit']: resetDeposit,
|
|
['spotBorrow']: resetBorrow,
|
|
['spotInOrders']: resetInOrders,
|
|
}
|
|
} else if (column === 'perpBasePosition') {
|
|
return {
|
|
...asset,
|
|
[column]: resetValue,
|
|
['perpPositionSide']: resetPositionSide,
|
|
['perpPositionPnL']: resetPerpPositionPnL,
|
|
['perpUnsettledFunding']: resetPerpUnsettledFunding,
|
|
['perpInOrders']: resetPerpInOrders,
|
|
}
|
|
} else {
|
|
return { ...asset, [column]: resetValue }
|
|
}
|
|
}))
|
|
: (resetRowData = scenarioBars.rowData)
|
|
|
|
const updatedScenarioBarData = updateCalculator(resetRowData)
|
|
setScenarioBars(updatedScenarioBarData)
|
|
}
|
|
|
|
// Update values based on user input
|
|
const updateValue = (symbol, field, val) => {
|
|
const updateValue = Number(val)
|
|
if (!Number.isNaN(val)) {
|
|
const updatedRowData = scenarioBars.rowData.map((asset) => {
|
|
if (asset.symbolName.toLowerCase() === symbol.toLowerCase()) {
|
|
switch (field) {
|
|
case 'spotNet':
|
|
return {
|
|
...asset,
|
|
[field]: updateValue,
|
|
['spotDeposit']: updateValue > 0 ? updateValue : 0,
|
|
['spotBorrow']: updateValue < 0 ? updateValue : 0,
|
|
}
|
|
case 'perpBasePosition':
|
|
return {
|
|
...asset,
|
|
[field]: updateValue,
|
|
['perpPositionSide']: val < 0 ? 'short' : 'long',
|
|
}
|
|
case 'perpAvgEntryPrice':
|
|
return {
|
|
...asset,
|
|
[field]: Math.abs(updateValue),
|
|
}
|
|
case 'price':
|
|
return {
|
|
...asset,
|
|
[field]: Math.abs(
|
|
sliderPercentage === 0
|
|
? updateValue
|
|
: updateValue / sliderPercentage
|
|
),
|
|
}
|
|
}
|
|
} else {
|
|
return asset
|
|
}
|
|
})
|
|
|
|
const calcData = updateCalculator(updatedRowData)
|
|
setScenarioBars(calcData)
|
|
}
|
|
}
|
|
|
|
// Anchor current displayed prices to zero
|
|
const anchorPricing = () => {
|
|
const updatedRowData = scenarioBars.rowData.map((asset) => {
|
|
return {
|
|
...asset,
|
|
['price']:
|
|
Math.abs(asset.price) * (asset.priceDisabled ? 1 : sliderPercentage),
|
|
}
|
|
})
|
|
|
|
const calcData = updateCalculator(updatedRowData)
|
|
setScenarioBars(calcData)
|
|
}
|
|
|
|
// Handle slider usage
|
|
const onChangeSlider = async (percentage) => {
|
|
setSliderPercentage(percentage)
|
|
}
|
|
|
|
// Calculate scenario health for display
|
|
function getScenarioDetails() {
|
|
const scenarioHashMap = new Map()
|
|
|
|
// Standard scenario variables
|
|
let assets = 0
|
|
let liabilities = 0
|
|
let initAssets = 0
|
|
let maintAssets = 0
|
|
let initLiabilities = 0
|
|
let maintLiabilities = 0
|
|
let percentToLiquidation = 0
|
|
let percentToLiquidationAbsolute = 0
|
|
|
|
// Detailed health scenario variables
|
|
let equity = 0
|
|
let leverage = 0
|
|
let initHealth = 1
|
|
let maintHealth = 1
|
|
let riskRanking = 'Not Set'
|
|
|
|
if (scenarioBars) {
|
|
// Return scenario health
|
|
const scenarioDetails = getHealthComponents(sliderPercentage)
|
|
|
|
assets = scenarioDetails['assets']
|
|
liabilities = scenarioDetails['liabilities']
|
|
initAssets = scenarioDetails['initAssets']
|
|
maintAssets = scenarioDetails['maintAssets']
|
|
initLiabilities = scenarioDetails['initLiabilities']
|
|
maintLiabilities = scenarioDetails['maintLiabilities']
|
|
|
|
equity = assets - liabilities
|
|
if (equity > 0 && liabilities != 0) {
|
|
leverage = Math.abs(liabilities / equity)
|
|
}
|
|
|
|
// Calculate health ratios and risk ranking
|
|
if (liabilities > 0) {
|
|
initHealth = initAssets / initLiabilities - 1
|
|
maintHealth = maintAssets / maintLiabilities - 1
|
|
}
|
|
|
|
riskRanking =
|
|
maintHealth > 0.4
|
|
? riskRanks[0]
|
|
: maintHealth > 0.3
|
|
? riskRanks[1]
|
|
: initHealth > 0
|
|
? riskRanks[2]
|
|
: maintHealth > 0
|
|
? riskRanks[3]
|
|
: riskRanks[4]
|
|
|
|
// Calculate percent to liquidation
|
|
const scenarioBaseLine = getHealthComponents(1)
|
|
const scenarioBaseChange = getHealthComponents(1.01)
|
|
const maintEquity =
|
|
scenarioBaseLine['maintAssets'] - scenarioBaseLine['maintLiabilities']
|
|
const maintAssetsRateOfChange =
|
|
scenarioBaseChange['maintAssets'] - scenarioBaseLine['maintAssets']
|
|
const maintLiabsRateOfChange =
|
|
scenarioBaseChange['maintLiabilities'] -
|
|
scenarioBaseLine['maintLiabilities']
|
|
const maintRateOfChange = maintLiabsRateOfChange - maintAssetsRateOfChange
|
|
percentToLiquidation =
|
|
maintHealth > 0
|
|
? roundToDecimal(
|
|
100 + maintEquity / maintRateOfChange - sliderPercentage * 100,
|
|
1
|
|
)
|
|
: 0
|
|
percentToLiquidationAbsolute =
|
|
maintHealth > 0
|
|
? roundToDecimal(1 / (sliderPercentage / percentToLiquidation), 1)
|
|
: 0
|
|
|
|
if (sliderPercentage * 100 + percentToLiquidation < 0) {
|
|
percentToLiquidation = -9999
|
|
percentToLiquidationAbsolute = -9999
|
|
}
|
|
}
|
|
|
|
// Add scenario details for display
|
|
scenarioHashMap.set('assets', assets)
|
|
scenarioHashMap.set('liabilities', liabilities)
|
|
scenarioHashMap.set('equity', equity)
|
|
scenarioHashMap.set('leverage', leverage)
|
|
scenarioHashMap.set('initWeightAssets', initAssets)
|
|
scenarioHashMap.set('initWeightLiabilities', initLiabilities)
|
|
scenarioHashMap.set('maintWeightAssets', maintAssets)
|
|
scenarioHashMap.set('maintWeightLiabilities', maintLiabilities)
|
|
scenarioHashMap.set('initHealth', initHealth)
|
|
scenarioHashMap.set('maintHealth', maintHealth)
|
|
scenarioHashMap.set('riskRanking', riskRanking)
|
|
scenarioHashMap.set(
|
|
'percentToLiquidation',
|
|
Number.isFinite(percentToLiquidation)
|
|
? percentToLiquidation === -9999
|
|
? 'N/A'
|
|
: percentToLiquidation
|
|
: 'N/A'
|
|
)
|
|
scenarioHashMap.set(
|
|
'percentToLiquidationAbsolute',
|
|
Number.isFinite(percentToLiquidationAbsolute)
|
|
? percentToLiquidationAbsolute === -9999
|
|
? 'N/A'
|
|
: percentToLiquidationAbsolute
|
|
: 'N/A'
|
|
)
|
|
|
|
return scenarioHashMap
|
|
}
|
|
|
|
// Calculate health components
|
|
function getHealthComponents(modPrice) {
|
|
// Standard scenario variables
|
|
let assets = 0
|
|
let liabilities = 0
|
|
let initAssets = 0
|
|
let maintAssets = 0
|
|
let initLiabilities = 0
|
|
let maintLiabilities = 0
|
|
|
|
// Spot Assets and Liabilities variables
|
|
let quoteCalc = 0
|
|
let spotAssets = 0
|
|
let spotLiabilities = 0
|
|
|
|
// Perps Assets and Liabilities variables
|
|
let perpsAssets = 0
|
|
let perpsLiabilities = 0
|
|
|
|
scenarioBars.rowData.map((asset) => {
|
|
// SPOT
|
|
// Calculate spot quote
|
|
if (asset.symbolName === 'USDC' && Number(asset.spotNet) > 0) {
|
|
quoteCalc += asset.spotNet
|
|
} else if (asset.symbolName === 'USDC' && asset.spotNet < 0) {
|
|
quoteCalc -= Math.abs(asset.spotNet)
|
|
}
|
|
|
|
let spotQuote =
|
|
asset.spotNet * asset.price * (asset.priceDisabled ? 1 : modPrice)
|
|
|
|
// Handle spot orders if not to be included as balance
|
|
if (
|
|
!ordersAsBalance &&
|
|
asset.symbolName !== 'USDC' &&
|
|
asset.spotInOrders != 0
|
|
) {
|
|
const spotBidsBaseNet =
|
|
asset.spotNet +
|
|
(asset.price > 0 ? asset.spotQuoteTokenLocked / asset.price : 0) +
|
|
asset.spotBaseTokenFree +
|
|
asset.spotBaseTokenLocked
|
|
const spotAsksBaseNet = asset.spotNet + asset.spotBaseTokenFree
|
|
if (spotBidsBaseNet > Math.abs(spotAsksBaseNet)) {
|
|
spotQuote =
|
|
spotBidsBaseNet * asset.price * (asset.priceDisabled ? 1 : modPrice)
|
|
quoteCalc += asset.spotQuoteTokenFree
|
|
} else {
|
|
spotQuote =
|
|
spotAsksBaseNet * asset.price * (asset.priceDisabled ? 1 : modPrice)
|
|
quoteCalc +=
|
|
(asset.price > 0 ? asset.spotBaseTokenLocked * asset.price : 0) +
|
|
asset.spotQuoteTokenFree +
|
|
asset.spotQuoteTokenLocked
|
|
}
|
|
}
|
|
|
|
// Calculate spot assets
|
|
spotAssets += spotQuote > 0 ? spotQuote : 0
|
|
initAssets +=
|
|
spotQuote > 0 && asset.symbolName !== 'USDC'
|
|
? spotQuote * asset.initAssetWeightSpot
|
|
: 0
|
|
maintAssets +=
|
|
spotQuote > 0 && asset.symbolName !== 'USDC'
|
|
? spotQuote * asset.maintAssetWeightSpot
|
|
: 0
|
|
|
|
// Calculate spot liabilities
|
|
spotLiabilities += spotQuote < 0 ? Math.abs(spotQuote) : 0
|
|
initLiabilities +=
|
|
spotQuote <= 0 && asset.symbolName !== 'USDC'
|
|
? Math.abs(spotQuote) * asset.initLiabWeightSpot
|
|
: 0
|
|
maintLiabilities +=
|
|
spotQuote <= 0 && asset.symbolName !== 'USDC'
|
|
? Math.abs(spotQuote) * asset.maintLiabWeightSpot
|
|
: 0
|
|
|
|
// PERPS
|
|
// Get simple perp asset and liability value
|
|
let assetVal = 0
|
|
let liabVal = 0
|
|
const perpBasPosValSimple =
|
|
asset.perpBasePosition * (asset.price * modPrice)
|
|
liabVal = asset.perpBasePosition < 0 ? perpBasPosValSimple : 0
|
|
assetVal = asset.perpBasePosition > 0 ? perpBasPosValSimple : 0
|
|
|
|
// Calculate scenario profit and loss
|
|
const scenarioPnL =
|
|
asset.perpBasePosition > 0
|
|
? asset.perpBasePosition *
|
|
(asset.price * modPrice - asset.perpAvgEntryPrice)
|
|
: Math.abs(asset.perpBasePosition) *
|
|
(asset.perpAvgEntryPrice - asset.price * modPrice)
|
|
|
|
// Get base and quote position values
|
|
let basPosVal = asset.perpBasePosition * (asset.price * modPrice)
|
|
let perpQuotePos = -1 * basPosVal + asset.perpPositionPnL + scenarioPnL
|
|
|
|
// Handle perp orders if not to be cancelled
|
|
if (
|
|
!ordersAsBalance &&
|
|
asset.symbolName !== 'USDC' &&
|
|
asset.perpInOrders != 0
|
|
) {
|
|
const perpBidsBaseNet = asset.perpBasePosition + asset.perpBids
|
|
const perpAsksBaseNet = asset.perpBasePosition - asset.perpAsks
|
|
if (!ordersAsBalance && perpBidsBaseNet > Math.abs(perpAsksBaseNet)) {
|
|
perpQuotePos =
|
|
-1 * basPosVal +
|
|
asset.perpPositionPnL +
|
|
scenarioPnL -
|
|
asset.perpBids * asset.price
|
|
basPosVal = perpBidsBaseNet * (asset.price * modPrice)
|
|
} else {
|
|
perpQuotePos =
|
|
-1 * basPosVal +
|
|
asset.perpPositionPnL +
|
|
scenarioPnL +
|
|
asset.perpAsks * asset.price
|
|
basPosVal = perpAsksBaseNet * (asset.price * modPrice)
|
|
}
|
|
}
|
|
|
|
// Adjust for PnL and unsettledFunding, then add to assets or liabilities
|
|
const realQuotePosition =
|
|
-1 * asset.perpBasePosition * (asset.price * modPrice) +
|
|
asset.perpPositionPnL +
|
|
scenarioPnL -
|
|
asset.perpUnsettledFunding
|
|
if (realQuotePosition < 0) {
|
|
liabVal = Math.abs(liabVal + realQuotePosition)
|
|
} else if (realQuotePosition > 0) {
|
|
assetVal = Math.abs(assetVal + realQuotePosition)
|
|
}
|
|
liabVal = Math.abs(liabVal)
|
|
assetVal = Math.abs(assetVal)
|
|
perpsAssets += assetVal
|
|
perpsLiabilities += liabVal
|
|
|
|
// Assign to quote, assets and liabilities
|
|
quoteCalc += perpQuotePos
|
|
initAssets += basPosVal > 0 ? basPosVal * asset.initAssetWeightPerp : 0
|
|
initLiabilities +=
|
|
basPosVal < 0 ? Math.abs(basPosVal) * asset.initLiabWeightPerp : 0
|
|
maintAssets += basPosVal > 0 ? basPosVal * asset.maintAssetWeightPerp : 0
|
|
maintLiabilities +=
|
|
basPosVal < 0 ? Math.abs(basPosVal) * asset.maintLiabWeightPerp : 0
|
|
})
|
|
|
|
// Calculate basic scenario details
|
|
assets = spotAssets + perpsAssets
|
|
liabilities = spotLiabilities + perpsLiabilities
|
|
|
|
// Calculate weighted assets and liabilities
|
|
initAssets += quoteCalc > 0 ? quoteCalc : 0
|
|
maintAssets += quoteCalc > 0 ? quoteCalc : 0
|
|
initLiabilities += quoteCalc <= 0 ? Math.abs(quoteCalc) : 0
|
|
maintLiabilities += quoteCalc <= 0 ? Math.abs(quoteCalc) : 0
|
|
|
|
return {
|
|
assets: assets,
|
|
liabilities: liabilities,
|
|
initAssets: initAssets,
|
|
maintAssets: maintAssets,
|
|
initLiabilities: initLiabilities,
|
|
maintLiabilities: maintLiabilities,
|
|
}
|
|
}
|
|
|
|
// Calculate single asset liquidation prices
|
|
function getLiquidationPrices() {
|
|
const liquidationHashMap = new Map()
|
|
|
|
if (scenarioBars) {
|
|
scenarioBars.rowData.map((assetToTest) => {
|
|
let liqPrice = 0
|
|
if (assetToTest.symbolName !== 'USDC') {
|
|
let quoteCalc = 0
|
|
let weightedAsset = 0
|
|
let partialHealth = 0
|
|
|
|
// Calculate quote
|
|
scenarioBars.rowData.map((asset) => {
|
|
if (asset.symbolName === 'USDC' && Number(asset.spotNet) > 0) {
|
|
quoteCalc += asset.spotNet
|
|
} else if (asset.symbolName === 'USDC' && asset.spotNet < 0) {
|
|
quoteCalc -= Math.abs(asset.spotNet)
|
|
}
|
|
|
|
const scenarioPnL =
|
|
asset.perpBasePosition > 0
|
|
? asset.perpBasePosition *
|
|
(asset.price * sliderPercentage - asset.perpAvgEntryPrice)
|
|
: Math.abs(asset.perpBasePosition) *
|
|
(asset.perpAvgEntryPrice - asset.price * sliderPercentage)
|
|
const basPosVal =
|
|
asset.perpBasePosition * (asset.price * sliderPercentage)
|
|
const perpQuotePos =
|
|
-1 * basPosVal + asset.perpPositionPnL + scenarioPnL
|
|
quoteCalc += perpQuotePos
|
|
})
|
|
|
|
// Calculate weighted asset and partial health to draw from
|
|
partialHealth = quoteCalc
|
|
scenarioBars.rowData.map((asset) => {
|
|
if (asset.symbolName !== 'USDC') {
|
|
if (asset.symbolName === assetToTest.symbolName) {
|
|
const weightedSpot =
|
|
asset.spotNet *
|
|
(asset.spotNet > 0
|
|
? asset.maintAssetWeightSpot
|
|
: asset.maintLiabWeightSpot)
|
|
const weightedPerp =
|
|
asset.perpBasePosition *
|
|
(asset.perpBasePosition > 0
|
|
? asset.maintAssetWeightPerp
|
|
: asset.maintLiabWeightPerp)
|
|
weightedAsset += (weightedSpot + weightedPerp) * -1
|
|
} else {
|
|
const spotHealth =
|
|
asset.spotNet *
|
|
asset.price *
|
|
(asset.priceDisabled ? 1 : sliderPercentage) *
|
|
(asset.spotNet > 0
|
|
? asset.maintAssetWeightSpot
|
|
: asset.maintLiabWeightSpot)
|
|
const perpHealth =
|
|
asset.perpBasePosition *
|
|
asset.price *
|
|
(asset.priceDisabled ? 1 : sliderPercentage) *
|
|
(asset.perpBasePosition > 0
|
|
? asset.maintAssetWeightPerp
|
|
: asset.maintLiabWeightPerp)
|
|
partialHealth += spotHealth + perpHealth
|
|
}
|
|
}
|
|
})
|
|
|
|
// Calculate liquidation price
|
|
if (weightedAsset === 0) {
|
|
liqPrice = 0
|
|
} else {
|
|
liqPrice = partialHealth / weightedAsset
|
|
if (liqPrice < 0) {
|
|
liqPrice = 0
|
|
}
|
|
}
|
|
|
|
liquidationHashMap.set(assetToTest.symbolName.toString(), liqPrice)
|
|
} else {
|
|
liquidationHashMap.set(assetToTest.symbolName.toString(), liqPrice)
|
|
}
|
|
})
|
|
}
|
|
|
|
return liquidationHashMap
|
|
}
|
|
|
|
// Get detailed scenario summary and liquidation prices
|
|
const scenarioDetails = getScenarioDetails()
|
|
const liquidationPrices = getLiquidationPrices()
|
|
|
|
// Update focused input and update scanerio if input is valid
|
|
const updateInterimValue = (symbol, field, type, identifier, val) => {
|
|
const interimVal = val
|
|
const interimIdentifier = identifier
|
|
|
|
switch (type) {
|
|
case 'focus':
|
|
setInterimValue(interimValue.set(interimIdentifier, interimVal))
|
|
break
|
|
case 'change':
|
|
if (Number(val) === 0 || Number.isNaN(val)) {
|
|
setInterimValue(interimValue.set(interimIdentifier, interimVal))
|
|
updateValue(symbol, field, 0)
|
|
} else {
|
|
updateValue(symbol, field, val)
|
|
setInterimValue(interimValue.set(interimIdentifier, val))
|
|
}
|
|
break
|
|
case 'blur':
|
|
if (Number(val) === 0 || Number.isNaN(val)) {
|
|
updateValue(symbol, field, 0)
|
|
interimValue.delete(interimIdentifier)
|
|
setInterimValue(new Map())
|
|
} else {
|
|
updateValue(symbol, field, val)
|
|
interimValue.delete(interimIdentifier)
|
|
setInterimValue(new Map())
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// Display all
|
|
return (
|
|
<div className={`bg-th-bkg-1 text-th-fgd-1 transition-all`}>
|
|
<TopBar />
|
|
<PageBodyContainer>
|
|
<div className="flex flex-col pt-8 pb-3 sm:pb-6 md:pt-10">
|
|
<h1 className={`mb-2`}>{t('calculator:risk-calculator')}</h1>
|
|
<p className="mb-0">{t('calculator:in-testing-warning')}</p>
|
|
</div>
|
|
{scenarioBars?.rowData.length > 0 ? (
|
|
<div className="rounded-lg bg-th-bkg-2">
|
|
<div className="grid grid-cols-12">
|
|
<div className="col-span-12 p-4 md:col-span-8">
|
|
<div className="flex justify-between px-0 pb-2 lg:px-3 lg:pb-3">
|
|
<h2 className="mb-4 lg:mb-0">
|
|
{t('calculator:scenario-balances')}
|
|
</h2>
|
|
<div className="flex justify-between lg:justify-start">
|
|
<Button
|
|
className={`flex h-8 items-center justify-center rounded pt-0 pb-0 pl-3 pr-3 text-xs sm:ml-3`}
|
|
onClick={() => {
|
|
setSliderPercentage(defaultSliderVal)
|
|
toggleOrdersAsBalance(false)
|
|
createScenario(accountConnected ? 'account' : 'blank')
|
|
}}
|
|
>
|
|
<div className="flex items-center hover:text-th-primary">
|
|
<RefreshIcon className="mr-1.5 h-5 w-5" />
|
|
{t('reset')}
|
|
</div>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="mb-3 flex h-8 items-center rounded border border-th-fgd-4 bg-th-bkg-1 px-3 lg:mx-3">
|
|
<div className="whitespace-nowrap pr-5 text-xs text-th-fgd-3">
|
|
{t('calculator:edit-all-prices')}
|
|
</div>
|
|
<div className="w-full">
|
|
<Slider
|
|
onChange={(e) => {
|
|
onChangeSlider(e)
|
|
}}
|
|
step={0.01}
|
|
value={sliderPercentage}
|
|
min={0}
|
|
max={3.5}
|
|
defaultValue={defaultSliderVal}
|
|
trackStyle={{ backgroundColor: '#F2C94C' }}
|
|
handleStyle={{
|
|
borderColor: '#F2C94C',
|
|
backgroundColor: '#f7f7f7',
|
|
}}
|
|
railStyle={{ backgroundColor: '#F2C94C' }}
|
|
/>
|
|
</div>
|
|
<div className="w-16 pl-4 text-xs text-th-fgd-1">
|
|
{`${Number((sliderPercentage - 1) * 100).toFixed(0)}%`}
|
|
</div>
|
|
<div className="w-16 pl-4 text-xs text-th-fgd-1 hover:text-th-primary">
|
|
<LinkButton
|
|
onClick={() => setSliderPercentage(defaultSliderVal)}
|
|
>
|
|
{t('reset')}
|
|
</LinkButton>
|
|
</div>
|
|
</div>
|
|
<div className="wrap flex justify-between px-0 pb-2 lg:px-3 lg:pb-3">
|
|
<div className="mb-3 flex h-8 items-center rounded px-3 lg:mx-3">
|
|
<Switch
|
|
checked={showZeroBalances}
|
|
className="text-xs"
|
|
onChange={() => setShowZeroBalances(!showZeroBalances)}
|
|
>
|
|
{t('show-zero')}
|
|
</Switch>
|
|
</div>
|
|
<div className="mb-3 flex h-8 items-center rounded px-3 lg:mx-3">
|
|
<Switch
|
|
checked={ordersAsBalance}
|
|
className="text-xs"
|
|
onChange={() => toggleOrdersAsBalance(!ordersAsBalance)}
|
|
>
|
|
{t('calculator:simulate-orders-cancelled')}
|
|
</Switch>
|
|
</div>
|
|
<div className="flex justify-between lg:justify-start">
|
|
<Tooltip content={t('calculator:tooltip-anchor-slider')}>
|
|
<Button
|
|
className={`flex h-8 items-center justify-center rounded pt-0 pb-0 pl-3 pr-3 text-xs sm:ml-3`}
|
|
onClick={() => {
|
|
anchorPricing()
|
|
setSliderPercentage(defaultSliderVal)
|
|
}}
|
|
>
|
|
<div className="flex items-center hover:text-th-primary">
|
|
<AnchorIcon className="mr-1.5 h-5 w-5" />
|
|
{t('calculator:anchor-slider')}
|
|
</div>
|
|
</Button>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
{/*Hidden panel that displays a short scenario summary on mobile instead of the detailed one*/}
|
|
<div className="sticky mb-3 w-full rounded border border-th-fgd-4 bg-th-bkg-1 md:hidden">
|
|
<Disclosure>
|
|
{({ open }) => (
|
|
<>
|
|
<Disclosure.Button className="default-transition flex w-full items-center justify-between bg-th-bkg-1 p-3 hover:bg-th-bkg-1 focus:outline-none">
|
|
<p className="mb-0">
|
|
{open
|
|
? t('calculator:scenario-details')
|
|
: t('calculator:scenario-maint-health')}
|
|
</p>
|
|
{open ? null : (
|
|
<div className="text-xs text-th-fgd-3">
|
|
{scenarioDetails.get('maintHealth') * 100 >= 9999
|
|
? '>10000'
|
|
: scenarioDetails.get('maintHealth') * 100 < 0
|
|
? '<0'
|
|
: (
|
|
scenarioDetails.get('maintHealth') * 100
|
|
).toFixed(2)}
|
|
%
|
|
</div>
|
|
)}
|
|
<ChevronUpIcon
|
|
className={`default-transition h-4 w-4 text-th-fgd-1 ${
|
|
open
|
|
? 'rotate-360 transform'
|
|
: 'rotate-180 transform'
|
|
}`}
|
|
/>
|
|
</Disclosure.Button>
|
|
<Disclosure.Panel className="p-3">
|
|
<div className="text-xs text-th-fgd-1">
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">
|
|
{t('maint-health')}
|
|
</div>
|
|
{scenarioDetails.get('maintHealth') * 100 >= 9999
|
|
? '>10000'
|
|
: scenarioDetails.get('maintHealth') * 100 < 0
|
|
? '<0'
|
|
: (
|
|
scenarioDetails.get('maintHealth') * 100
|
|
).toFixed(2)}
|
|
%
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">
|
|
{t('init-health')}
|
|
</div>
|
|
{scenarioDetails.get('initHealth') * 100 >= 9999
|
|
? '>10000'
|
|
: scenarioDetails.get('initHealth') * 100 < 0
|
|
? '<0'
|
|
: (
|
|
scenarioDetails.get('initHealth') * 100
|
|
).toFixed(2)}
|
|
%
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">
|
|
{t('calculator:new-positions-openable')}
|
|
</div>
|
|
<div
|
|
className={`font-bold ${
|
|
scenarioDetails.get('initHealth') * 100 >= 0
|
|
? 'text-th-green'
|
|
: 'text-th-red'
|
|
}`}
|
|
>
|
|
{scenarioDetails.get('initHealth') * 100 >= 0
|
|
? t('calculator:yes')
|
|
: t('calculator:no')}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">{t('health')}</div>
|
|
<div className="font-bold">
|
|
{
|
|
<div
|
|
className={`font-bold ${
|
|
scenarioDetails.get('maintHealth') * 100 <
|
|
0
|
|
? 'text-th-red'
|
|
: scenarioDetails.get('riskRanking') ===
|
|
riskRanks[3]
|
|
? 'text-th-red'
|
|
: scenarioDetails.get('riskRanking') ===
|
|
riskRanks[2]
|
|
? 'text-th-orange'
|
|
: scenarioDetails.get('riskRanking') ===
|
|
riskRanks[1]
|
|
? 'text-th-primary'
|
|
: 'text-th-green'
|
|
}`}
|
|
>
|
|
{scenarioDetails.get('maintHealth') * 100 <
|
|
0
|
|
? riskRanks[4]
|
|
: scenarioDetails.get('riskRanking')}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">
|
|
{t('account-value')}
|
|
</div>
|
|
<div className="font-bold">
|
|
{formatUsdValue(
|
|
scenarioDetails.get('equity')
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">
|
|
{t('calculator:percent-move-liquidation')}
|
|
</div>
|
|
<div className="font-bold">
|
|
{scenarioDetails.get(
|
|
'percentToLiquidationAbsolute'
|
|
)}
|
|
%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Disclosure.Panel>
|
|
</>
|
|
)}
|
|
</Disclosure>
|
|
</div>
|
|
{/*Create scenario table for display*/}
|
|
<div className={`flex flex-col pb-2`}>
|
|
<div className={`-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8`}>
|
|
<div
|
|
className={`inline-block min-w-full align-middle sm:px-6 lg:px-8`}
|
|
>
|
|
<Table className="min-w-full divide-y divide-th-bkg-2">
|
|
<Thead>
|
|
<Tr className="text-xs text-th-fgd-3">
|
|
<Th
|
|
scope="col"
|
|
className={`px-1 py-1 text-left font-normal lg:px-3`}
|
|
>
|
|
{t('asset')}
|
|
</Th>
|
|
<Th
|
|
scope="col"
|
|
className={`px-1 py-1 text-left font-normal lg:px-3`}
|
|
>
|
|
<div className="flex justify-start md:justify-between">
|
|
<div className="pr-2">{t('spot')}</div>
|
|
<LinkButton
|
|
onClick={() => resetScenarioColumn('spotNet')}
|
|
>
|
|
{t('reset')}
|
|
</LinkButton>
|
|
</div>
|
|
</Th>
|
|
<Th
|
|
scope="col"
|
|
className={`px-1 py-1 text-left font-normal lg:px-3`}
|
|
>
|
|
<div className="flex justify-start md:justify-between">
|
|
<div className="pr-2">{t('perp')}</div>
|
|
<LinkButton
|
|
onClick={() =>
|
|
resetScenarioColumn('perpBasePosition')
|
|
}
|
|
>
|
|
{t('reset')}
|
|
</LinkButton>
|
|
</div>
|
|
</Th>
|
|
<Th
|
|
scope="col"
|
|
className={`px-1 py-1 text-left font-normal lg:px-3`}
|
|
>
|
|
<div className="flex justify-start md:justify-between">
|
|
<div className="pr-2">
|
|
{t('calculator:perp-entry')}
|
|
</div>
|
|
<LinkButton
|
|
onClick={() =>
|
|
resetScenarioColumn('perpAvgEntryPrice')
|
|
}
|
|
>
|
|
{t('reset')}
|
|
</LinkButton>
|
|
</div>
|
|
</Th>
|
|
<Th
|
|
scope="col"
|
|
className={`px-1 py-1 font-normal lg:px-3`}
|
|
>
|
|
<div className="flex justify-start md:justify-between">
|
|
<div className="pr-2">{t('price')}</div>
|
|
<LinkButton
|
|
onClick={() => resetScenarioColumn('price')}
|
|
>
|
|
{t('reset')}
|
|
</LinkButton>
|
|
</div>
|
|
</Th>
|
|
<Th
|
|
scope="col"
|
|
className={`px-1 py-1 text-left font-normal lg:px-3`}
|
|
>
|
|
<div className="flex justify-start md:justify-between">
|
|
<Tooltip
|
|
content={t('calculator:spot-val-perp-val')}
|
|
>
|
|
<div className="pr-2">{t('value')}</div>
|
|
</Tooltip>
|
|
</div>
|
|
</Th>
|
|
<Th
|
|
scope="col"
|
|
className={`px-1 py-1 text-left font-normal lg:px-3`}
|
|
>
|
|
<div className="flex justify-start md:justify-between">
|
|
<Tooltip
|
|
content={t('calculator:single-asset-liq')}
|
|
>
|
|
<div className="pr-2">
|
|
{t('calculator:liq-price')}
|
|
</div>
|
|
</Tooltip>
|
|
</div>
|
|
</Th>
|
|
</Tr>
|
|
</Thead>
|
|
<Tbody>
|
|
{/*Populate scenario table with data*/}
|
|
{scenarioBars.rowData.map((asset, i) =>
|
|
asset.symbolName === 'USDC' ||
|
|
(asset.spotNet != 0 && asset.hasMarketSpot) ||
|
|
(asset.perpBasePosition != 0 &&
|
|
asset.hasMarketPerp) ||
|
|
showZeroBalances ? (
|
|
<Tr
|
|
className={`${
|
|
i % 2 === 0
|
|
? `bg-th-bkg-3 md:bg-th-bkg-2`
|
|
: `bg-th-bkg-2`
|
|
}`}
|
|
key={`${i}`}
|
|
>
|
|
<Td
|
|
className={`w-24 whitespace-nowrap px-3 py-2 text-sm text-th-fgd-1`}
|
|
>
|
|
<div className="flex items-center">
|
|
<img
|
|
alt=""
|
|
width="20"
|
|
height="20"
|
|
src={`/assets/icons/${asset.symbolName.toLowerCase()}.svg`}
|
|
className={`mr-2.5`}
|
|
/>
|
|
<div>{asset.symbolName}</div>
|
|
</div>
|
|
</Td>
|
|
<Td
|
|
className={`px-1 py-2 text-sm text-th-fgd-1 lg:px-3`}
|
|
>
|
|
<Input
|
|
id={'spotNet_' + i}
|
|
type="number"
|
|
onFocus={(e) =>
|
|
e.target.id === e.currentTarget.id
|
|
? updateInterimValue(
|
|
asset.symbolName,
|
|
'spotNet',
|
|
'focus',
|
|
('spotNet_' + i).toString(),
|
|
asset.spotNet !== 0
|
|
? asset.spotNet
|
|
: ''
|
|
)
|
|
: null
|
|
}
|
|
onChange={(e) =>
|
|
updateInterimValue(
|
|
asset.symbolName,
|
|
'spotNet',
|
|
'change',
|
|
('spotNet_' + i).toString(),
|
|
e.target.value
|
|
)
|
|
}
|
|
onBlur={(e) =>
|
|
updateInterimValue(
|
|
asset.symbolName,
|
|
'spotNet',
|
|
'blur',
|
|
('spotNet_' + i).toString(),
|
|
e.target.value
|
|
)
|
|
}
|
|
placeholder={'0.0'}
|
|
value={
|
|
interimValue.has(
|
|
('spotNet_' + i).toString()
|
|
)
|
|
? interimValue.get(
|
|
('spotNet_' + i).toString()
|
|
)
|
|
: asset.spotNet !== 0
|
|
? asset.spotNet
|
|
: ''
|
|
}
|
|
disabled={
|
|
asset.hasMarketSpot ||
|
|
asset.symbolName === 'USDC'
|
|
? false
|
|
: true
|
|
}
|
|
/>
|
|
</Td>
|
|
<Td
|
|
className={`px-1 py-2 text-sm text-th-fgd-1 lg:px-3`}
|
|
>
|
|
<Input
|
|
id={'perpBasePosition_' + i}
|
|
type="number"
|
|
onFocus={(e) =>
|
|
e.target.id === e.currentTarget.id
|
|
? updateInterimValue(
|
|
asset.symbolName,
|
|
'perpBasePosition',
|
|
'focus',
|
|
(
|
|
'perpBasePosition_' + i
|
|
).toString(),
|
|
asset.spotNet !== 0
|
|
? asset.spotNet
|
|
: ''
|
|
)
|
|
: null
|
|
}
|
|
onChange={(e) =>
|
|
updateInterimValue(
|
|
asset.symbolName,
|
|
'perpBasePosition',
|
|
'change',
|
|
('perpBasePosition_' + i).toString(),
|
|
e.target.value
|
|
)
|
|
}
|
|
onBlur={(e) =>
|
|
updateInterimValue(
|
|
asset.symbolName,
|
|
'perpBasePosition',
|
|
'blur',
|
|
('perpBasePosition_' + i).toString(),
|
|
e.target.value
|
|
)
|
|
}
|
|
placeholder={'0.0'}
|
|
value={
|
|
interimValue.has(
|
|
('perpBasePosition_' + i).toString()
|
|
)
|
|
? interimValue.get(
|
|
('perpBasePosition_' + i).toString()
|
|
)
|
|
: asset.perpBasePosition !== 0
|
|
? asset.perpBasePosition
|
|
: ''
|
|
}
|
|
disabled={
|
|
asset.hasMarketPerp ? false : true
|
|
}
|
|
/>
|
|
</Td>
|
|
<Td
|
|
className={`px-1 py-2 text-sm text-th-fgd-1 lg:px-3`}
|
|
>
|
|
<Input
|
|
id={'perpAvgEntryPrice_' + i}
|
|
type="number"
|
|
onFocus={(e) =>
|
|
e.target.id === e.currentTarget.id
|
|
? updateInterimValue(
|
|
asset.symbolName,
|
|
'perpAvgEntryPrice',
|
|
'focus',
|
|
(
|
|
'perpAvgEntryPrice_' + i
|
|
).toString(),
|
|
asset.perpAvgEntryPrice !== 0
|
|
? asset.perpAvgEntryPrice
|
|
: ''
|
|
)
|
|
: null
|
|
}
|
|
onChange={(e) =>
|
|
updateInterimValue(
|
|
asset.symbolName,
|
|
'perpAvgEntryPrice',
|
|
'change',
|
|
('perpAvgEntryPrice_' + i).toString(),
|
|
e.target.value
|
|
)
|
|
}
|
|
onBlur={(e) =>
|
|
updateInterimValue(
|
|
asset.symbolName,
|
|
'perpAvgEntryPrice',
|
|
'blur',
|
|
('perpAvgEntryPrice_' + i).toString(),
|
|
e.target.value
|
|
)
|
|
}
|
|
placeholder={'0.0'}
|
|
value={
|
|
interimValue.has(
|
|
('perpAvgEntryPrice_' + i).toString()
|
|
)
|
|
? interimValue.get(
|
|
(
|
|
'perpAvgEntryPrice_' + i
|
|
).toString()
|
|
)
|
|
: asset.perpAvgEntryPrice !== 0
|
|
? asset.perpAvgEntryPrice
|
|
: ''
|
|
}
|
|
disabled={
|
|
asset.hasMarketPerp ? false : true
|
|
}
|
|
/>
|
|
</Td>
|
|
<Td
|
|
className={`whitespace-nowrap px-1 py-2 text-sm text-th-fgd-1 lg:px-3`}
|
|
>
|
|
<Input
|
|
id={'price_' + i}
|
|
type="number"
|
|
onFocus={(e) =>
|
|
e.target.id === e.currentTarget.id
|
|
? updateInterimValue(
|
|
asset.symbolName,
|
|
'price',
|
|
'focus',
|
|
('price_' + i).toString(),
|
|
asset.perpAvgEntryPrice !== 0
|
|
? asset.perpAvgEntryPrice
|
|
: ''
|
|
)
|
|
: null
|
|
}
|
|
onChange={(e) => {
|
|
updateInterimValue(
|
|
asset.symbolName,
|
|
'price',
|
|
'change',
|
|
('price_' + i).toString(),
|
|
e.target.value
|
|
)
|
|
}}
|
|
onBlur={(e) =>
|
|
updateInterimValue(
|
|
asset.symbolName,
|
|
'price',
|
|
'blur',
|
|
('price_' + i).toString(),
|
|
e.target.value
|
|
)
|
|
}
|
|
placeholder={'0.0'}
|
|
value={
|
|
asset.priceDisabled
|
|
? interimValue.has(
|
|
('price_' + i).toString()
|
|
)
|
|
? interimValue.get(
|
|
('price_' + i).toString()
|
|
)
|
|
: asset.price !== 0
|
|
? asset.price
|
|
: ''
|
|
: interimValue.has(
|
|
('price_' + i).toString()
|
|
)
|
|
? interimValue.get(
|
|
('price_' + i).toString()
|
|
)
|
|
: asset.price !== 0
|
|
? asset.price * sliderPercentage
|
|
: ''
|
|
}
|
|
disabled={asset.priceDisabled}
|
|
/>
|
|
</Td>
|
|
<Td
|
|
className={`whitespace-nowrap px-1 py-2 text-sm text-th-fgd-1 lg:px-3`}
|
|
>
|
|
<Input
|
|
type="text"
|
|
value={usdFormatter(
|
|
Number(
|
|
asset.spotNet *
|
|
asset.price *
|
|
(asset.priceDisabled
|
|
? 1
|
|
: sliderPercentage)
|
|
) +
|
|
Number(
|
|
asset.perpPositionSide === 'long'
|
|
? asset.perpPositionPnL +
|
|
asset.perpBasePosition *
|
|
(asset.price *
|
|
sliderPercentage -
|
|
asset.perpAvgEntryPrice)
|
|
: asset.perpPositionPnL -
|
|
asset.perpBasePosition *
|
|
(asset.perpAvgEntryPrice -
|
|
asset.price *
|
|
sliderPercentage)
|
|
)
|
|
)}
|
|
onChange={null}
|
|
disabled
|
|
/>
|
|
</Td>
|
|
<Td
|
|
className={`whitespace-nowrap px-1 py-2 text-sm text-th-fgd-1 lg:px-3`}
|
|
>
|
|
<Input
|
|
type="text"
|
|
value={usdFormatter(
|
|
liquidationPrices.get(asset.symbolName)
|
|
)}
|
|
onChange={null}
|
|
disabled
|
|
/>
|
|
</Td>
|
|
</Tr>
|
|
) : null
|
|
)}
|
|
</Tbody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/*Populate detailed scenario summary*/}
|
|
{scenarioBars?.rowData.length > 0 ? (
|
|
<div className="relative col-span-4 hidden rounded-r-lg bg-th-bkg-3 p-4 md:block">
|
|
<h2 className="mb-4">{t('calculator:scenario-details')}</h2>
|
|
{/* Joke Wrapper */}
|
|
<div className="relative col-span-4">
|
|
{scenarioDetails.get('liabilities') === 0 &&
|
|
scenarioDetails.get('equity') === 0 ? (
|
|
<div className="mb-6 flex flex-col items-center rounded border border-th-green-dark bg-th-green-dark p-3 text-center text-th-fgd-1">
|
|
<div className="pb-0.5 text-th-fgd-1">
|
|
{t('calculator:joke-get-party-started')}
|
|
</div>
|
|
<div className="text-xs text-th-fgd-1">
|
|
{t('calculator:joke-mangoes-are-ripe')}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
{scenarioDetails.get('liabilities') === 0 &&
|
|
scenarioDetails.get('equity') > 0 ? (
|
|
<div className="mb-6 flex flex-col items-center rounded border border-th-green p-3 text-center text-th-fgd-1">
|
|
<div className="pb-0.5 text-th-fgd-1">
|
|
{t('calculator:joke-zero-borrows-risk')}
|
|
</div>
|
|
<div className="text-xs text-th-fgd-3">
|
|
{t('calculator:joke-live-a-little')}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
{scenarioDetails.get('riskRanking') === riskRanks[0] &&
|
|
scenarioDetails.get('leverage') !== 0 ? (
|
|
<div className="mb-6 flex flex-col items-center rounded border border-th-green p-3 text-center text-th-fgd-1">
|
|
<div className="pb-0.5 text-th-fgd-1">
|
|
{t('calculator:joke-looking-good')}
|
|
</div>
|
|
<div className="text-xs text-th-fgd-3">
|
|
{t('calculator:joke-sun-shining')}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
{scenarioDetails.get('riskRanking') === riskRanks[1] ? (
|
|
<div className="mb-6 flex flex-col items-center rounded border border-th-orange p-3 text-center text-th-fgd-1">
|
|
<div className="pb-0.5 text-th-fgd-1">
|
|
{t('calculator:joke-liquidator-activity')}
|
|
</div>
|
|
<div className="text-xs text-th-fgd-3">
|
|
{t('calculator:joke-rethink-positions')}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
{scenarioDetails.get('riskRanking') === riskRanks[2] ? (
|
|
<div className="mb-6 flex flex-col items-center rounded border border-th-red p-3 text-center text-th-fgd-1">
|
|
<div className="pb-0.5 text-th-fgd-1">
|
|
{t('calculator:joke-liquidators-closing')}
|
|
</div>
|
|
<div className="text-xs text-th-fgd-3">
|
|
{t('calculator:joke-hit-em-with')}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
{scenarioDetails.get('riskRanking') === riskRanks[3] ? (
|
|
<div className="mb-6 flex flex-col items-center rounded border border-th-red p-3 text-center text-th-fgd-1">
|
|
<div className="pb-0.5 text-th-fgd-1">
|
|
{t('calculator:joke-liquidators-spotted-you')}
|
|
</div>
|
|
<div className="text-xs text-th-fgd-3">
|
|
{t('calculator:joke-throw-some-money')}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
{scenarioDetails.get('riskRanking') === riskRanks[4] ? (
|
|
<div className="mb-6 flex flex-col items-center rounded border border-th-red bg-th-red p-3 text-center text-th-fgd-1">
|
|
<div className="pb-0.5 text-th-fgd-1">
|
|
{t('calculator:joke-liquidated')}
|
|
</div>
|
|
<div className="text-xs text-th-fgd-1">
|
|
{t('calculator:joke-insert-coin')}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<Tooltip content={t('calculator:tooltip-maint-health')}>
|
|
<div className="text-th-fgd-3">
|
|
{t('calculator:maintenance-health')}
|
|
</div>
|
|
</Tooltip>
|
|
<div className="font-bold">
|
|
{scenarioDetails.get('maintHealth') * 100 >= 9999
|
|
? '>10000'
|
|
: scenarioDetails.get('maintHealth') * 100 < 0
|
|
? '<0'
|
|
: (scenarioDetails.get('maintHealth') * 100).toFixed(2)}
|
|
%
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<Tooltip content={t('calculator:tooltip-init-health')}>
|
|
<div className="text-th-fgd-3">
|
|
{t('calculator:initial-health')}
|
|
</div>
|
|
</Tooltip>
|
|
<div className="font-bold">
|
|
{scenarioDetails.get('initHealth') * 100 >= 9999
|
|
? '>10000'
|
|
: scenarioDetails.get('initHealth') * 100 < 0
|
|
? '<0'
|
|
: (scenarioDetails.get('initHealth') * 100).toFixed(2)}
|
|
%
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">
|
|
{t('calculator:new-positions-openable')}
|
|
</div>
|
|
<div
|
|
className={`font-bold ${
|
|
scenarioDetails.get('initHealth') * 100 >= 0
|
|
? 'text-th-green'
|
|
: 'text-th-red'
|
|
}`}
|
|
>
|
|
{scenarioDetails.get('initHealth') * 100 >= 0
|
|
? t('calculator:yes')
|
|
: t('calculator:no')}
|
|
</div>
|
|
</div>
|
|
<div className="mb-6 flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">{t('account-health')}</div>
|
|
{
|
|
<div
|
|
className={`font-bold ${
|
|
scenarioDetails.get('maintHealth') * 100 < 0
|
|
? 'text-th-red'
|
|
: scenarioDetails.get('riskRanking') ===
|
|
riskRanks[3]
|
|
? 'text-th-red'
|
|
: scenarioDetails.get('riskRanking') ===
|
|
riskRanks[2]
|
|
? 'text-th-orange'
|
|
: scenarioDetails.get('riskRanking') ===
|
|
riskRanks[1]
|
|
? 'text-th-primary'
|
|
: 'text-th-green'
|
|
}`}
|
|
>
|
|
{scenarioDetails.get('maintHealth') * 100 < 0
|
|
? riskRanks[4]
|
|
: scenarioDetails.get('riskRanking')}
|
|
</div>
|
|
}
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">{t('account-value')}</div>
|
|
<div className="font-bold">
|
|
{formatUsdValue(scenarioDetails.get('equity'))}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">{t('assets')}</div>
|
|
<div className="font-bold">
|
|
{formatUsdValue(scenarioDetails.get('assets'))}
|
|
</div>
|
|
</div>
|
|
<div className="mb-6 flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">{t('liabilities')}</div>
|
|
<div className="font-bold">
|
|
{formatUsdValue(scenarioDetails.get('liabilities'))}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">
|
|
{t('calculator:maint-weighted-assets')}
|
|
</div>
|
|
<div className="font-bold">
|
|
{formatUsdValue(scenarioDetails.get('maintWeightAssets'))}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">
|
|
{t('calculator:maint-weighted-liabilities')}
|
|
</div>
|
|
<div className="font-bold">
|
|
{formatUsdValue(
|
|
scenarioDetails.get('maintWeightLiabilities')
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">
|
|
{t('calculator:init-weighted-assets')}
|
|
</div>
|
|
<div className="font-bold">
|
|
{formatUsdValue(scenarioDetails.get('initWeightAssets'))}
|
|
</div>
|
|
</div>
|
|
<div className="mb-6 flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">
|
|
{t('calculator:init-weighted-assets')}
|
|
</div>
|
|
<div className="font-bold">
|
|
{formatUsdValue(
|
|
scenarioDetails.get('initWeightLiabilities')
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">{t('leverage')}</div>
|
|
<div className="font-bold">
|
|
{scenarioDetails.get('leverage').toFixed(2)}x
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">
|
|
{t('calculator:percent-move-liquidation')}
|
|
</div>
|
|
<div className="font-bold">
|
|
{scenarioDetails.get('percentToLiquidationAbsolute')}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="h-64 w-full animate-pulse rounded-lg bg-th-bkg-3" />
|
|
)}
|
|
</PageBodyContainer>
|
|
</div>
|
|
)
|
|
}
|