diff --git a/components/shared/FormatNumericValue.tsx b/components/shared/FormatNumericValue.tsx index 980fdbce..e37b7bb3 100644 --- a/components/shared/FormatNumericValue.tsx +++ b/components/shared/FormatNumericValue.tsx @@ -2,18 +2,20 @@ import Decimal from 'decimal.js' import { formatCurrencyValue, formatNumericValue } from 'utils/numbers' const FormatNumericValue = ({ + classNames, value, decimals, isUsd, roundUp, }: { + classNames?: string value: Decimal | number | string decimals?: number isUsd?: boolean roundUp?: boolean }) => { return ( - + {isUsd ? formatCurrencyValue(value, decimals) : formatNumericValue(value, decimals, roundUp)} diff --git a/components/trade/PerpPositions.tsx b/components/trade/PerpPositions.tsx index add531f1..dd025f5e 100644 --- a/components/trade/PerpPositions.tsx +++ b/components/trade/PerpPositions.tsx @@ -5,7 +5,12 @@ 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 { NoSymbolIcon } from '@heroicons/react/20/solid' +import { + ArrowTrendingDownIcon, + ArrowTrendingUpIcon, + ChevronDownIcon, + NoSymbolIcon, +} from '@heroicons/react/20/solid' import { useWallet } from '@solana/wallet-adapter-react' import mangoStore from '@store/mangoStore' import useMangoAccount from 'hooks/useMangoAccount' @@ -14,8 +19,6 @@ import useSelectedMarket from 'hooks/useSelectedMarket' import useUnownedAccount from 'hooks/useUnownedAccount' import { useViewport } from 'hooks/useViewport' import { useTranslation } from 'next-i18next' -import Link from 'next/link' -import { useRouter } from 'next/router' import { useCallback, useState } from 'react' import { floorToDecimal, @@ -26,9 +29,10 @@ import { breakpoints } from 'utils/theme' import { calculateLimitPriceForMarketOrder } from 'utils/tradeForm' import MarketCloseModal from './MarketCloseModal' import MarketLogos from './MarketLogos' -import PerpSideBadge from './PerpSideBadge' import TableMarketName from './TableMarketName' import Tooltip from '@components/shared/Tooltip' +import { Disclosure, Transition } from '@headlessui/react' +import useOpenPerpPositions from 'hooks/useOpenPerpPositions' const PerpPositions = () => { const { t } = useTranslation(['common', 'trade']) @@ -41,14 +45,13 @@ const PerpPositions = () => { const [positionToShare, setPositionToShare] = useState( null ) - const perpPositions = mangoStore((s) => s.mangoAccount.perpPositions) + const openPerpPositions = useOpenPerpPositions() const { selectedMarket } = useSelectedMarket() const { connected } = useWallet() - const { mangoAccountAddress } = useMangoAccount() + const { mangoAccount } = useMangoAccount() const { isUnownedAccount } = useUnownedAccount() const { width } = useViewport() const showTableView = width ? width > breakpoints.md : false - const { asPath } = useRouter() const handlePositionClick = (positionSize: number, market: PerpMarket) => { const tradeForm = mangoStore.getState().tradeForm @@ -94,21 +97,9 @@ const PerpPositions = () => { if (!group) return null - const openPerpPositions = Object.values(perpPositions) - .filter((p) => p.basePositionLots.toNumber()) - .sort((a, b) => { - const aMarket = group.getPerpMarketByMarketIndex(a.marketIndex) - const bMarket = group.getPerpMarketByMarketIndex(b.marketIndex) - const aBasePosition = a.getBasePositionUi(aMarket) - const bBasePosition = b.getBasePositionUi(bMarket) - const aNotional = aBasePosition * aMarket._uiPrice - const bNotional = bBasePosition * bMarket._uiPrice - return Math.abs(bNotional) - Math.abs(aNotional) - }) - return ( <> - {mangoAccountAddress && openPerpPositions.length ? ( + {mangoAccount && openPerpPositions.length ? ( showTableView ? ( <>
@@ -116,14 +107,17 @@ const PerpPositions = () => { {t('market')} - {t('trade:side')} {t('trade:size')} - {t('trade:notional')} {t('trade:avg-entry-price')} - {t('trade:oracle-price')} - {`${t('trade:unsettled')} ${t( - 'pnl' - )}`} + +
+ + + {t('trade:est-liq-price')} + + +
+ {t('trade:unrealized-pnl')} {!isUnownedAccount ? : null}
@@ -144,11 +138,22 @@ const PerpPositions = () => { 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 + const estLiqPrice = position.getLiquidationPriceUi( + group, + mangoAccount + ) return ( { className="my-1 p-2" > - - - - + -

- {isSelectedMarket ? ( + {isSelectedMarket ? ( +

@@ -177,68 +182,99 @@ const PerpPositions = () => { )} /> - ) : ( + +
+ ) : ( +
- )} -

- - - - - - - - - - - - - - - - } - delay={100} - > +
+ )} + + +
+ + +
+ + + {estLiqPrice ? ( + + ) : ( + '–' + )} + + +
+ + } + delay={100} + > + = 0 + ? 'text-th-up' + : 'text-th-down' + }`} + > + + + 0 - ? 'text-th-up' - : 'text-th-down' - }`} + className={ + roe >= 0 ? 'text-th-up' : 'text-th-down' + } > + %{' '} + + (ROE) + - +
{!isUnownedAccount ? ( @@ -278,8 +314,8 @@ const PerpPositions = () => { ) : null} ) : ( - <> - {openPerpPositions.map((position) => { +
+ {openPerpPositions.map((position, i) => { const market = group.getPerpMarketByMarketIndex( position.marketIndex ) @@ -293,111 +329,266 @@ const PerpPositions = () => { 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) return ( -
-
-
- -
-
-
- {selectedMarket?.name === market.name ? ( - - {market.name} - - ) : ( - -
- - {market.name} - -
- - )} - -
-
-

- - {isSelectedMarket && asPath === '/trade' ? ( - - handlePositionClick(floorBasePosition, market) - } - > - - - ) : ( + + {({ open }) => ( + <> + +

+
+ + + {market.name} + + {side === 'buy' ? ( + + ) : side === 'sell' ? ( + + ) : null} +
+
+ {' '} + + {market.name.split('-')[0]} + + + | + 0 + ? 'text-th-up' + : 'text-th-down' + }`} + > + - )} - - from - - - -

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

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

- - -
- {!isUnownedAccount ? ( - - ) : null} -
-
+ +
+
+

+ {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' + }`} + > + % +

+
+
+ + +
+
+
+ + + )} + ) })} - +
) - ) : mangoAccountAddress || connected ? ( + ) : mangoAccount || connected ? (

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

@@ -425,15 +616,25 @@ const PnlTooltipContent = ({ unrealizedPnl, realizedPnl, totalPnl, + unsettledPnl, }: { unrealizedPnl: number realizedPnl: number totalPnl: number + unsettledPnl: number }) => { const { t } = useTranslation(['common', 'trade']) return ( <> -
+
+

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

+ + {formatCurrencyValue(unsettledPnl, 2)} + +
+

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

diff --git a/components/trade/TableMarketName.tsx b/components/trade/TableMarketName.tsx index 94fa3a97..b939b4cd 100644 --- a/components/trade/TableMarketName.tsx +++ b/components/trade/TableMarketName.tsx @@ -3,24 +3,68 @@ import useSelectedMarket from 'hooks/useSelectedMarket' import Link from 'next/link' import { useRouter } from 'next/router' import MarketLogos from './MarketLogos' +import { + ArrowTrendingDownIcon, + ArrowTrendingUpIcon, +} from '@heroicons/react/20/solid' -const TableMarketName = ({ market }: { market: PerpMarket | Serum3Market }) => { +const TableMarketName = ({ + market, + side, +}: { + market: PerpMarket | Serum3Market + side?: 'buy' | 'sell' +}) => { const { selectedMarket } = useSelectedMarket() const { asPath } = useRouter() return selectedMarket?.name === market.name && asPath.includes('/trade') ? ( -
- - {market.name} +
+
) : ( -
- - {market.name} +
+
) } export default TableMarketName + +const NameAndSide = ({ + market, + side, +}: { + market: PerpMarket | Serum3Market + side?: 'buy' | 'sell' +}) => { + return ( + <> + + {market.name} + {side === 'buy' ? ( + + ) : side === 'sell' ? ( + + ) : null} + + ) +} diff --git a/hooks/useOpenPerpPositions.ts b/hooks/useOpenPerpPositions.ts index ce7485c9..e9f1a6d0 100644 --- a/hooks/useOpenPerpPositions.ts +++ b/hooks/useOpenPerpPositions.ts @@ -7,10 +7,19 @@ const useOpenPerpPositions = () => { const perpPositions = mangoStore((s) => s.mangoAccount.perpPositions) const openPositions = useMemo(() => { - if (!mangoAccountAddress) return [] - return Object.values(perpPositions).filter((p) => - p.basePositionLots.toNumber() - ) + const group = mangoStore.getState().group + if (!mangoAccountAddress || !group) return [] + return Object.values(perpPositions) + .filter((p) => p.basePositionLots.toNumber()) + .sort((a, b) => { + const aMarket = group.getPerpMarketByMarketIndex(a.marketIndex) + const bMarket = group.getPerpMarketByMarketIndex(b.marketIndex) + const aBasePosition = a.getBasePositionUi(aMarket) + const bBasePosition = b.getBasePositionUi(bMarket) + const aNotional = aBasePosition * aMarket._uiPrice + const bNotional = bBasePosition * bMarket._uiPrice + return Math.abs(bNotional) - Math.abs(aNotional) + }) }, [mangoAccountAddress, perpPositions]) return openPositions diff --git a/package.json b/package.json index 08febba8..a6d994c6 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@blockworks-foundation/mango-feeds": "0.1.6", - "@blockworks-foundation/mango-v4": "^0.16.7", + "@blockworks-foundation/mango-v4": "^0.16.9", "@headlessui/react": "1.6.6", "@heroicons/react": "2.0.10", "@metaplex-foundation/js": "0.18.3", diff --git a/public/locales/en/trade.json b/public/locales/en/trade.json index 4a10c408..f6e01aca 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -16,6 +16,7 @@ "copy-and-share": "Copy Image to Clipboard", "current-price": "Current Price", "edit-order": "Edit Order", + "est-liq-price": "Est. Liq. Price", "avg-entry-price": "Avg. Entry Price", "est-slippage": "Est. Slippage", "funding-limits": "Funding Limits", @@ -81,6 +82,7 @@ "tooltip-borrow-balance": "You'll use your {{balance}} {{token}} balance and borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%", "tooltip-borrow-no-balance": "You'll borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%", "tooltip-enable-margin": "Enable spot margin for this trade", + "tooltip-est-liq-price": "Estimated liquidation price is calculated in isolation and does not factor price changes in other markets/tokens.", "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-insured": "Whether or not {{tokenOrMarket}} losses can be recovered from the insurance fund in the event of bankruptcies.", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index 4a10c408..f6e01aca 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -16,6 +16,7 @@ "copy-and-share": "Copy Image to Clipboard", "current-price": "Current Price", "edit-order": "Edit Order", + "est-liq-price": "Est. Liq. Price", "avg-entry-price": "Avg. Entry Price", "est-slippage": "Est. Slippage", "funding-limits": "Funding Limits", @@ -81,6 +82,7 @@ "tooltip-borrow-balance": "You'll use your {{balance}} {{token}} balance and borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%", "tooltip-borrow-no-balance": "You'll borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%", "tooltip-enable-margin": "Enable spot margin for this trade", + "tooltip-est-liq-price": "Estimated liquidation price is calculated in isolation and does not factor price changes in other markets/tokens.", "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-insured": "Whether or not {{tokenOrMarket}} losses can be recovered from the insurance fund in the event of bankruptcies.", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index 4a10c408..f6e01aca 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -16,6 +16,7 @@ "copy-and-share": "Copy Image to Clipboard", "current-price": "Current Price", "edit-order": "Edit Order", + "est-liq-price": "Est. Liq. Price", "avg-entry-price": "Avg. Entry Price", "est-slippage": "Est. Slippage", "funding-limits": "Funding Limits", @@ -81,6 +82,7 @@ "tooltip-borrow-balance": "You'll use your {{balance}} {{token}} balance and borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%", "tooltip-borrow-no-balance": "You'll borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%", "tooltip-enable-margin": "Enable spot margin for this trade", + "tooltip-est-liq-price": "Estimated liquidation price is calculated in isolation and does not factor price changes in other markets/tokens.", "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-insured": "Whether or not {{tokenOrMarket}} losses can be recovered from the insurance fund in the event of bankruptcies.", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", diff --git a/public/locales/zh/trade.json b/public/locales/zh/trade.json index 4a10c408..f6e01aca 100644 --- a/public/locales/zh/trade.json +++ b/public/locales/zh/trade.json @@ -16,6 +16,7 @@ "copy-and-share": "Copy Image to Clipboard", "current-price": "Current Price", "edit-order": "Edit Order", + "est-liq-price": "Est. Liq. Price", "avg-entry-price": "Avg. Entry Price", "est-slippage": "Est. Slippage", "funding-limits": "Funding Limits", @@ -81,6 +82,7 @@ "tooltip-borrow-balance": "You'll use your {{balance}} {{token}} balance and borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%", "tooltip-borrow-no-balance": "You'll borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%", "tooltip-enable-margin": "Enable spot margin for this trade", + "tooltip-est-liq-price": "Estimated liquidation price is calculated in isolation and does not factor price changes in other markets/tokens.", "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-insured": "Whether or not {{tokenOrMarket}} losses can be recovered from the insurance fund in the event of bankruptcies.", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", diff --git a/public/locales/zh_tw/trade.json b/public/locales/zh_tw/trade.json index 4a10c408..f6e01aca 100644 --- a/public/locales/zh_tw/trade.json +++ b/public/locales/zh_tw/trade.json @@ -16,6 +16,7 @@ "copy-and-share": "Copy Image to Clipboard", "current-price": "Current Price", "edit-order": "Edit Order", + "est-liq-price": "Est. Liq. Price", "avg-entry-price": "Avg. Entry Price", "est-slippage": "Est. Slippage", "funding-limits": "Funding Limits", @@ -81,6 +82,7 @@ "tooltip-borrow-balance": "You'll use your {{balance}} {{token}} balance and borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%", "tooltip-borrow-no-balance": "You'll borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%", "tooltip-enable-margin": "Enable spot margin for this trade", + "tooltip-est-liq-price": "Estimated liquidation price is calculated in isolation and does not factor price changes in other markets/tokens.", "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-insured": "Whether or not {{tokenOrMarket}} losses can be recovered from the insurance fund in the event of bankruptcies.", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", diff --git a/styles/globals.css b/styles/globals.css index 5ff9838c..2a25e9ef 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -610,7 +610,7 @@ input[type='range']::-webkit-slider-runnable-track { } .tooltip-underline { - @apply default-transition w-max border-b border-dashed border-current leading-tight hover:cursor-help hover:border-transparent; + @apply default-transition box-border w-max border-b border-dashed border-current leading-tight hover:cursor-help hover:border-transparent; } .radial-gradient-bg { diff --git a/yarn.lock b/yarn.lock index b4d3fc60..68ce9e70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21,10 +21,10 @@ dependencies: ws "^8.13.0" -"@blockworks-foundation/mango-v4@^0.16.7": - version "0.16.7" - resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.16.7.tgz#44e0d06bf62f12fb6ced35ccc67c0b3b6d6da93e" - integrity sha512-ZuspGdg6K0ANzK/C8+HwUDp8T6ZybMFshFVjg/WWYuQp6CY5OFdrdsCFB/Dm1Rf4AkdehB0e+QAmDaMsQ6g2Xg== +"@blockworks-foundation/mango-v4@^0.16.9": + version "0.16.9" + resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.16.9.tgz#85cdc374bc057d5ff37ec80fc097af6743c022ab" + integrity sha512-UYw3lqLWyHwPG4bRB5xff0ozpCpXLsbjqCthwAj7YFQqo1WMrKqLPUwI9wDi09aJWhkmAvdJbzRDy2y7FyevAQ== dependencies: "@coral-xyz/anchor" "^0.27.0" "@project-serum/serum" "0.13.65"