From b76df3c87cbe0b860676b666efc16985bab21190 Mon Sep 17 00:00:00 2001 From: saml33 Date: Tue, 27 Jun 2023 12:43:15 +1000 Subject: [PATCH 1/6] new layout and add liq price --- components/shared/FormatNumericValue.tsx | 4 +- components/trade/PerpPositions.tsx | 443 ++++++++++++++++------- components/trade/TableMarketName.tsx | 58 ++- public/locales/en/trade.json | 1 + public/locales/es/trade.json | 1 + public/locales/ru/trade.json | 1 + public/locales/zh/trade.json | 1 + public/locales/zh_tw/trade.json | 1 + styles/globals.css | 2 +- 9 files changed, 370 insertions(+), 142 deletions(-) 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 09dedc8a..e5635ea7 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,21 +19,20 @@ 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, formatCurrencyValue, + formatNumericValue, getDecimalCount, } from 'utils/numbers' 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' const PerpPositions = () => { const { t } = useTranslation(['common', 'trade']) @@ -44,11 +48,10 @@ const PerpPositions = () => { const perpPositions = mangoStore((s) => s.mangoAccount.perpPositions) const { selectedMarket } = useSelectedMarket() const { connected } = useWallet() - const { mangoAccountAddress } = useMangoAccount() + const { mangoAccount, mangoAccountAddress } = 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 @@ -92,7 +95,7 @@ const PerpPositions = () => { setShowShareModal(true) } - if (!group) return null + if (!group || !mangoAccount) return null const openPerpPositions = Object.values(perpPositions) .filter((p) => p.basePositionLots.toNumber()) @@ -116,11 +119,9 @@ const PerpPositions = () => { {t('market')} - {t('trade:side')} {t('trade:size')} - {t('trade:notional')} {t('trade:entry-price')} - {t('trade:oracle-price')} + {t('trade:est-liq-price')} {`${t('trade:unsettled')} ${t( 'pnl' )}`} @@ -144,11 +145,17 @@ const PerpPositions = () => { if (!basePosition) return null + const isLong = basePosition > 0 const unsettledPnl = position.getUnsettledPnlUi(market) const totalPnl = position.cumulativePnlOverPositionLifetimeUi(market) const unrealizedPnl = position.getUnRealizedPnlUi(market) const realizedPnl = position.getRealizedPnlUi() + const roe = unrealizedPnl / basePosition + const estLiqPrice = position.getLiquidationPriceUi( + group, + mangoAccount + ) return ( { className="my-1 p-2" > - - - - + -

- {isSelectedMarket ? ( + {isSelectedMarket ? ( +

@@ -177,35 +184,55 @@ const PerpPositions = () => { )} /> - ) : ( + +
+ ) : ( +
- )} -

+ +
+ )} + + +
+ + +
- - - - - - - + {estLiqPrice ? ( + + ) : ( + '–' + )} { unrealizedPnl={unrealizedPnl} realizedPnl={realizedPnl} totalPnl={totalPnl} + roe={roe} /> } delay={100} > 0 ? 'text-th-up' : 'text-th-down' @@ -278,8 +306,8 @@ const PerpPositions = () => { ) : null} ) : ( - <> - {openPerpPositions.map((position) => { +
+ {openPerpPositions.map((position, i) => { const market = group.getPerpMarketByMarketIndex( position.marketIndex ) @@ -293,109 +321,250 @@ const PerpPositions = () => { selectedMarket.perpMarketIndex === position.marketIndex if (!basePosition) return null + const side = + basePosition > 0 ? 'buy' : basePosition < 0 ? 'sell' : '' const totalPnl = position.cumulativePnlOverPositionLifetimeUi(market) const unrealizedPnl = position.getUnRealizedPnlUi(market) const realizedPnl = position.getRealizedPnlUi() + const roe = unrealizedPnl / basePosition + 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:entry-price')} +

+
+ + +
+
+
+

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

+

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

+
+
+

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

+

+ +

+
+
+

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

+

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

+
+
+ + +
+
+
+ + + )} + ) })} - +
) ) : mangoAccountAddress || connected ? (
@@ -425,10 +594,12 @@ const PnlTooltipContent = ({ unrealizedPnl, realizedPnl, totalPnl, + roe, }: { unrealizedPnl: number realizedPnl: number totalPnl: number + roe: number }) => { const { t } = useTranslation(['common', 'trade']) return ( @@ -452,6 +623,12 @@ const PnlTooltipContent = ({ {formatCurrencyValue(totalPnl, 2)}
+
+

ROE

+ + {formatNumericValue(roe, 2)}% + +
{ +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/public/locales/en/trade.json b/public/locales/en/trade.json index b397f8e9..acd78983 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -17,6 +17,7 @@ "current-price": "Current Price", "edit-order": "Edit Order", "entry-price": "Entry Price", + "est-liq-price": "Est. Liq. Price", "est-slippage": "Est. Slippage", "funding-limits": "Funding Limits", "funding-rate": "1h Avg Funding Rate", diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index b397f8e9..acd78983 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -17,6 +17,7 @@ "current-price": "Current Price", "edit-order": "Edit Order", "entry-price": "Entry Price", + "est-liq-price": "Est. Liq. Price", "est-slippage": "Est. Slippage", "funding-limits": "Funding Limits", "funding-rate": "1h Avg Funding Rate", diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index b397f8e9..acd78983 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -17,6 +17,7 @@ "current-price": "Current Price", "edit-order": "Edit Order", "entry-price": "Entry Price", + "est-liq-price": "Est. Liq. Price", "est-slippage": "Est. Slippage", "funding-limits": "Funding Limits", "funding-rate": "1h Avg Funding Rate", diff --git a/public/locales/zh/trade.json b/public/locales/zh/trade.json index b397f8e9..acd78983 100644 --- a/public/locales/zh/trade.json +++ b/public/locales/zh/trade.json @@ -17,6 +17,7 @@ "current-price": "Current Price", "edit-order": "Edit Order", "entry-price": "Entry Price", + "est-liq-price": "Est. Liq. Price", "est-slippage": "Est. Slippage", "funding-limits": "Funding Limits", "funding-rate": "1h Avg Funding Rate", diff --git a/public/locales/zh_tw/trade.json b/public/locales/zh_tw/trade.json index b397f8e9..acd78983 100644 --- a/public/locales/zh_tw/trade.json +++ b/public/locales/zh_tw/trade.json @@ -17,6 +17,7 @@ "current-price": "Current Price", "edit-order": "Edit Order", "entry-price": "Entry Price", + "est-liq-price": "Est. Liq. Price", "est-slippage": "Est. Slippage", "funding-limits": "Funding Limits", "funding-rate": "1h Avg Funding Rate", 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 { From febb6a7ad35bad132ddef89bd485c4eb7d689be0 Mon Sep 17 00:00:00 2001 From: saml33 Date: Wed, 28 Jun 2023 12:10:47 +1000 Subject: [PATCH 2/6] calc liq price outside of the positions table --- components/trade/PerpPositions.tsx | 159 ++++++++++++++++------------- hooks/useEstLiqPrice.ts | 53 ++++++++++ hooks/useOpenPerpPositions.ts | 17 ++- public/locales/en/trade.json | 1 + public/locales/es/trade.json | 1 + public/locales/ru/trade.json | 1 + public/locales/zh/trade.json | 1 + public/locales/zh_tw/trade.json | 1 + 8 files changed, 160 insertions(+), 74 deletions(-) create mode 100644 hooks/useEstLiqPrice.ts diff --git a/components/trade/PerpPositions.tsx b/components/trade/PerpPositions.tsx index e5635ea7..1ed6a9a4 100644 --- a/components/trade/PerpPositions.tsx +++ b/components/trade/PerpPositions.tsx @@ -23,7 +23,6 @@ import { useCallback, useState } from 'react' import { floorToDecimal, formatCurrencyValue, - formatNumericValue, getDecimalCount, } from 'utils/numbers' import { breakpoints } from 'utils/theme' @@ -33,6 +32,8 @@ 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 useEstLiqPrice from 'hooks/useEstLiqPrice' const PerpPositions = () => { const { t } = useTranslation(['common', 'trade']) @@ -45,10 +46,11 @@ const PerpPositions = () => { const [positionToShare, setPositionToShare] = useState( null ) - const perpPositions = mangoStore((s) => s.mangoAccount.perpPositions) + const openPerpPositions = useOpenPerpPositions() + const liqPrices = useEstLiqPrice() const { selectedMarket } = useSelectedMarket() const { connected } = useWallet() - const { mangoAccount, mangoAccountAddress } = useMangoAccount() + const { mangoAccountAddress } = useMangoAccount() const { isUnownedAccount } = useUnownedAccount() const { width } = useViewport() const showTableView = width ? width > breakpoints.md : false @@ -95,19 +97,7 @@ const PerpPositions = () => { setShowShareModal(true) } - if (!group || !mangoAccount) 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) - }) + if (!group) return null return ( <> @@ -121,10 +111,15 @@ const PerpPositions = () => { {t('market')} {t('trade:size')} {t('trade:entry-price')} - {t('trade:est-liq-price')} - {`${t('trade:unsettled')} ${t( - 'pnl' - )}`} + +
+ + + {t('trade:est-liq-price')} + + +
+ {t('trade:unrealized-pnl')} {!isUnownedAccount ? : null} @@ -152,10 +147,9 @@ const PerpPositions = () => { const unrealizedPnl = position.getUnRealizedPnlUi(market) const realizedPnl = position.getRealizedPnlUi() const roe = unrealizedPnl / basePosition - const estLiqPrice = position.getLiquidationPriceUi( - group, - mangoAccount - ) + const estLiqPrice = liqPrices.find( + (p) => p.marketIndex === position.marketIndex + )?.liqPrice return ( { '–' )} - - - - - } - delay={100} - > +
+ + } + 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 ? ( @@ -328,10 +332,9 @@ const PerpPositions = () => { const unrealizedPnl = position.getUnRealizedPnlUi(market) const realizedPnl = position.getRealizedPnlUi() const roe = unrealizedPnl / basePosition - const estLiqPrice = position.getLiquidationPriceUi( - group, - mangoAccount - ) + const estLiqPrice = liqPrices.find( + (p) => p.marketIndex === position.marketIndex + )?.liqPrice const unsettledPnl = position.getUnsettledPnlUi(market) return ( @@ -475,9 +478,13 @@ const PerpPositions = () => {
-

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

+ +

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

+

{estLiqPrice ? ( {

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

{ unrealizedPnl={unrealizedPnl} realizedPnl={realizedPnl} totalPnl={totalPnl} - roe={roe} + unsettledPnl={unsettledPnl} /> } delay={100} > 0 + unrealizedPnl >= 0 ? 'text-th-up' : 'text-th-down' }`} @@ -534,6 +541,16 @@ const PerpPositions = () => {

+
+

ROE

+

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

+