From 597e07608f57c0b88b4479812932f27638b950ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Tue, 20 Jun 2023 23:49:52 +0200 Subject: [PATCH] feat(positions): add liquidation to positions view (#4115) --- .../app/components/markets/markets-table.tsx | 2 +- libs/datagrid/src/lib/type-helpers.ts | 10 +- .../deal-ticket/deal-ticket-fee-details.tsx | 50 +- .../markets-container/use-column-defs.tsx | 15 +- libs/positions/src/lib/liquidation-price.tsx | 60 ++ .../src/lib/positions-data-providers.ts | 3 + .../src/lib/positions-table.spec.tsx | 28 +- libs/positions/src/lib/positions-table.tsx | 661 +++++++++--------- libs/utils/src/lib/format/index.ts | 1 + .../src/lib/format/range.spec.ts} | 7 +- libs/utils/src/lib/format/range.ts | 38 + 11 files changed, 478 insertions(+), 397 deletions(-) create mode 100644 libs/positions/src/lib/liquidation-price.tsx rename libs/{deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.spec.tsx => utils/src/lib/format/range.spec.ts} (93%) create mode 100644 libs/utils/src/lib/format/range.ts diff --git a/apps/explorer/src/app/components/markets/markets-table.tsx b/apps/explorer/src/app/components/markets/markets-table.tsx index 39218f0e5..a28a36161 100644 --- a/apps/explorer/src/app/components/markets/markets-table.tsx +++ b/apps/explorer/src/app/components/markets/markets-table.tsx @@ -77,7 +77,7 @@ export const MarketsTable = ({ data }: MarketsTableProps) => { hide={window.innerWidth <= BREAKPOINT_MD} valueGetter={({ data, - }: VegaValueGetterParams) => { + }: VegaValueGetterParams) => { return data?.state ? MarketStateMapping[data?.state] : '-'; }} /> diff --git a/libs/datagrid/src/lib/type-helpers.ts b/libs/datagrid/src/lib/type-helpers.ts index 494c9b81c..471883ca8 100644 --- a/libs/datagrid/src/lib/type-helpers.ts +++ b/libs/datagrid/src/lib/type-helpers.ts @@ -24,11 +24,13 @@ export type VegaValueFormatterParams = RowHelper< TField >; -export type VegaValueGetterParams = RowHelper< +export type VegaValueGetterParams = Omit< ValueGetterParams, - TRow, - TField ->; + 'data' | 'node' +> & { + data?: TRow; + node: (Omit & { data?: TRow }) | null; +}; export type VegaICellRendererParams = Omit< RowHelper, diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx index df0f9f40e..921513a90 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx @@ -11,11 +11,7 @@ import type { EstimatePositionQuery } from '@vegaprotocol/positions'; import type { EstimateFeesQuery } from '../../hooks/__generated__/EstimateOrder'; import { AccountBreakdownDialog } from '@vegaprotocol/accounts'; -import { - addDecimalsFormatNumber, - isNumeric, - addDecimalsFormatNumberQuantum, -} from '@vegaprotocol/utils'; +import { formatRange, formatValue } from '@vegaprotocol/utils'; import { marketMarginDataProvider } from '@vegaprotocol/accounts'; import { useDataProvider } from '@vegaprotocol/data-provider'; @@ -31,32 +27,6 @@ import { const emptyValue = '-'; -export const formatValue = ( - value: string | number | null | undefined, - formatDecimals: number, - quantum?: string -): string => { - if (!isNumeric(value)) return emptyValue; - if (!quantum) return addDecimalsFormatNumber(value, formatDecimals); - return addDecimalsFormatNumberQuantum(value, formatDecimals, quantum); -}; - -export const formatRange = ( - min: string | number | null | undefined, - max: string | number | null | undefined, - formatDecimals: number, - quantum?: string -) => { - const minFormatted = formatValue(min, formatDecimals, quantum); - const maxFormatted = formatValue(max, formatDecimals, quantum); - if (minFormatted !== maxFormatted) { - return `${minFormatted} - ${maxFormatted}`; - } - if (minFormatted !== emptyValue) { - return minFormatted; - } - return maxFormatted; -}; export interface DealTicketFeeDetailPros { label: string; value?: string | null | undefined; @@ -232,7 +202,6 @@ export const DealTicketFeeDetails = ({ } let liquidationPriceEstimate = emptyValue; - let liquidationPriceEstimateFormatted; if (liquidationEstimate) { const liquidationEstimateBestCaseIncludingBuyOrders = BigInt( @@ -259,17 +228,6 @@ export const DealTicketFeeDetails = ({ ? liquidationEstimateWorstCaseIncludingBuyOrders : liquidationEstimateWorstCaseIncludingSellOrders; liquidationPriceEstimate = formatRange( - (liquidationEstimateBestCase < liquidationEstimateWorstCase - ? liquidationEstimateBestCase - : liquidationEstimateWorstCase - ).toString(), - (liquidationEstimateBestCase > liquidationEstimateWorstCase - ? liquidationEstimateBestCase - : liquidationEstimateWorstCase - ).toString(), - assetDecimals - ); - liquidationPriceEstimateFormatted = formatRange( (liquidationEstimateBestCase < liquidationEstimateWorstCase ? liquidationEstimateBestCase : liquidationEstimateWorstCase @@ -279,7 +237,8 @@ export const DealTicketFeeDetails = ({ : liquidationEstimateWorstCase ).toString(), assetDecimals, - quantum + undefined, + market.decimalPlaces ); } @@ -376,8 +335,7 @@ export const DealTicketFeeDetails = ({ {projectedMargin} diff --git a/libs/markets/src/lib/components/markets-container/use-column-defs.tsx b/libs/markets/src/lib/components/markets-container/use-column-defs.tsx index 0bd0a4bee..669856a7f 100644 --- a/libs/markets/src/lib/components/markets-container/use-column-defs.tsx +++ b/libs/markets/src/lib/components/markets-container/use-column-defs.tsx @@ -76,9 +76,7 @@ export const useColumnDefs = ({ onMarketClick }: Props) => { type: 'rightAligned', cellRenderer: 'PriceFlashCell', filter: 'agNumberColumnFilter', - valueGetter: ({ - data, - }: VegaValueGetterParams) => { + valueGetter: ({ data }: VegaValueGetterParams) => { return data?.data?.bestBidPrice === undefined ? undefined : toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber(); @@ -102,12 +100,7 @@ export const useColumnDefs = ({ onMarketClick }: Props) => { type: 'rightAligned', cellRenderer: 'PriceFlashCell', filter: 'agNumberColumnFilter', - valueGetter: ({ - data, - }: VegaValueGetterParams< - MarketMaybeWithData, - 'data.bestOfferPrice' - >) => { + valueGetter: ({ data }: VegaValueGetterParams) => { return data?.data?.bestOfferPrice === undefined ? undefined : toBigNum( @@ -134,9 +127,7 @@ export const useColumnDefs = ({ onMarketClick }: Props) => { type: 'rightAligned', cellRenderer: 'PriceFlashCell', filter: 'agNumberColumnFilter', - valueGetter: ({ - data, - }: VegaValueGetterParams) => { + valueGetter: ({ data }: VegaValueGetterParams) => { return data?.data?.markPrice === undefined ? undefined : toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber(); diff --git a/libs/positions/src/lib/liquidation-price.tsx b/libs/positions/src/lib/liquidation-price.tsx new file mode 100644 index 000000000..ae08e806b --- /dev/null +++ b/libs/positions/src/lib/liquidation-price.tsx @@ -0,0 +1,60 @@ +import { useEstimatePositionQuery } from './__generated__/Positions'; +import { formatRange } from '@vegaprotocol/utils'; + +export const LiquidationPrice = ({ + marketId, + openVolume, + collateralAvailable, + decimalPlaces, + formatDecimals, +}: { + marketId: string; + openVolume: string; + collateralAvailable: string; + decimalPlaces: number; + formatDecimals: number; +}) => { + const { data: currentData, previousData } = useEstimatePositionQuery({ + variables: { + marketId, + openVolume, + collateralAvailable, + }, + fetchPolicy: 'no-cache', + skip: !openVolume || openVolume === '0', + }); + const data = currentData || previousData; + let value = '-'; + + if (data) { + const bestCase = + data.estimatePosition?.liquidation?.bestCase.open_volume_only.replace( + /\..*/, + '' + ); + const worstCase = + data.estimatePosition?.liquidation?.worstCase.open_volume_only.replace( + /\..*/, + '' + ); + value = + bestCase && worstCase && BigInt(bestCase) < BigInt(worstCase) + ? formatRange( + bestCase, + worstCase, + decimalPlaces, + undefined, + formatDecimals, + value + ) + : formatRange( + worstCase, + bestCase, + decimalPlaces, + undefined, + formatDecimals, + value + ); + } + return {value}; +}; diff --git a/libs/positions/src/lib/positions-data-providers.ts b/libs/positions/src/lib/positions-data-providers.ts index 98be1aa4f..b7685804f 100644 --- a/libs/positions/src/lib/positions-data-providers.ts +++ b/libs/positions/src/lib/positions-data-providers.ts @@ -34,6 +34,7 @@ export interface Position { averageEntryPrice: string; currentLeverage: number | undefined; decimals: number; + quantum: string; lossSocializationAmount: string; marginAccountBalance: string; marketDecimalPlaces: number; @@ -73,6 +74,7 @@ export const getMetrics = ( decimals, id: assetId, symbol: assetSymbol, + quantum, } = market.tradableInstrument.instrument.product.settlementAsset; const generalAccount = accounts?.find( (account) => @@ -114,6 +116,7 @@ export const getMetrics = ( averageEntryPrice: position.averageEntryPrice, currentLeverage: currentLeverage ? currentLeverage.toNumber() : undefined, decimals, + quantum, lossSocializationAmount: position.lossSocializationAmount || '0', marginAccountBalance: marginAccount?.balance ?? '0', marketDecimalPlaces, diff --git a/libs/positions/src/lib/positions-table.spec.tsx b/libs/positions/src/lib/positions-table.spec.tsx index 994ca46c7..be4f2a1d0 100644 --- a/libs/positions/src/lib/positions-table.spec.tsx +++ b/libs/positions/src/lib/positions-table.spec.tsx @@ -7,6 +7,12 @@ import * as Schema from '@vegaprotocol/types'; import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types'; import type { ICellRendererParams } from 'ag-grid-community'; +jest.mock('./liquidation-price', () => ({ + LiquidationPrice: () => ( + liquidation price + ), +})); + const singleRow: Position = { partyId: 'partyId', assetId: 'asset-id', @@ -14,6 +20,7 @@ const singleRow: Position = { averageEntryPrice: '133', currentLeverage: 1.1, decimals: 2, + quantum: '0.1', lossSocializationAmount: '0', marginAccountBalance: '12345600', marketDecimalPlaces: 1, @@ -48,7 +55,7 @@ it('render correct columns', async () => { }); const headers = screen.getAllByRole('columnheader'); - expect(headers).toHaveLength(11); + expect(headers).toHaveLength(12); expect( headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim()) ).toEqual([ @@ -56,6 +63,7 @@ it('render correct columns', async () => { 'Notional', 'Open volume', 'Mark price', + 'Liquidation price', 'Settlement asset', 'Entry price', 'Leverage', @@ -143,12 +151,20 @@ it('displays mark price', async () => { expect(cells[3].textContent).toEqual('-'); }); +it('displays liquidation price', async () => { + await act(async () => { + render(); + }); + const cells = screen.getAllByRole('gridcell'); + expect(cells[4].textContent).toEqual('liquidation price'); +}); + it('displays leverage', async () => { await act(async () => { render(); }); const cells = screen.getAllByRole('gridcell'); - expect(cells[6].textContent).toEqual('1.1'); + expect(cells[7].textContent).toEqual('1.1'); }); it('displays allocated margin', async () => { @@ -156,7 +172,7 @@ it('displays allocated margin', async () => { render(); }); const cells = screen.getAllByRole('gridcell'); - const cell = cells[7]; + const cell = cells[8]; expect(cell.textContent).toEqual('123,456.00'); }); @@ -165,7 +181,7 @@ it('displays realised and unrealised PNL', async () => { render(); }); const cells = screen.getAllByRole('gridcell'); - expect(cells[9].textContent).toEqual('4.56'); + expect(cells[10].textContent).toEqual('4.56'); }); it('displays close button', async () => { @@ -182,7 +198,7 @@ it('displays close button', async () => { ); }); const cells = screen.getAllByRole('gridcell'); - expect(cells[11].textContent).toEqual('Close'); + expect(cells[12].textContent).toEqual('Close'); }); it('do not display close button if openVolume is zero', async () => { @@ -198,7 +214,7 @@ it('do not display close button if openVolume is zero', async () => { ); }); const cells = screen.getAllByRole('gridcell'); - expect(cells[11].textContent).toEqual(''); + expect(cells[12].textContent).toEqual(''); }); describe('PNLCell', () => { diff --git a/libs/positions/src/lib/positions-table.tsx b/libs/positions/src/lib/positions-table.tsx index 0de7c4b38..68d2944b6 100644 --- a/libs/positions/src/lib/positions-table.tsx +++ b/libs/positions/src/lib/positions-table.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; -import { forwardRef } from 'react'; +import { forwardRef, useMemo } from 'react'; import type { CSSProperties, ReactNode } from 'react'; -import type { CellRendererSelectorResult } from 'ag-grid-community'; +import type { ColDef } from 'ag-grid-community'; import type { VegaValueFormatterParams, VegaValueGetterParams, @@ -33,7 +33,6 @@ import { addDecimalsFormatNumber, } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; -import { AgGridColumn } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react'; import type { Position } from './positions-data-providers'; import * as Schema from '@vegaprotocol/types'; @@ -43,6 +42,7 @@ import { DocsLinks } from '@vegaprotocol/environment'; import { PositionTableActions } from './position-actions-dropdown'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; +import { LiquidationPrice } from './liquidation-price'; interface Props extends TypedDataAgGrid { onClose?: (data: Position) => void; @@ -121,333 +121,344 @@ export const PositionsTable = forwardRef( MarketNameCell, }} {...props} - > - {multipleKeys ? ( - ) => - (data?.partyId && - pubKeys && - pubKeys.find((key) => key.publicKey === data.partyId)?.name) || - data?.partyId - } - minWidth={190} - /> - ) : null} - - ) => { - return !data?.notional - ? undefined - : toBigNum(data.notional, data.marketDecimalPlaces).toNumber(); - }} - valueFormatter={({ - data, - }: VegaValueFormatterParams) => { - return !data || !data.notional - ? '-' - : addDecimalsFormatNumber( - data.notional, + columnDefs={useMemo(() => { + const columnDefs: (ColDef | null)[] = [ + multipleKeys + ? { + headerName: t('Vega key'), + field: 'partyId', + valueGetter: ({ data }: VegaValueGetterParams) => + (data?.partyId && + pubKeys && + pubKeys.find((key) => key.publicKey === data.partyId) + ?.name) || + data?.partyId, + minWidth: 190, + } + : null, + { + headerName: t('Market'), + field: 'marketName', + cellRenderer: 'MarketNameCell', + cellRendererParams: { idPath: 'marketId', onMarketClick }, + minWidth: 190, + }, + { + headerName: t('Notional'), + headerTooltip: t('Mark price x open volume.'), + field: 'notional', + type: 'rightAligned', + cellClass: 'font-mono text-right', + filter: 'agNumberColumnFilter', + valueGetter: ({ data }: VegaValueGetterParams) => { + return !data?.notional + ? undefined + : toBigNum( + data.notional, + data.marketDecimalPlaces + ).toNumber(); + }, + valueFormatter: ({ + data, + }: VegaValueFormatterParams) => { + return !data || !data.notional + ? '-' + : addDecimalsFormatNumber( + data.notional, + data.marketDecimalPlaces + ); + }, + minWidth: 80, + }, + { + headerName: t('Open volume'), + field: 'openVolume', + type: 'rightAligned', + cellClass: 'font-mono text-right', + cellClassRules: signedNumberCssClassRules, + filter: 'agNumberColumnFilter', + valueGetter: ({ data }: VegaValueGetterParams) => { + return data?.openVolume === undefined + ? undefined + : toBigNum( + data?.openVolume, + data.positionDecimalPlaces + ).toNumber(); + }, + valueFormatter: ({ + data, + }: VegaValueFormatterParams): string => { + return data?.openVolume === undefined + ? '' + : volumePrefix( + addDecimalsFormatNumber( + data.openVolume, + data.positionDecimalPlaces + ) + ); + }, + cellRenderer: OpenVolumeCell, + minWidth: 100, + }, + { + headerName: t('Mark price'), + field: 'markPrice', + type: 'rightAligned', + cellRenderer: PriceFlashCell, + filter: 'agNumberColumnFilter', + valueGetter: ({ data }: VegaValueGetterParams) => { + return !data || + !data.markPrice || + data.marketTradingMode === + Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION + ? undefined + : toBigNum( + data.markPrice, + data.marketDecimalPlaces + ).toNumber(); + }, + valueFormatter: ({ + data, + }: VegaValueFormatterParams) => { + if (!data) { + return ''; + } + if ( + !data.markPrice || + data.marketTradingMode === + Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION + ) { + return '-'; + } + return addDecimalsFormatNumber( + data.markPrice, data.marketDecimalPlaces ); - }} - minWidth={80} - /> - ) => { - return data?.openVolume === undefined - ? undefined - : toBigNum( - data?.openVolume, - data.positionDecimalPlaces - ).toNumber(); - }} - valueFormatter={({ - data, - }: VegaValueFormatterParams): - | string - | undefined => { - return data?.openVolume === undefined - ? undefined - : volumePrefix( - addDecimalsFormatNumber( - data.openVolume, - data.positionDecimalPlaces - ) + }, + minWidth: 100, + }, + { + headerName: t('Liquidation price'), + colId: 'liquidationPrice', + type: 'rightAligned', + cellRenderer: ({ data }: VegaICellRendererParams) => { + if (!data) return null; + return ( + ); - }} - cellRenderer={OpenVolumeCell} - minWidth={100} - /> - { - return { - component: PriceFlashCell, - }; - }} - filter="agNumberColumnFilter" - valueGetter={({ - data, - }: VegaValueGetterParams) => { - return !data || - !data.markPrice || - data.marketTradingMode === - Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION - ? undefined - : toBigNum(data.markPrice, data.marketDecimalPlaces).toNumber(); - }} - valueFormatter={({ - data, - }: VegaValueFormatterParams) => { - if (!data) { - return undefined; - } - if ( - !data.markPrice || - data.marketTradingMode === - Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION - ) { - return '-'; - } - return addDecimalsFormatNumber( - data.markPrice, - data.marketDecimalPlaces - ); - }} - minWidth={100} - /> - ) => { - if (!data) return null; - return ( - { - openAssetDetailsDialog(data.assetId, e.target as HTMLElement); - }} - > - {data?.assetSymbol} - - ); - }} - /> - { - return { - component: PriceFlashCell, - }; - }} - filter="agNumberColumnFilter" - valueGetter={({ - data, - }: VegaValueGetterParams) => { - return data?.markPrice === undefined || !data - ? undefined - : toBigNum( + }, + }, + { + headerName: t('Settlement asset'), + field: 'assetSymbol', + colId: 'asset', + minWidth: 100, + cellRenderer: ({ data }: VegaICellRendererParams) => { + if (!data) return null; + return ( + { + openAssetDetailsDialog( + data.assetId, + e.target as HTMLElement + ); + }} + > + {data?.assetSymbol} + + ); + }, + }, + { + headerName: t('Entry price'), + field: 'averageEntryPrice', + type: 'rightAligned', + cellRenderer: PriceFlashCell, + filter: 'agNumberColumnFilter', + valueGetter: ({ data }: VegaValueGetterParams) => { + return data?.markPrice === undefined || !data + ? undefined + : toBigNum( + data.averageEntryPrice, + data.marketDecimalPlaces + ).toNumber(); + }, + valueFormatter: ({ + data, + }: VegaValueFormatterParams< + Position, + 'averageEntryPrice' + >): string => { + if (!data) { + return ''; + } + return addDecimalsFormatNumber( data.averageEntryPrice, data.marketDecimalPlaces - ).toNumber(); - }} - valueFormatter={({ - data, - }: VegaValueFormatterParams): - | string - | undefined => { - if (!data) { - return undefined; - } - return addDecimalsFormatNumber( - data.averageEntryPrice, - data.marketDecimalPlaces - ); - }} - minWidth={100} - /> - {multipleKeys ? null : ( - { - return { - component: PriceFlashCell, - }; - }} - valueFormatter={({ - value, - }: VegaValueFormatterParams) => - value === undefined - ? undefined - : formatNumber(value.toString(), 1) - } - minWidth={100} - /> - )} - {multipleKeys ? null : ( - { - return { - component: PriceFlashCell, - }; - }} - valueGetter={({ - data, - }: VegaValueGetterParams) => { - return !data - ? undefined - : toBigNum(data.marginAccountBalance, data.decimals).toNumber(); - }} - valueFormatter={({ - data, - }: VegaValueFormatterParams): - | string - | undefined => { - if (!data) { - return undefined; - } - return addDecimalsFormatNumber( - data.marginAccountBalance, - data.decimals - ); - }} - minWidth={100} - /> - )} - ) => { - return !data - ? undefined - : toBigNum(data.realisedPNL, data.decimals).toNumber(); - }} - valueFormatter={({ - data, - }: VegaValueFormatterParams) => { - return !data - ? undefined - : addDecimalsFormatNumber(data.realisedPNL, data.decimals); - }} - headerTooltip={t( - 'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.' - )} - cellRenderer={PNLCell} - minWidth={100} - /> - ) => { - return !data - ? undefined - : toBigNum(data.unrealisedPNL, data.decimals).toNumber(); - }} - valueFormatter={({ - data, - }: VegaValueFormatterParams) => - !data - ? undefined - : addDecimalsFormatNumber(data.unrealisedPNL, data.decimals) - } - headerTooltip={t( - 'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.' - )} - cellRenderer={PNLCell} - minWidth={100} - /> - ) => { - if (!value) { - return value; - } - return getDateTimeFormat().format(new Date(value)); - }} - minWidth={150} - /> - {onClose && !isReadOnly ? ( - ) => { - return ( -
- {data?.openVolume && - data?.openVolume !== '0' && - data.partyId === pubKey ? ( - data && onClose(data)} - > - {t('Close')} - - ) : null} - {data?.assetId && ( - - )} -
- ); - }} - minWidth={90} - maxWidth={90} - /> - ) : null} - + ); + }, + minWidth: 100, + }, + multipleKeys + ? null + : { + headerName: t('Leverage'), + field: 'currentLeverage', + type: 'rightAligned', + filter: 'agNumberColumnFilter', + cellRenderer: PriceFlashCell, + valueFormatter: ({ + value, + }: VegaValueFormatterParams) => + value === undefined + ? '' + : formatNumber(value.toString(), 1), + minWidth: 100, + }, + multipleKeys + ? null + : { + headerName: t('Margin allocated'), + field: 'marginAccountBalance', + type: 'rightAligned', + filter: 'agNumberColumnFilter', + cellRenderer: PriceFlashCell, + valueGetter: ({ data }: VegaValueGetterParams) => { + return !data + ? undefined + : toBigNum( + data.marginAccountBalance, + data.decimals + ).toNumber(); + }, + valueFormatter: ({ + data, + }: VegaValueFormatterParams< + Position, + 'marginAccountBalance' + >): string => { + if (!data) { + return ''; + } + return addDecimalsFormatNumber( + data.marginAccountBalance, + data.decimals + ); + }, + minWidth: 100, + }, + { + headerName: t('Realised PNL'), + field: 'realisedPNL', + type: 'rightAligned', + cellClassRules: signedNumberCssClassRules, + cellClass: 'font-mono text-right', + filter: 'agNumberColumnFilter', + valueGetter: ({ data }: VegaValueGetterParams) => { + return !data + ? undefined + : toBigNum(data.realisedPNL, data.decimals).toNumber(); + }, + valueFormatter: ({ + data, + }: VegaValueFormatterParams) => { + return !data + ? '' + : addDecimalsFormatNumber(data.realisedPNL, data.decimals); + }, + headerTooltip: t( + 'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.' + ), + cellRenderer: PNLCell, + minWidth: 100, + }, + { + headerName: t('Unrealised PNL'), + field: 'unrealisedPNL', + type: 'rightAligned', + cellClassRules: signedNumberCssClassRules, + cellClass: 'font-mono text-right', + filter: 'agNumberColumnFilter', + valueGetter: ({ data }: VegaValueGetterParams) => { + return !data + ? undefined + : toBigNum(data.unrealisedPNL, data.decimals).toNumber(); + }, + valueFormatter: ({ + data, + }: VegaValueFormatterParams) => + !data + ? '' + : addDecimalsFormatNumber(data.unrealisedPNL, data.decimals), + headerTooltip: t( + 'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.' + ), + cellRenderer: PNLCell, + minWidth: 100, + }, + { + headerName: t('Updated'), + field: 'updatedAt', + type: 'rightAligned', + filter: DateRangeFilter, + valueFormatter: ({ + value, + }: VegaValueFormatterParams) => { + if (!value) { + return ''; + } + return getDateTimeFormat().format(new Date(value)); + }, + minWidth: 150, + }, + onClose && !isReadOnly + ? { + ...COL_DEFS.actions, + cellRenderer: ({ + data, + }: VegaICellRendererParams) => { + return ( +
+ {data?.openVolume && + data?.openVolume !== '0' && + data.partyId === pubKey ? ( + data && onClose(data)} + > + {t('Close')} + + ) : null} + {data?.assetId && ( + + )} +
+ ); + }, + minWidth: 90, + maxWidth: 90, + } + : null, + ]; + return columnDefs.filter( + (colDef: ColDef | null): colDef is ColDef => colDef !== null + ); + }, [ + isReadOnly, + multipleKeys, + onClose, + onMarketClick, + openAssetDetailsDialog, + pubKey, + pubKeys, + ])} + /> ); } ); diff --git a/libs/utils/src/lib/format/index.ts b/libs/utils/src/lib/format/index.ts index fefb8320f..448b06347 100644 --- a/libs/utils/src/lib/format/index.ts +++ b/libs/utils/src/lib/format/index.ts @@ -1,4 +1,5 @@ export * from './date'; export * from './number'; +export * from './range'; export * from './size'; export * from './strings'; diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.spec.tsx b/libs/utils/src/lib/format/range.spec.ts similarity index 93% rename from libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.spec.tsx rename to libs/utils/src/lib/format/range.spec.ts index 871b12c3e..6f59db248 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.spec.tsx +++ b/libs/utils/src/lib/format/range.spec.ts @@ -1,6 +1,6 @@ -import { formatRange, formatValue } from './deal-ticket-fee-details'; +import { formatRange, formatValue } from './range'; -describe('formatRange, formatValue', () => { +describe('formatValue', () => { it.each([ { v: 123000, d: 5, o: '1.23' }, { v: 123000, d: 3, o: '123.00' }, @@ -35,7 +35,8 @@ describe('formatRange, formatValue', () => { expect(formatValue(v.toString(), d, q)).toStrictEqual(o); } ); - +}); +describe('formatRange', () => { it.each([ { min: 123000, max: 12300011111, d: 5, o: '1.23 - 123,000.111', q: '0.1' }, { diff --git a/libs/utils/src/lib/format/range.ts b/libs/utils/src/lib/format/range.ts new file mode 100644 index 000000000..d5f466aef --- /dev/null +++ b/libs/utils/src/lib/format/range.ts @@ -0,0 +1,38 @@ +import { + addDecimalsFormatNumber, + addDecimalsFormatNumberQuantum, + isNumeric, +} from './number'; + +export const formatValue = ( + value: string | number | null | undefined, + decimalPlaces: number, + quantum?: string, + formatDecimals?: number, + emptyValue = '-' +): string => { + if (!isNumeric(value)) return emptyValue; + if (!quantum) { + return addDecimalsFormatNumber(value, decimalPlaces, formatDecimals); + } + return addDecimalsFormatNumberQuantum(value, decimalPlaces, quantum); +}; + +export const formatRange = ( + min: string | number | null | undefined, + max: string | number | null | undefined, + decimalPlaces: number, + quantum?: string, + formatDecimals?: number, + emptyValue = '-' +) => { + const minFormatted = formatValue(min, decimalPlaces, quantum, formatDecimals); + const maxFormatted = formatValue(max, decimalPlaces, quantum, formatDecimals); + if (minFormatted !== maxFormatted) { + return `${minFormatted} - ${maxFormatted}`; + } + if (minFormatted !== emptyValue) { + return minFormatted; + } + return maxFormatted; +};