import { PerpMarket, PerpPosition } from '@blockworks-foundation/mango-v4' import { TwitterIcon } from '@components/icons/TwitterIcon' import SharePositionModal from '@components/modals/SharePositionModal' import Button, { IconButton, LinkButton } from '@components/shared/Button' import ConnectEmptyState from '@components/shared/ConnectEmptyState' import FormatNumericValue from '@components/shared/FormatNumericValue' import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements' import { ChevronDownIcon, NoSymbolIcon } from '@heroicons/react/20/solid' import { useWallet } from '@solana/wallet-adapter-react' import mangoStore from '@store/mangoStore' import useMangoAccount from 'hooks/useMangoAccount' import useMangoGroup from 'hooks/useMangoGroup' import useSelectedMarket from 'hooks/useSelectedMarket' import useUnownedAccount from 'hooks/useUnownedAccount' import { useViewport } from 'hooks/useViewport' import { useTranslation } from 'next-i18next' import { useCallback, useMemo, useState } from 'react' import { floorToDecimal, getDecimalCount } from 'utils/numbers' import { breakpoints } from 'utils/theme' import { calculateLimitPriceForMarketOrder } from 'utils/tradeForm' import MarketCloseModal from './MarketCloseModal' import MarketLogos from './MarketLogos' import TableMarketName from './TableMarketName' import Tooltip from '@components/shared/Tooltip' import { Disclosure, Transition } from '@headlessui/react' import useOpenPerpPositions from 'hooks/useOpenPerpPositions' import PnlTooltipContent from '@components/shared/PnlTooltipContent' import PerpSideBadge from './PerpSideBadge' import CloseAllPositionsModal from './CloseAllPositionsModal' import NukeIcon from '@components/icons/NukeIcon' const PerpPositions = () => { const { t } = useTranslation(['common', 'trade']) const { group } = useMangoGroup() const [showMarketCloseModal, setShowMarketCloseModal] = useState(false) const [showCloseAllModal, setShowCloseAllModal] = useState(false) const [positionToClose, setPositionToClose] = useState( null, ) const [showShareModal, setShowShareModal] = useState(false) const [positionToShare, setPositionToShare] = useState( null, ) const openPerpPositions = useOpenPerpPositions() const { selectedMarket } = useSelectedMarket() const { connected } = useWallet() const { mangoAccount } = useMangoAccount() const { isUnownedAccount } = useUnownedAccount() const { width } = useViewport() const showTableView = width ? width > breakpoints.md : false const totalPnlStats = useMemo(() => { if (openPerpPositions.length && group !== undefined) { const pnlByMarket = openPerpPositions.map((position) => { const market = group.getPerpMarketByMarketIndex(position.marketIndex) const basePosition = position.getBasePositionUi(market) const avgEntryPrice = position.getAverageEntryPriceUi(market) return { unrealized: position.getUnRealizedPnlUi(market), realized: position.getRealizedPnlUi(), total: position.cumulativePnlOverPositionLifetimeUi(market), unsettled: position.getUnsettledPnlUi(market), averageEntryValue: Math.abs(basePosition) * avgEntryPrice, } }) const p = pnlByMarket.reduce((a, b) => { return { unrealized: a.unrealized + b.unrealized, realized: a.realized + b.realized, total: a.total + b.total, unsettled: a.unsettled + b.unsettled, averageEntryValue: a.averageEntryValue + b.averageEntryValue, } }) return { unrealized: p.unrealized, realized: p.realized, total: p.total, unsettled: p.unsettled, roe: (p.unrealized / p.averageEntryValue) * 100, } } return { unrealized: 0, realized: 0, total: 0, unsettled: 0, roe: 0 } }, [openPerpPositions, group]) const handlePositionClick = (positionSize: number, market: PerpMarket) => { const tradeForm = mangoStore.getState().tradeForm const set = mangoStore.getState().set let price = Number(tradeForm.price) if (tradeForm.tradeType === 'Market') { const orderbook = mangoStore.getState().selectedMarket.orderbook price = calculateLimitPriceForMarketOrder( orderbook, positionSize, tradeForm.side, ) } const newSide = positionSize > 0 ? 'sell' : 'buy' const baseSize = Math.abs(positionSize) const quoteSize = floorToDecimal( baseSize * price, getDecimalCount(market.tickSize), ) set((s) => { s.tradeForm.side = newSide s.tradeForm.baseSize = baseSize.toString() s.tradeForm.quoteSize = quoteSize.toString() }) } const showClosePositionModal = useCallback((position: PerpPosition) => { setShowMarketCloseModal(true) setPositionToClose(position) }, []) const hideClosePositionModal = useCallback(() => { setShowMarketCloseModal(false) setPositionToClose(null) }, []) const handleShowShare = (position: PerpPosition) => { setPositionToShare(position) setShowShareModal(true) } if (!group) return null return ( <> {mangoAccount && openPerpPositions.length ? ( showTableView ? (
{!isUnownedAccount ? ( ) : null} {openPerpPositions.map((position, index) => { const market = group.getPerpMarketByMarketIndex( position.marketIndex, ) const basePosition = position.getBasePositionUi(market) const floorBasePosition = floorToDecimal( basePosition, getDecimalCount(market.minOrderSize), ).toNumber() const isSelectedMarket = selectedMarket instanceof PerpMarket && selectedMarket.perpMarketIndex === position.marketIndex if (!basePosition) return null const isLong = basePosition > 0 const avgEntryPrice = position.getAverageEntryPriceUi(market) const unsettledPnl = position.getUnsettledPnlUi(market) const totalPnl = position.cumulativePnlOverPositionLifetimeUi(market) const unrealizedPnl = position.getUnRealizedPnlUi(market) const realizedPnl = position.getRealizedPnlUi() const roe = (unrealizedPnl / (Math.abs(basePosition) * avgEntryPrice)) * 100 let estLiqPrice try { estLiqPrice = position.getLiquidationPriceUi( group, mangoAccount, ) } catch (e) { estLiqPrice = null } return ( {!isUnownedAccount ? ( ) : null} ) })} {openPerpPositions.length > 1 ? ( {!isUnownedAccount ? ( ) : null} ) : null}
{t('market')} {t('trade:size')} {t('trade:avg-entry-price')}
{t('trade:est-liq-price')}
{t('trade:unrealized-pnl')} {openPerpPositions?.length > 1 ? (
setShowCloseAllModal(true)} > {t('trade:close-all')}
) : null}
{isSelectedMarket ? (
handlePositionClick(floorBasePosition, market) } >
) : (
)}
{estLiqPrice ? ( ) : ( '–' )}
} delay={100} > = 0 ? 'text-th-up' : 'text-th-down' }`} >
handleShowShare(openPerpPositions[index]) } disabled={!group || !basePosition} >
<> <> <> <>
Total: } delay={100} >
= 0 ? 'text-th-up' : 'text-th-down' }`} value={totalPnlStats.unrealized} isUsd decimals={2} />
{' '} <>
) : (
{openPerpPositions.map((position, i) => { const market = group.getPerpMarketByMarketIndex( position.marketIndex, ) const basePosition = position.getBasePositionUi(market) const floorBasePosition = floorToDecimal( basePosition, getDecimalCount(market.minOrderSize), ).toNumber() const isSelectedMarket = selectedMarket instanceof PerpMarket && selectedMarket.perpMarketIndex === position.marketIndex if (!basePosition) return null const side = basePosition > 0 ? 'buy' : basePosition < 0 ? 'sell' : '' const avgEntryPrice = position.getAverageEntryPriceUi(market) const totalPnl = position.cumulativePnlOverPositionLifetimeUi(market) const unrealizedPnl = position.getUnRealizedPnlUi(market) const realizedPnl = position.getRealizedPnlUi() const roe = (unrealizedPnl / (Math.abs(basePosition) * avgEntryPrice)) * 100 const estLiqPrice = position.getLiquidationPriceUi( group, mangoAccount, ) const unsettledPnl = position.getUnsettledPnlUi(market) const notional = Math.abs(floorBasePosition) * market._uiPrice return ( {({ open }) => ( <>
{market.name}
|
0 ? 'text-th-up' : 'text-th-down' }`} >

{t('trade:size')}

{isSelectedMarket ? (
handlePositionClick( floorBasePosition, market, ) } >
) : (
)}

{t('trade:avg-entry-price')}

{t('trade:est-liq-price')}

{estLiqPrice ? ( ) : ( '–' )}

{t('trade:unsettled')} {t('pnl')}

{t('trade:unrealized-pnl')}

} delay={100} > = 0 ? 'text-th-up' : 'text-th-down' }`} >

ROE

= 0 ? 'text-th-up' : 'text-th-down' }`} > %

{openPerpPositions?.length > 1 ? ( ) : null}
)}
) })} {openPerpPositions.length > 0 ? ( <> {({ open }) => ( <>
Total Unrealized PnL: 0 ? 'text-th-up' : 'text-th-down' }`} >
Total ROE: = 0 ? 'text-th-up' : 'text-th-down' }`} > %{' '}
)}
) : null}
) ) : mangoAccount || connected ? (

{t('trade:no-positions')}

) : (
)} {showShareModal ? ( setShowShareModal(false)} position={positionToShare!} /> ) : null} {showMarketCloseModal && positionToClose ? ( ) : null} {showCloseAllModal ? ( setShowCloseAllModal(false)} /> ) : null} ) } export default PerpPositions