2159 lines
88 KiB
TypeScript
2159 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'])),
|
|
},
|
|
}
|
|
}
|
|
|
|
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')
|
|
|
|
// Get mango account data
|
|
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
|
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
|
|
const mangoClient = useMangoStore((s) => s.connection.client)
|
|
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)
|
|
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
|
|
? 'Great'
|
|
: maintHealth > 0.3
|
|
? 'OK'
|
|
: initHealth > 0
|
|
? 'Poor'
|
|
: maintHealth > 0
|
|
? 'Very Poor'
|
|
: 'Rekt'
|
|
|
|
// 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 text-th-fgd-1 text-2xl font-semibold`}>
|
|
Risk Calculator
|
|
</h1>
|
|
<p className="mb-0">
|
|
IN TESTING (Use at your own risk): Please report any bugs or
|
|
comments in our #dev-ui discord channel.
|
|
</p>
|
|
{/* <p className="mb-0">
|
|
Stay healthy to keep the dream alive. We hear that one mango smoothie a day keeps the liquidators away!
|
|
</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 md:col-span-8 p-4">
|
|
<div className="flex justify-between pb-2 lg:pb-3 px-0 lg:px-3">
|
|
<div className="pb-4 lg:pb-0 text-th-fgd-1 text-lg">
|
|
Scenario Balances
|
|
</div>
|
|
<div className="flex justify-between lg:justify-start">
|
|
<Button
|
|
className={`text-xs flex items-center justify-center sm:ml-3 pt-0 pb-0 h-8 pl-3 pr-3 rounded`}
|
|
onClick={() => {
|
|
setSliderPercentage(defaultSliderVal)
|
|
toggleOrdersAsBalance(false)
|
|
createScenario(accountConnected ? 'account' : 'blank')
|
|
}}
|
|
>
|
|
<div className="flex items-center hover:text-th-primary">
|
|
<RefreshIcon className="h-5 w-5 mr-1.5" />
|
|
Reset
|
|
</div>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="bg-th-bkg-1 border border-th-fgd-4 flex items-center mb-3 lg:mx-3 px-3 h-8 rounded">
|
|
<div className="pr-5 text-th-fgd-3 text-xs whitespace-nowrap">
|
|
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="pl-4 text-th-fgd-1 text-xs w-16">
|
|
{`${Number((sliderPercentage - 1) * 100).toFixed(0)}%`}
|
|
</div>
|
|
<div className="pl-4 text-th-fgd-1 text-xs w-16 hover:text-th-primary">
|
|
<LinkButton
|
|
onClick={() => setSliderPercentage(defaultSliderVal)}
|
|
>
|
|
Reset
|
|
</LinkButton>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-between wrap pb-2 lg:pb-3 px-0 lg:px-3">
|
|
<div className="flex items-center mb-3 lg:mx-3 px-3 h-8 rounded">
|
|
<Switch
|
|
checked={showZeroBalances}
|
|
className="text-xs"
|
|
onChange={() => setShowZeroBalances(!showZeroBalances)}
|
|
>
|
|
{t('show-zero')}
|
|
</Switch>
|
|
</div>
|
|
<div className="flex items-center mb-3 lg:mx-3 px-3 h-8 rounded">
|
|
<Switch
|
|
checked={ordersAsBalance}
|
|
className="text-xs"
|
|
onChange={() => toggleOrdersAsBalance(!ordersAsBalance)}
|
|
>
|
|
Simulate orders cancelled
|
|
</Switch>
|
|
</div>
|
|
<div className="flex justify-between lg:justify-start">
|
|
<Tooltip content="Set current pricing to be the anchor point (0%) for slider">
|
|
<Button
|
|
className={`text-xs flex items-center justify-center sm:ml-3 pt-0 pb-0 h-8 pl-3 pr-3 rounded`}
|
|
onClick={() => {
|
|
anchorPricing()
|
|
setSliderPercentage(defaultSliderVal)
|
|
}}
|
|
>
|
|
<div className="flex items-center hover:text-th-primary">
|
|
<AnchorIcon className="h-5 w-5 mr-1.5" />
|
|
Anchor slider
|
|
</div>
|
|
</Button>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
{/*Hidden panel that displays a short scenario summary on mobile instead of the detailed one*/}
|
|
<div className="bg-th-bkg-1 border border-th-fgd-4 md:hidden sticky w-full rounded mb-3">
|
|
<Disclosure>
|
|
{({ open }) => (
|
|
<>
|
|
<Disclosure.Button className="bg-th-bkg-1 default-transition flex items-center justify-between p-3 w-full hover:bg-th-bkg-1 focus:outline-none">
|
|
<div className="text-th-fgd-3">
|
|
{open
|
|
? 'Scenario Details'
|
|
: 'Scenario Maintenance Health:'}
|
|
</div>
|
|
{open ? null : (
|
|
<div className="text-th-fgd-3 text-xs">
|
|
{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 text-th-fgd-1 w-4 ${
|
|
open
|
|
? 'transform rotate-360'
|
|
: 'transform rotate-180'
|
|
}`}
|
|
/>
|
|
</Disclosure.Button>
|
|
<Disclosure.Panel className="p-3">
|
|
<div className="text-th-fgd-1 text-xs">
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">
|
|
Maintenance 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">
|
|
Initial 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">
|
|
New Positions Can Be Opened
|
|
</div>
|
|
<div
|
|
className={`font-bold ${
|
|
scenarioDetails.get('initHealth') * 100 >= 0
|
|
? 'text-th-green'
|
|
: 'text-th-red'
|
|
}`}
|
|
>
|
|
{scenarioDetails.get('initHealth') * 100 >= 0
|
|
? 'Yes'
|
|
: 'No'}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">
|
|
Account Health
|
|
</div>
|
|
<div className="font-bold">
|
|
{
|
|
<div
|
|
className={`font-bold ${
|
|
scenarioDetails.get('maintHealth') * 100 <
|
|
0
|
|
? 'text-th-red'
|
|
: scenarioDetails.get('riskRanking') ===
|
|
'Very Poor'
|
|
? 'text-th-red'
|
|
: scenarioDetails.get('riskRanking') ===
|
|
'Poor'
|
|
? 'text-th-orange'
|
|
: scenarioDetails.get('riskRanking') ===
|
|
'OK'
|
|
? 'text-th-primary'
|
|
: 'text-th-green'
|
|
}`}
|
|
>
|
|
{scenarioDetails.get('maintHealth') * 100 <
|
|
0
|
|
? 'Rekt'
|
|
: scenarioDetails.get('riskRanking')}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">
|
|
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">
|
|
Percent Move To 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={`align-middle inline-block min-w-full sm:px-6 lg:px-8`}
|
|
>
|
|
<Table className="min-w-full divide-y divide-th-bkg-2">
|
|
<Thead>
|
|
<Tr className="text-th-fgd-3 text-xs">
|
|
<Th
|
|
scope="col"
|
|
className={`px-1 lg:px-3 py-1 text-left font-normal`}
|
|
>
|
|
Asset
|
|
</Th>
|
|
<Th
|
|
scope="col"
|
|
className={`px-1 lg:px-3 py-1 text-left font-normal`}
|
|
>
|
|
<div className="flex justify-start md:justify-between">
|
|
<div className="pr-2">Spot</div>
|
|
<LinkButton
|
|
onClick={() => resetScenarioColumn('spotNet')}
|
|
>
|
|
Reset
|
|
</LinkButton>
|
|
</div>
|
|
</Th>
|
|
<Th
|
|
scope="col"
|
|
className={`px-1 lg:px-3 py-1 text-left font-normal`}
|
|
>
|
|
<div className="flex justify-start md:justify-between">
|
|
<div className="pr-2">Perp</div>
|
|
<LinkButton
|
|
onClick={() =>
|
|
resetScenarioColumn('perpBasePosition')
|
|
}
|
|
>
|
|
Reset
|
|
</LinkButton>
|
|
</div>
|
|
</Th>
|
|
<Th
|
|
scope="col"
|
|
className={`px-1 lg:px-3 py-1 text-left font-normal`}
|
|
>
|
|
<div className="flex justify-start md:justify-between">
|
|
<div className="pr-2">Perp Entry</div>
|
|
<LinkButton
|
|
onClick={() =>
|
|
resetScenarioColumn('perpAvgEntryPrice')
|
|
}
|
|
>
|
|
Reset
|
|
</LinkButton>
|
|
</div>
|
|
</Th>
|
|
<Th
|
|
scope="col"
|
|
className={`px-1 lg:px-3 py-1 font-normal`}
|
|
>
|
|
<div className="flex justify-start md:justify-between">
|
|
<div className="pr-2">Price</div>
|
|
<LinkButton
|
|
onClick={() => resetScenarioColumn('price')}
|
|
>
|
|
Reset
|
|
</LinkButton>
|
|
</div>
|
|
</Th>
|
|
<Th
|
|
scope="col"
|
|
className={`px-1 lg:px-3 py-1 text-left font-normal`}
|
|
>
|
|
<div className="flex justify-start md:justify-between">
|
|
<Tooltip content="Spot Value + Perp Balance">
|
|
<div className="pr-2">Value</div>
|
|
</Tooltip>
|
|
</div>
|
|
</Th>
|
|
<Th
|
|
scope="col"
|
|
className={`px-1 lg:px-3 py-1 text-left font-normal`}
|
|
>
|
|
<div className="flex justify-start md:justify-between">
|
|
<Tooltip content="Single asset liquidation price assuming all other asset prices remain constant">
|
|
<div className="pr-2">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={`px-3 py-2 whitespace-nowrap text-sm text-th-fgd-1 w-24`}
|
|
>
|
|
<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 lg:px-3 py-2 text-sm text-th-fgd-1`}
|
|
>
|
|
<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 lg:px-3 py-2 text-sm text-th-fgd-1`}
|
|
>
|
|
<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 lg:px-3 py-2 text-sm text-th-fgd-1`}
|
|
>
|
|
<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={`px-1 lg:px-3 py-2 whitespace-nowrap text-sm text-th-fgd-1`}
|
|
>
|
|
<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={`px-1 lg:px-3 py-2 whitespace-nowrap text-sm text-th-fgd-1`}
|
|
>
|
|
<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={`px-1 lg:px-3 py-2 whitespace-nowrap text-sm text-th-fgd-1`}
|
|
>
|
|
<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="bg-th-bkg-3 col-span-4 hidden md:block p-4 relative rounded-r-lg">
|
|
<div className="pb-4 text-th-fgd-1 text-lg">
|
|
Scenario Details
|
|
</div>
|
|
{/* Joke Wrapper */}
|
|
<div className="relative col-span-4">
|
|
{scenarioDetails.get('liabilities') === 0 &&
|
|
scenarioDetails.get('equity') === 0 ? (
|
|
<div className="bg-th-green-dark border border-th-green-dark flex flex-col items-center mb-6 p-3 rounded text-center text-th-fgd-1">
|
|
<div className="pb-0.5 text-th-fgd-1">
|
|
Let's get this party started
|
|
</div>
|
|
<div className="text-th-fgd-1 text-xs">
|
|
The mangoes are ripe for the picking...
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
{scenarioDetails.get('liabilities') === 0 &&
|
|
scenarioDetails.get('equity') > 0 ? (
|
|
<div className="border border-th-green flex flex-col items-center mb-6 p-3 rounded text-center text-th-fgd-1">
|
|
<div className="pb-0.5 text-th-fgd-1">
|
|
0 Borrows = 0 Risk
|
|
</div>
|
|
<div className="text-th-fgd-3 text-xs">
|
|
Come on, live a little...
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
{scenarioDetails.get('riskRanking') === 'Great' &&
|
|
scenarioDetails.get('leverage') !== 0 ? (
|
|
<div className="border border-th-green flex flex-col items-center mb-6 p-3 rounded text-center text-th-fgd-1">
|
|
<div className="pb-0.5 text-th-fgd-1">Looking good</div>
|
|
<div className="text-th-fgd-3 text-xs">
|
|
The sun is shining and the mangoes are ripe...
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
{scenarioDetails.get('riskRanking') === 'OK' ? (
|
|
<div className="border border-th-orange flex flex-col items-center mb-6 p-3 rounded text-center text-th-fgd-1">
|
|
<div className="pb-0.5 text-th-fgd-1">
|
|
Liquidator activity is increasing
|
|
</div>
|
|
<div className="text-th-fgd-3 text-xs">
|
|
It might be time to re-think your positions
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
{scenarioDetails.get('riskRanking') === 'Poor' ? (
|
|
<div className="border border-th-red flex flex-col items-center mb-6 p-3 rounded text-center text-th-fgd-1">
|
|
<div className="pb-0.5 text-th-fgd-1">
|
|
Liquidators are closing in
|
|
</div>
|
|
<div className="text-th-fgd-3 text-xs">
|
|
Hit 'em with everything you've got...
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
{scenarioDetails.get('riskRanking') === 'Very Poor' ? (
|
|
<div className="border border-th-red flex flex-col items-center mb-6 p-3 rounded text-center text-th-fgd-1">
|
|
<div className="pb-0.5 text-th-fgd-1">
|
|
Liquidators have spotted you
|
|
</div>
|
|
<div className="text-th-fgd-3 text-xs">
|
|
Throw some money at them to make them go away...
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
{scenarioDetails.get('riskRanking') === 'Rekt' ? (
|
|
<div className="bg-th-red border border-th-red flex flex-col items-center mb-6 p-3 rounded text-center text-th-fgd-1">
|
|
<div className="pb-0.5 text-th-fgd-1">Liquidated!</div>
|
|
<div className="text-th-fgd-1 text-xs">
|
|
Insert coin to continue...
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<Tooltip content="Maintenance health must be above 0% to avoid liquidation.">
|
|
<div className="text-th-fgd-3">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="Initial health must be above 0% to open new positions.">
|
|
<div className="text-th-fgd-3">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">
|
|
New Positions Can Be Opened
|
|
</div>
|
|
<div
|
|
className={`font-bold ${
|
|
scenarioDetails.get('initHealth') * 100 >= 0
|
|
? 'text-th-green'
|
|
: 'text-th-red'
|
|
}`}
|
|
>
|
|
{scenarioDetails.get('initHealth') * 100 >= 0
|
|
? 'Yes'
|
|
: 'No'}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3 mb-6">
|
|
<div className="text-th-fgd-3">Account Health</div>
|
|
{
|
|
<div
|
|
className={`font-bold ${
|
|
scenarioDetails.get('maintHealth') * 100 < 0
|
|
? 'text-th-red'
|
|
: scenarioDetails.get('riskRanking') === 'Very Poor'
|
|
? 'text-th-red'
|
|
: scenarioDetails.get('riskRanking') === 'Poor'
|
|
? 'text-th-orange'
|
|
: scenarioDetails.get('riskRanking') === 'OK'
|
|
? 'text-th-primary'
|
|
: 'text-th-green'
|
|
}`}
|
|
>
|
|
{scenarioDetails.get('maintHealth') * 100 < 0
|
|
? 'Rekt'
|
|
: scenarioDetails.get('riskRanking')}
|
|
</div>
|
|
}
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3">
|
|
<div className="text-th-fgd-3">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">Assets</div>
|
|
<div className="font-bold">
|
|
{formatUsdValue(scenarioDetails.get('assets'))}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3 mb-6">
|
|
<div className="text-th-fgd-3">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">
|
|
Maint. Weighted Assets Value
|
|
</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">
|
|
Maint. Weighted Liabilities Value
|
|
</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">
|
|
Init. Weighted Assets Value
|
|
</div>
|
|
<div className="font-bold">
|
|
{formatUsdValue(scenarioDetails.get('initWeightAssets'))}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between pb-3 mb-6">
|
|
<div className="text-th-fgd-3">
|
|
Init. Weighted Liabilities Value
|
|
</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">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">
|
|
Percent Move To Liquidation
|
|
</div>
|
|
<div className="font-bold">
|
|
{scenarioDetails.get('percentToLiquidationAbsolute')}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="animate-pulse bg-th-bkg-3 h-64 rounded-lg w-full" />
|
|
)}
|
|
</PageBodyContainer>
|
|
</div>
|
|
)
|
|
}
|