2023-07-03 05:29:10 -07:00
|
|
|
|
import FormatNumericValue from '@components/shared/FormatNumericValue'
|
|
|
|
|
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
|
|
|
|
|
import TableMarketName from '@components/trade/TableMarketName'
|
2023-07-04 17:05:03 -07:00
|
|
|
|
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/20/solid'
|
2023-07-03 05:29:10 -07:00
|
|
|
|
import useMangoGroup from 'hooks/useMangoGroup'
|
|
|
|
|
import { useTranslation } from 'next-i18next'
|
|
|
|
|
import { abbreviateAddress } from 'utils/formatting'
|
|
|
|
|
import { floorToDecimal, getDecimalCount } from 'utils/numbers'
|
|
|
|
|
import Tooltip from '@components/shared/Tooltip'
|
|
|
|
|
import PnlTooltipContent from '@components/shared/PnlTooltipContent'
|
2023-07-04 17:05:03 -07:00
|
|
|
|
import { useViewport } from 'hooks/useViewport'
|
|
|
|
|
import { breakpoints } from 'utils/theme'
|
|
|
|
|
import { Disclosure, Transition } from '@headlessui/react'
|
|
|
|
|
import { PositionStat } from 'types'
|
2023-07-03 05:29:10 -07:00
|
|
|
|
|
|
|
|
|
const PerpPositionsStatsTable = ({
|
|
|
|
|
positions,
|
|
|
|
|
}: {
|
|
|
|
|
positions: PositionStat[]
|
|
|
|
|
}) => {
|
|
|
|
|
const { t } = useTranslation(['common', 'stats', 'trade'])
|
|
|
|
|
const { group } = useMangoGroup()
|
2023-07-04 17:05:03 -07:00
|
|
|
|
const { width } = useViewport()
|
|
|
|
|
const showTableView = width ? width > breakpoints.md : false
|
2023-07-03 05:29:10 -07:00
|
|
|
|
|
|
|
|
|
if (!group) return null
|
|
|
|
|
|
2023-07-04 17:05:03 -07:00
|
|
|
|
return showTableView ? (
|
2023-07-03 05:29:10 -07:00
|
|
|
|
<div className="thin-scroll overflow-x-auto">
|
|
|
|
|
<Table>
|
|
|
|
|
<thead>
|
|
|
|
|
<TrHead>
|
|
|
|
|
<Th className="text-left">{t('market')}</Th>
|
|
|
|
|
<Th className="text-right">{t('trade:size')}</Th>
|
|
|
|
|
<Th className="text-right">{t('trade:avg-entry-price')}</Th>
|
|
|
|
|
<Th className="text-right">{t('trade:est-liq-price')}</Th>
|
|
|
|
|
<Th className="text-right">{t('trade:unrealized-pnl')}</Th>
|
|
|
|
|
<Th className="text-right">{t('account')}</Th>
|
|
|
|
|
</TrHead>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{positions.map(({ account, perpPosition, mangoAccount }, i) => {
|
|
|
|
|
const market = group.getPerpMarketByMarketIndex(
|
2023-07-21 11:47:53 -07:00
|
|
|
|
perpPosition.marketIndex,
|
2023-07-03 05:29:10 -07:00
|
|
|
|
)
|
|
|
|
|
const basePosition = perpPosition.getBasePositionUi(market)
|
|
|
|
|
|
|
|
|
|
if (!basePosition) return null
|
|
|
|
|
|
|
|
|
|
const floorBasePosition = floorToDecimal(
|
|
|
|
|
basePosition,
|
2023-07-21 11:47:53 -07:00
|
|
|
|
getDecimalCount(market.minOrderSize),
|
2023-07-03 05:29:10 -07:00
|
|
|
|
).toNumber()
|
|
|
|
|
|
|
|
|
|
const isLong = basePosition > 0
|
|
|
|
|
const avgEntryPrice = perpPosition.getAverageEntryPriceUi(market)
|
|
|
|
|
const unsettledPnl = perpPosition.getUnsettledPnlUi(market)
|
|
|
|
|
const totalPnl =
|
|
|
|
|
perpPosition.cumulativePnlOverPositionLifetimeUi(market)
|
|
|
|
|
const unrealizedPnl = perpPosition.getUnRealizedPnlUi(market)
|
|
|
|
|
const realizedPnl = perpPosition.getRealizedPnlUi()
|
|
|
|
|
const roe =
|
|
|
|
|
(unrealizedPnl / (Math.abs(basePosition) * avgEntryPrice)) * 100
|
|
|
|
|
let estLiqPrice
|
|
|
|
|
if (account) {
|
|
|
|
|
estLiqPrice = perpPosition.getLiquidationPriceUi(group, account)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<TrBody
|
|
|
|
|
key={`${perpPosition.marketIndex}${basePosition}${i}`}
|
|
|
|
|
className="my-1 p-2"
|
|
|
|
|
>
|
|
|
|
|
<Td>
|
|
|
|
|
<TableMarketName
|
|
|
|
|
market={market}
|
|
|
|
|
side={isLong ? 'long' : 'short'}
|
|
|
|
|
/>
|
|
|
|
|
</Td>
|
|
|
|
|
<Td className="text-right font-mono">
|
|
|
|
|
<div className="flex flex-col items-end space-y-0.5">
|
|
|
|
|
<FormatNumericValue
|
|
|
|
|
value={Math.abs(basePosition)}
|
|
|
|
|
decimals={getDecimalCount(market.minOrderSize)}
|
|
|
|
|
/>
|
|
|
|
|
<FormatNumericValue
|
|
|
|
|
classNames="text-xs text-th-fgd-3"
|
|
|
|
|
value={Math.abs(floorBasePosition) * market._uiPrice}
|
|
|
|
|
isUsd
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</Td>
|
|
|
|
|
<Td className="font-mono">
|
|
|
|
|
<div className="flex flex-col items-end space-y-0.5">
|
|
|
|
|
<FormatNumericValue
|
|
|
|
|
value={avgEntryPrice}
|
|
|
|
|
decimals={getDecimalCount(market.tickSize)}
|
|
|
|
|
isUsd
|
|
|
|
|
/>
|
|
|
|
|
<FormatNumericValue
|
|
|
|
|
classNames="text-xs text-th-fgd-3"
|
|
|
|
|
value={market.uiPrice}
|
|
|
|
|
decimals={getDecimalCount(market.tickSize)}
|
|
|
|
|
isUsd
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</Td>
|
|
|
|
|
<Td className="text-right font-mono">
|
|
|
|
|
{estLiqPrice ? (
|
|
|
|
|
<FormatNumericValue
|
|
|
|
|
value={estLiqPrice}
|
|
|
|
|
decimals={getDecimalCount(market.tickSize)}
|
|
|
|
|
isUsd
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
'–'
|
|
|
|
|
)}
|
|
|
|
|
</Td>
|
|
|
|
|
<Td className="text-right font-mono">
|
|
|
|
|
<div className="flex flex-col items-end ">
|
|
|
|
|
<Tooltip
|
|
|
|
|
content={
|
|
|
|
|
<PnlTooltipContent
|
|
|
|
|
unrealizedPnl={unrealizedPnl}
|
|
|
|
|
realizedPnl={realizedPnl}
|
|
|
|
|
totalPnl={totalPnl}
|
|
|
|
|
unsettledPnl={unsettledPnl}
|
2023-08-09 20:29:17 -07:00
|
|
|
|
roe={roe}
|
2023-07-03 05:29:10 -07:00
|
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
delay={100}
|
|
|
|
|
>
|
|
|
|
|
<span
|
|
|
|
|
className={`tooltip-underline ${
|
|
|
|
|
unrealizedPnl >= 0 ? 'text-th-up' : 'text-th-down'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<FormatNumericValue
|
|
|
|
|
value={unrealizedPnl}
|
|
|
|
|
isUsd
|
|
|
|
|
decimals={2}
|
|
|
|
|
/>
|
|
|
|
|
</span>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
<span className={roe >= 0 ? 'text-th-up' : 'text-th-down'}>
|
|
|
|
|
<FormatNumericValue
|
|
|
|
|
classNames="text-xs"
|
|
|
|
|
value={roe}
|
|
|
|
|
decimals={2}
|
|
|
|
|
/>
|
|
|
|
|
%{' '}
|
|
|
|
|
<span className="font-body text-xs text-th-fgd-3">
|
|
|
|
|
(ROE)
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</Td>
|
|
|
|
|
<Td>
|
|
|
|
|
<div className="flex items-center justify-end">
|
|
|
|
|
<a
|
|
|
|
|
href={`/?address=${mangoAccount.toString()}`}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="flex cursor-pointer items-center text-th-fgd-2"
|
|
|
|
|
>
|
|
|
|
|
<span className="mr-1">
|
|
|
|
|
{abbreviateAddress(mangoAccount)}
|
|
|
|
|
</span>
|
|
|
|
|
<ChevronRightIcon className="h-5 w-5" />
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
</Td>
|
|
|
|
|
</TrBody>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</tbody>
|
|
|
|
|
</Table>
|
|
|
|
|
</div>
|
2023-07-04 17:05:03 -07:00
|
|
|
|
) : (
|
|
|
|
|
<div className="border-b border-th-bkg-3">
|
|
|
|
|
{positions.map(({ account, perpPosition, mangoAccount }) => {
|
|
|
|
|
const market = group.getPerpMarketByMarketIndex(
|
2023-07-21 11:47:53 -07:00
|
|
|
|
perpPosition.marketIndex,
|
2023-07-04 17:05:03 -07:00
|
|
|
|
)
|
|
|
|
|
const basePosition = perpPosition.getBasePositionUi(market)
|
|
|
|
|
|
|
|
|
|
if (!basePosition) return null
|
|
|
|
|
|
|
|
|
|
const floorBasePosition = floorToDecimal(
|
|
|
|
|
basePosition,
|
2023-07-21 11:47:53 -07:00
|
|
|
|
getDecimalCount(market.minOrderSize),
|
2023-07-04 17:05:03 -07:00
|
|
|
|
).toNumber()
|
|
|
|
|
|
|
|
|
|
const isLong = basePosition > 0
|
|
|
|
|
const avgEntryPrice = perpPosition.getAverageEntryPriceUi(market)
|
|
|
|
|
const unsettledPnl = perpPosition.getUnsettledPnlUi(market)
|
|
|
|
|
const totalPnl =
|
|
|
|
|
perpPosition.cumulativePnlOverPositionLifetimeUi(market)
|
|
|
|
|
const unrealizedPnl = perpPosition.getUnRealizedPnlUi(market)
|
|
|
|
|
const realizedPnl = perpPosition.getRealizedPnlUi()
|
|
|
|
|
const roe =
|
|
|
|
|
(unrealizedPnl / (Math.abs(basePosition) * avgEntryPrice)) * 100
|
|
|
|
|
let estLiqPrice: number | null
|
|
|
|
|
if (account) {
|
|
|
|
|
estLiqPrice = perpPosition.getLiquidationPriceUi(group, account)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Disclosure key={perpPosition.marketIndex + basePosition}>
|
|
|
|
|
{({ open }) => (
|
|
|
|
|
<>
|
|
|
|
|
<Disclosure.Button
|
|
|
|
|
className={`flex w-full items-center justify-between border-t border-th-bkg-3 p-4 text-left focus:outline-none`}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex w-full flex-col sm:flex-row sm:items-center sm:justify-between">
|
|
|
|
|
<TableMarketName
|
|
|
|
|
market={market}
|
|
|
|
|
side={isLong ? 'long' : 'short'}
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<span className="font-mono">
|
|
|
|
|
<FormatNumericValue
|
|
|
|
|
value={Math.abs(basePosition)}
|
|
|
|
|
decimals={getDecimalCount(market.minOrderSize)}
|
|
|
|
|
/>{' '}
|
|
|
|
|
<span className="font-body text-th-fgd-3">
|
|
|
|
|
{market.name.split('-')[0]}
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-th-fgd-4">|</span>
|
|
|
|
|
<span className="font-mono text-th-fgd-3">
|
|
|
|
|
<FormatNumericValue
|
|
|
|
|
value={Math.abs(floorBasePosition) * market._uiPrice}
|
|
|
|
|
isUsd
|
|
|
|
|
/>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<ChevronDownIcon
|
|
|
|
|
className={`${
|
|
|
|
|
open ? 'rotate-180' : 'rotate-360'
|
|
|
|
|
} ml-3 h-6 w-6 flex-shrink-0 text-th-fgd-3`}
|
|
|
|
|
/>
|
|
|
|
|
</Disclosure.Button>
|
|
|
|
|
<Transition
|
|
|
|
|
enter="transition ease-in duration-200"
|
|
|
|
|
enterFrom="opacity-0"
|
|
|
|
|
enterTo="opacity-100"
|
|
|
|
|
>
|
|
|
|
|
<Disclosure.Panel>
|
|
|
|
|
<div className="mx-4 grid grid-cols-2 gap-4 border-t border-th-bkg-3 pt-4 pb-4">
|
|
|
|
|
<div className="col-span-1">
|
|
|
|
|
<p className="text-xs text-th-fgd-3">
|
|
|
|
|
{t('trade:size')}
|
|
|
|
|
</p>
|
|
|
|
|
<div className="flex flex-col font-mono text-th-fgd-2">
|
|
|
|
|
<FormatNumericValue
|
|
|
|
|
value={Math.abs(basePosition)}
|
|
|
|
|
decimals={getDecimalCount(market.minOrderSize)}
|
|
|
|
|
/>
|
|
|
|
|
<FormatNumericValue
|
|
|
|
|
classNames="text-xs text-th-fgd-3"
|
|
|
|
|
value={
|
|
|
|
|
Math.abs(floorBasePosition) * market._uiPrice
|
|
|
|
|
}
|
|
|
|
|
isUsd
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="col-span-1">
|
|
|
|
|
<p className="text-xs text-th-fgd-3">
|
|
|
|
|
{t('trade:avg-entry-price')}
|
|
|
|
|
</p>
|
|
|
|
|
<div className="flex flex-col font-mono">
|
|
|
|
|
<FormatNumericValue
|
|
|
|
|
classNames="text-th-fgd-2"
|
|
|
|
|
value={avgEntryPrice}
|
|
|
|
|
decimals={getDecimalCount(market.tickSize)}
|
|
|
|
|
isUsd
|
|
|
|
|
/>
|
|
|
|
|
<FormatNumericValue
|
|
|
|
|
classNames="text-xs text-th-fgd-3"
|
|
|
|
|
value={market.uiPrice}
|
|
|
|
|
decimals={getDecimalCount(market.tickSize)}
|
|
|
|
|
isUsd
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="col-span-1">
|
|
|
|
|
<Tooltip content={t('trade:tooltip-est-liq-price')}>
|
|
|
|
|
<p className="tooltip-underline text-xs text-th-fgd-3">
|
|
|
|
|
{t('trade:est-liq-price')}
|
|
|
|
|
</p>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
<p className="font-mono text-th-fgd-2">
|
|
|
|
|
{estLiqPrice ? (
|
|
|
|
|
<FormatNumericValue
|
|
|
|
|
value={estLiqPrice}
|
|
|
|
|
decimals={getDecimalCount(market.tickSize)}
|
|
|
|
|
isUsd
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
'–'
|
|
|
|
|
)}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="col-span-1">
|
|
|
|
|
<p className="text-xs text-th-fgd-3">
|
|
|
|
|
{t('trade:unrealized-pnl')}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="font-mono text-th-fgd-2">
|
|
|
|
|
<Tooltip
|
|
|
|
|
content={
|
|
|
|
|
<PnlTooltipContent
|
|
|
|
|
unrealizedPnl={unrealizedPnl}
|
|
|
|
|
realizedPnl={realizedPnl}
|
|
|
|
|
totalPnl={totalPnl}
|
|
|
|
|
unsettledPnl={unsettledPnl}
|
2023-08-09 20:29:17 -07:00
|
|
|
|
roe={roe}
|
2023-07-04 17:05:03 -07:00
|
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
delay={100}
|
|
|
|
|
>
|
|
|
|
|
<span
|
|
|
|
|
className={`tooltip-underline mb-1 ${
|
|
|
|
|
unrealizedPnl >= 0
|
|
|
|
|
? 'text-th-up'
|
|
|
|
|
: 'text-th-down'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<FormatNumericValue
|
|
|
|
|
value={unrealizedPnl}
|
|
|
|
|
isUsd
|
|
|
|
|
decimals={2}
|
|
|
|
|
/>
|
|
|
|
|
</span>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="col-span-1">
|
|
|
|
|
<p className="text-xs text-th-fgd-3">ROE</p>
|
|
|
|
|
<p
|
|
|
|
|
className={`font-mono ${
|
|
|
|
|
roe >= 0 ? 'text-th-up' : 'text-th-down'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<FormatNumericValue value={roe} decimals={2} />%
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="col-span-1">
|
|
|
|
|
<p className="text-xs text-th-fgd-3">{t('account')}</p>
|
|
|
|
|
<a
|
|
|
|
|
href={`/?address=${mangoAccount.toString()}`}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="flex cursor-pointer items-center text-th-fgd-2"
|
|
|
|
|
>
|
|
|
|
|
<span className="mr-1">
|
|
|
|
|
{abbreviateAddress(mangoAccount)}
|
|
|
|
|
</span>
|
|
|
|
|
<ChevronRightIcon className="h-5 w-5" />
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Disclosure.Panel>
|
|
|
|
|
</Transition>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Disclosure>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
2023-07-03 05:29:10 -07:00
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default PerpPositionsStatsTable
|