feat(positions): add liquidation to positions view (#4115)

This commit is contained in:
Bartłomiej Głownia 2023-06-20 23:49:52 +02:00 committed by GitHub
parent e451dc54b3
commit 597e07608f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 478 additions and 397 deletions

View File

@ -77,7 +77,7 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
hide={window.innerWidth <= BREAKPOINT_MD} hide={window.innerWidth <= BREAKPOINT_MD}
valueGetter={({ valueGetter={({
data, data,
}: VegaValueGetterParams<MarketFieldsFragment, 'state'>) => { }: VegaValueGetterParams<MarketFieldsFragment>) => {
return data?.state ? MarketStateMapping[data?.state] : '-'; return data?.state ? MarketStateMapping[data?.state] : '-';
}} }}
/> />

View File

@ -24,11 +24,13 @@ export type VegaValueFormatterParams<TRow, TField extends Field> = RowHelper<
TField TField
>; >;
export type VegaValueGetterParams<TRow, TField extends Field> = RowHelper< export type VegaValueGetterParams<TRow> = Omit<
ValueGetterParams, ValueGetterParams,
TRow, 'data' | 'node'
TField > & {
>; data?: TRow;
node: (Omit<RowNode, 'data'> & { data?: TRow }) | null;
};
export type VegaICellRendererParams<TRow, TField extends Field = string> = Omit< export type VegaICellRendererParams<TRow, TField extends Field = string> = Omit<
RowHelper<ICellRendererParams, TRow, TField>, RowHelper<ICellRendererParams, TRow, TField>,

View File

@ -11,11 +11,7 @@ import type { EstimatePositionQuery } from '@vegaprotocol/positions';
import type { EstimateFeesQuery } from '../../hooks/__generated__/EstimateOrder'; import type { EstimateFeesQuery } from '../../hooks/__generated__/EstimateOrder';
import { AccountBreakdownDialog } from '@vegaprotocol/accounts'; import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
import { import { formatRange, formatValue } from '@vegaprotocol/utils';
addDecimalsFormatNumber,
isNumeric,
addDecimalsFormatNumberQuantum,
} from '@vegaprotocol/utils';
import { marketMarginDataProvider } from '@vegaprotocol/accounts'; import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
@ -31,32 +27,6 @@ import {
const emptyValue = '-'; 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 { export interface DealTicketFeeDetailPros {
label: string; label: string;
value?: string | null | undefined; value?: string | null | undefined;
@ -232,7 +202,6 @@ export const DealTicketFeeDetails = ({
} }
let liquidationPriceEstimate = emptyValue; let liquidationPriceEstimate = emptyValue;
let liquidationPriceEstimateFormatted;
if (liquidationEstimate) { if (liquidationEstimate) {
const liquidationEstimateBestCaseIncludingBuyOrders = BigInt( const liquidationEstimateBestCaseIncludingBuyOrders = BigInt(
@ -259,17 +228,6 @@ export const DealTicketFeeDetails = ({
? liquidationEstimateWorstCaseIncludingBuyOrders ? liquidationEstimateWorstCaseIncludingBuyOrders
: liquidationEstimateWorstCaseIncludingSellOrders; : liquidationEstimateWorstCaseIncludingSellOrders;
liquidationPriceEstimate = formatRange( liquidationPriceEstimate = formatRange(
(liquidationEstimateBestCase < liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
(liquidationEstimateBestCase > liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
assetDecimals
);
liquidationPriceEstimateFormatted = formatRange(
(liquidationEstimateBestCase < liquidationEstimateWorstCase (liquidationEstimateBestCase < liquidationEstimateWorstCase
? liquidationEstimateBestCase ? liquidationEstimateBestCase
: liquidationEstimateWorstCase : liquidationEstimateWorstCase
@ -279,7 +237,8 @@ export const DealTicketFeeDetails = ({
: liquidationEstimateWorstCase : liquidationEstimateWorstCase
).toString(), ).toString(),
assetDecimals, assetDecimals,
quantum undefined,
market.decimalPlaces
); );
} }
@ -376,8 +335,7 @@ export const DealTicketFeeDetails = ({
{projectedMargin} {projectedMargin}
<DealTicketFeeDetail <DealTicketFeeDetail
label={t('Liquidation price estimate')} label={t('Liquidation price estimate')}
value={liquidationPriceEstimate} formattedValue={liquidationPriceEstimate}
formattedValue={liquidationPriceEstimateFormatted}
symbol={assetSymbol} symbol={assetSymbol}
labelDescription={LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT} labelDescription={LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT}
/> />

View File

@ -76,9 +76,7 @@ export const useColumnDefs = ({ onMarketClick }: Props) => {
type: 'rightAligned', type: 'rightAligned',
cellRenderer: 'PriceFlashCell', cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter', filter: 'agNumberColumnFilter',
valueGetter: ({ valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.bestBidPrice'>) => {
return data?.data?.bestBidPrice === undefined return data?.data?.bestBidPrice === undefined
? undefined ? undefined
: toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber(); : toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber();
@ -102,12 +100,7 @@ export const useColumnDefs = ({ onMarketClick }: Props) => {
type: 'rightAligned', type: 'rightAligned',
cellRenderer: 'PriceFlashCell', cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter', filter: 'agNumberColumnFilter',
valueGetter: ({ valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
data,
}: VegaValueGetterParams<
MarketMaybeWithData,
'data.bestOfferPrice'
>) => {
return data?.data?.bestOfferPrice === undefined return data?.data?.bestOfferPrice === undefined
? undefined ? undefined
: toBigNum( : toBigNum(
@ -134,9 +127,7 @@ export const useColumnDefs = ({ onMarketClick }: Props) => {
type: 'rightAligned', type: 'rightAligned',
cellRenderer: 'PriceFlashCell', cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter', filter: 'agNumberColumnFilter',
valueGetter: ({ valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.markPrice'>) => {
return data?.data?.markPrice === undefined return data?.data?.markPrice === undefined
? undefined ? undefined
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber(); : toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();

View File

@ -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 <span data-testid="liquidation-price">{value}</span>;
};

View File

@ -34,6 +34,7 @@ export interface Position {
averageEntryPrice: string; averageEntryPrice: string;
currentLeverage: number | undefined; currentLeverage: number | undefined;
decimals: number; decimals: number;
quantum: string;
lossSocializationAmount: string; lossSocializationAmount: string;
marginAccountBalance: string; marginAccountBalance: string;
marketDecimalPlaces: number; marketDecimalPlaces: number;
@ -73,6 +74,7 @@ export const getMetrics = (
decimals, decimals,
id: assetId, id: assetId,
symbol: assetSymbol, symbol: assetSymbol,
quantum,
} = market.tradableInstrument.instrument.product.settlementAsset; } = market.tradableInstrument.instrument.product.settlementAsset;
const generalAccount = accounts?.find( const generalAccount = accounts?.find(
(account) => (account) =>
@ -114,6 +116,7 @@ export const getMetrics = (
averageEntryPrice: position.averageEntryPrice, averageEntryPrice: position.averageEntryPrice,
currentLeverage: currentLeverage ? currentLeverage.toNumber() : undefined, currentLeverage: currentLeverage ? currentLeverage.toNumber() : undefined,
decimals, decimals,
quantum,
lossSocializationAmount: position.lossSocializationAmount || '0', lossSocializationAmount: position.lossSocializationAmount || '0',
marginAccountBalance: marginAccount?.balance ?? '0', marginAccountBalance: marginAccount?.balance ?? '0',
marketDecimalPlaces, marketDecimalPlaces,

View File

@ -7,6 +7,12 @@ import * as Schema from '@vegaprotocol/types';
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types'; import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import type { ICellRendererParams } from 'ag-grid-community'; import type { ICellRendererParams } from 'ag-grid-community';
jest.mock('./liquidation-price', () => ({
LiquidationPrice: () => (
<span data-testid="liquidation-price">liquidation price</span>
),
}));
const singleRow: Position = { const singleRow: Position = {
partyId: 'partyId', partyId: 'partyId',
assetId: 'asset-id', assetId: 'asset-id',
@ -14,6 +20,7 @@ const singleRow: Position = {
averageEntryPrice: '133', averageEntryPrice: '133',
currentLeverage: 1.1, currentLeverage: 1.1,
decimals: 2, decimals: 2,
quantum: '0.1',
lossSocializationAmount: '0', lossSocializationAmount: '0',
marginAccountBalance: '12345600', marginAccountBalance: '12345600',
marketDecimalPlaces: 1, marketDecimalPlaces: 1,
@ -48,7 +55,7 @@ it('render correct columns', async () => {
}); });
const headers = screen.getAllByRole('columnheader'); const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(11); expect(headers).toHaveLength(12);
expect( expect(
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim()) headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual([ ).toEqual([
@ -56,6 +63,7 @@ it('render correct columns', async () => {
'Notional', 'Notional',
'Open volume', 'Open volume',
'Mark price', 'Mark price',
'Liquidation price',
'Settlement asset', 'Settlement asset',
'Entry price', 'Entry price',
'Leverage', 'Leverage',
@ -143,12 +151,20 @@ it('displays mark price', async () => {
expect(cells[3].textContent).toEqual('-'); expect(cells[3].textContent).toEqual('-');
}); });
it('displays liquidation price', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[4].textContent).toEqual('liquidation price');
});
it('displays leverage', async () => { it('displays leverage', async () => {
await act(async () => { await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />); render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
}); });
const cells = screen.getAllByRole('gridcell'); const cells = screen.getAllByRole('gridcell');
expect(cells[6].textContent).toEqual('1.1'); expect(cells[7].textContent).toEqual('1.1');
}); });
it('displays allocated margin', async () => { it('displays allocated margin', async () => {
@ -156,7 +172,7 @@ it('displays allocated margin', async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />); render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
}); });
const cells = screen.getAllByRole('gridcell'); const cells = screen.getAllByRole('gridcell');
const cell = cells[7]; const cell = cells[8];
expect(cell.textContent).toEqual('123,456.00'); expect(cell.textContent).toEqual('123,456.00');
}); });
@ -165,7 +181,7 @@ it('displays realised and unrealised PNL', async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />); render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
}); });
const cells = screen.getAllByRole('gridcell'); const cells = screen.getAllByRole('gridcell');
expect(cells[9].textContent).toEqual('4.56'); expect(cells[10].textContent).toEqual('4.56');
}); });
it('displays close button', async () => { it('displays close button', async () => {
@ -182,7 +198,7 @@ it('displays close button', async () => {
); );
}); });
const cells = screen.getAllByRole('gridcell'); 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 () => { 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'); const cells = screen.getAllByRole('gridcell');
expect(cells[11].textContent).toEqual(''); expect(cells[12].textContent).toEqual('');
}); });
describe('PNLCell', () => { describe('PNLCell', () => {

View File

@ -1,7 +1,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { forwardRef } from 'react'; import { forwardRef, useMemo } from 'react';
import type { CSSProperties, ReactNode } from 'react'; import type { CSSProperties, ReactNode } from 'react';
import type { CellRendererSelectorResult } from 'ag-grid-community'; import type { ColDef } from 'ag-grid-community';
import type { import type {
VegaValueFormatterParams, VegaValueFormatterParams,
VegaValueGetterParams, VegaValueGetterParams,
@ -33,7 +33,6 @@ import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import type { Position } from './positions-data-providers'; import type { Position } from './positions-data-providers';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
@ -43,6 +42,7 @@ import { DocsLinks } from '@vegaprotocol/environment';
import { PositionTableActions } from './position-actions-dropdown'; import { PositionTableActions } from './position-actions-dropdown';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { LiquidationPrice } from './liquidation-price';
interface Props extends TypedDataAgGrid<Position> { interface Props extends TypedDataAgGrid<Position> {
onClose?: (data: Position) => void; onClose?: (data: Position) => void;
@ -121,44 +121,44 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
MarketNameCell, MarketNameCell,
}} }}
{...props} {...props}
> columnDefs={useMemo<ColDef[]>(() => {
{multipleKeys ? ( const columnDefs: (ColDef | null)[] = [
<AgGridColumn multipleKeys
headerName={t('Vega key')} ? {
field="partyId" headerName: t('Vega key'),
valueGetter={({ field: 'partyId',
data, valueGetter: ({ data }: VegaValueGetterParams<Position>) =>
}: VegaValueGetterParams<Position, 'partyId'>) =>
(data?.partyId && (data?.partyId &&
pubKeys && pubKeys &&
pubKeys.find((key) => key.publicKey === data.partyId)?.name) || pubKeys.find((key) => key.publicKey === data.partyId)
data?.partyId ?.name) ||
data?.partyId,
minWidth: 190,
} }
minWidth={190} : null,
/> {
) : null} headerName: t('Market'),
<AgGridColumn field: 'marketName',
headerName={t('Market')} cellRenderer: 'MarketNameCell',
field="marketName" cellRendererParams: { idPath: 'marketId', onMarketClick },
cellRenderer="MarketNameCell" minWidth: 190,
cellRendererParams={{ idPath: 'marketId', onMarketClick }} },
minWidth={190} {
/> headerName: t('Notional'),
<AgGridColumn headerTooltip: t('Mark price x open volume.'),
headerName={t('Notional')} field: 'notional',
headerTooltip={t('Mark price x open volume.')} type: 'rightAligned',
field="notional" cellClass: 'font-mono text-right',
type="rightAligned" filter: 'agNumberColumnFilter',
cellClass="font-mono text-right" valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'notional'>) => {
return !data?.notional return !data?.notional
? undefined ? undefined
: toBigNum(data.notional, data.marketDecimalPlaces).toNumber(); : toBigNum(
}} data.notional,
valueFormatter={({ data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data, data,
}: VegaValueFormatterParams<Position, 'notional'>) => { }: VegaValueFormatterParams<Position, 'notional'>) => {
return !data || !data.notional return !data || !data.notional
@ -167,68 +167,61 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
data.notional, data.notional,
data.marketDecimalPlaces data.marketDecimalPlaces
); );
}} },
minWidth={80} minWidth: 80,
/> },
<AgGridColumn {
headerName={t('Open volume')} headerName: t('Open volume'),
field="openVolume" field: 'openVolume',
type="rightAligned" type: 'rightAligned',
cellClass="font-mono text-right" cellClass: 'font-mono text-right',
cellClassRules={signedNumberCssClassRules} cellClassRules: signedNumberCssClassRules,
filter="agNumberColumnFilter" filter: 'agNumberColumnFilter',
valueGetter={({ valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
data,
}: VegaValueGetterParams<Position, 'openVolume'>) => {
return data?.openVolume === undefined return data?.openVolume === undefined
? undefined ? undefined
: toBigNum( : toBigNum(
data?.openVolume, data?.openVolume,
data.positionDecimalPlaces data.positionDecimalPlaces
).toNumber(); ).toNumber();
}} },
valueFormatter={({ valueFormatter: ({
data, data,
}: VegaValueFormatterParams<Position, 'openVolume'>): }: VegaValueFormatterParams<Position, 'openVolume'>): string => {
| string
| undefined => {
return data?.openVolume === undefined return data?.openVolume === undefined
? undefined ? ''
: volumePrefix( : volumePrefix(
addDecimalsFormatNumber( addDecimalsFormatNumber(
data.openVolume, data.openVolume,
data.positionDecimalPlaces data.positionDecimalPlaces
) )
); );
}} },
cellRenderer={OpenVolumeCell} cellRenderer: OpenVolumeCell,
minWidth={100} minWidth: 100,
/> },
<AgGridColumn {
headerName={t('Mark price')} headerName: t('Mark price'),
field="markPrice" field: 'markPrice',
type="rightAligned" type: 'rightAligned',
cellRendererSelector={(): CellRendererSelectorResult => { cellRenderer: PriceFlashCell,
return { filter: 'agNumberColumnFilter',
component: PriceFlashCell, valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
};
}}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'markPrice'>) => {
return !data || return !data ||
!data.markPrice || !data.markPrice ||
data.marketTradingMode === data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
? undefined ? undefined
: toBigNum(data.markPrice, data.marketDecimalPlaces).toNumber(); : toBigNum(
}} data.markPrice,
valueFormatter={({ data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data, data,
}: VegaValueFormatterParams<Position, 'markPrice'>) => { }: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data) { if (!data) {
return undefined; return '';
} }
if ( if (
!data.markPrice || !data.markPrice ||
@ -241,190 +234,195 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
data.markPrice, data.markPrice,
data.marketDecimalPlaces data.marketDecimalPlaces
); );
}} },
minWidth={100} minWidth: 100,
},
{
headerName: t('Liquidation price'),
colId: 'liquidationPrice',
type: 'rightAligned',
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<LiquidationPrice
marketId={data.marketId}
openVolume={data.openVolume}
collateralAvailable={data.totalBalance}
decimalPlaces={data.decimals}
formatDecimals={data.marketDecimalPlaces}
/> />
<AgGridColumn );
headerName={t('Settlement asset')} },
field="assetSymbol" },
colId="asset" {
minWidth={100} headerName: t('Settlement asset'),
cellRenderer={({ data }: VegaICellRendererParams<Position>) => { field: 'assetSymbol',
colId: 'asset',
minWidth: 100,
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null; if (!data) return null;
return ( return (
<ButtonLink <ButtonLink
onClick={(e) => { onClick={(e) => {
openAssetDetailsDialog(data.assetId, e.target as HTMLElement); openAssetDetailsDialog(
data.assetId,
e.target as HTMLElement
);
}} }}
> >
{data?.assetSymbol} {data?.assetSymbol}
</ButtonLink> </ButtonLink>
); );
}} },
/> },
<AgGridColumn {
headerName={t('Entry price')} headerName: t('Entry price'),
field="averageEntryPrice" field: 'averageEntryPrice',
type="rightAligned" type: 'rightAligned',
cellRendererSelector={(): CellRendererSelectorResult => { cellRenderer: PriceFlashCell,
return { filter: 'agNumberColumnFilter',
component: PriceFlashCell, valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
};
}}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'averageEntryPrice'>) => {
return data?.markPrice === undefined || !data return data?.markPrice === undefined || !data
? undefined ? undefined
: toBigNum( : toBigNum(
data.averageEntryPrice, data.averageEntryPrice,
data.marketDecimalPlaces data.marketDecimalPlaces
).toNumber(); ).toNumber();
}} },
valueFormatter={({ valueFormatter: ({
data, data,
}: VegaValueFormatterParams<Position, 'averageEntryPrice'>): }: VegaValueFormatterParams<
| string Position,
| undefined => { 'averageEntryPrice'
>): string => {
if (!data) { if (!data) {
return undefined; return '';
} }
return addDecimalsFormatNumber( return addDecimalsFormatNumber(
data.averageEntryPrice, data.averageEntryPrice,
data.marketDecimalPlaces data.marketDecimalPlaces
); );
}} },
minWidth={100} minWidth: 100,
/> },
{multipleKeys ? null : ( multipleKeys
<AgGridColumn ? null
headerName={t('Leverage')} : {
field="currentLeverage" headerName: t('Leverage'),
type="rightAligned" field: 'currentLeverage',
filter="agNumberColumnFilter" type: 'rightAligned',
cellRendererSelector={(): CellRendererSelectorResult => { filter: 'agNumberColumnFilter',
return { cellRenderer: PriceFlashCell,
component: PriceFlashCell, valueFormatter: ({
};
}}
valueFormatter={({
value, value,
}: VegaValueFormatterParams<Position, 'currentLeverage'>) => }: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined value === undefined
? undefined ? ''
: formatNumber(value.toString(), 1) : formatNumber(value.toString(), 1),
} minWidth: 100,
minWidth={100} },
/> multipleKeys
)} ? null
{multipleKeys ? null : ( : {
<AgGridColumn headerName: t('Margin allocated'),
headerName={t('Margin allocated')} field: 'marginAccountBalance',
field="marginAccountBalance" type: 'rightAligned',
type="rightAligned" filter: 'agNumberColumnFilter',
filter="agNumberColumnFilter" cellRenderer: PriceFlashCell,
cellRendererSelector={(): CellRendererSelectorResult => { valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return {
component: PriceFlashCell,
};
}}
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'marginAccountBalance'>) => {
return !data return !data
? undefined ? undefined
: toBigNum(data.marginAccountBalance, data.decimals).toNumber(); : toBigNum(
}} data.marginAccountBalance,
valueFormatter={({ data.decimals
).toNumber();
},
valueFormatter: ({
data, data,
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>): }: VegaValueFormatterParams<
| string Position,
| undefined => { 'marginAccountBalance'
>): string => {
if (!data) { if (!data) {
return undefined; return '';
} }
return addDecimalsFormatNumber( return addDecimalsFormatNumber(
data.marginAccountBalance, data.marginAccountBalance,
data.decimals data.decimals
); );
}} },
minWidth={100} minWidth: 100,
/> },
)} {
<AgGridColumn headerName: t('Realised PNL'),
headerName={t('Realised PNL')} field: 'realisedPNL',
field="realisedPNL" type: 'rightAligned',
type="rightAligned" cellClassRules: signedNumberCssClassRules,
cellClassRules={signedNumberCssClassRules} cellClass: 'font-mono text-right',
cellClass="font-mono text-right" filter: 'agNumberColumnFilter',
filter="agNumberColumnFilter" valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'realisedPNL'>) => {
return !data return !data
? undefined ? undefined
: toBigNum(data.realisedPNL, data.decimals).toNumber(); : toBigNum(data.realisedPNL, data.decimals).toNumber();
}} },
valueFormatter={({ valueFormatter: ({
data, data,
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => { }: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
return !data return !data
? undefined ? ''
: addDecimalsFormatNumber(data.realisedPNL, data.decimals); : addDecimalsFormatNumber(data.realisedPNL, data.decimals);
}} },
headerTooltip={t( 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.' '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} cellRenderer: PNLCell,
minWidth={100} minWidth: 100,
/> },
<AgGridColumn {
headerName={t('Unrealised PNL')} headerName: t('Unrealised PNL'),
field="unrealisedPNL" field: 'unrealisedPNL',
type="rightAligned" type: 'rightAligned',
cellClassRules={signedNumberCssClassRules} cellClassRules: signedNumberCssClassRules,
cellClass="font-mono text-right" cellClass: 'font-mono text-right',
filter="agNumberColumnFilter" filter: 'agNumberColumnFilter',
valueGetter={({ valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
data,
}: VegaValueGetterParams<Position, 'unrealisedPNL'>) => {
return !data return !data
? undefined ? undefined
: toBigNum(data.unrealisedPNL, data.decimals).toNumber(); : toBigNum(data.unrealisedPNL, data.decimals).toNumber();
}} },
valueFormatter={({ valueFormatter: ({
data, data,
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) => }: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
!data !data
? undefined ? ''
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals) : addDecimalsFormatNumber(data.unrealisedPNL, data.decimals),
} headerTooltip: t(
headerTooltip={t(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.' 'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
)} ),
cellRenderer={PNLCell} cellRenderer: PNLCell,
minWidth={100} minWidth: 100,
/> },
<AgGridColumn {
headerName={t('Updated')} headerName: t('Updated'),
field="updatedAt" field: 'updatedAt',
type="rightAligned" type: 'rightAligned',
filter={DateRangeFilter} filter: DateRangeFilter,
valueFormatter={({ valueFormatter: ({
value, value,
}: VegaValueFormatterParams<Position, 'updatedAt'>) => { }: VegaValueFormatterParams<Position, 'updatedAt'>) => {
if (!value) { if (!value) {
return value; return '';
} }
return getDateTimeFormat().format(new Date(value)); return getDateTimeFormat().format(new Date(value));
}} },
minWidth={150} minWidth: 150,
/> },
{onClose && !isReadOnly ? ( onClose && !isReadOnly
<AgGridColumn ? {
{...COL_DEFS.actions} ...COL_DEFS.actions,
cellRenderer={({ data }: VegaICellRendererParams<Position>) => { cellRenderer: ({
data,
}: VegaICellRendererParams<Position>) => {
return ( return (
<div className="flex gap-2 items-center justify-end"> <div className="flex gap-2 items-center justify-end">
{data?.openVolume && {data?.openVolume &&
@ -442,12 +440,25 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
)} )}
</div> </div>
); );
}} },
minWidth={90} minWidth: 90,
maxWidth={90} maxWidth: 90,
}
: null,
];
return columnDefs.filter<ColDef>(
(colDef: ColDef | null): colDef is ColDef => colDef !== null
);
}, [
isReadOnly,
multipleKeys,
onClose,
onMarketClick,
openAssetDetailsDialog,
pubKey,
pubKeys,
])}
/> />
) : null}
</AgGrid>
); );
} }
); );

View File

@ -1,4 +1,5 @@
export * from './date'; export * from './date';
export * from './number'; export * from './number';
export * from './range';
export * from './size'; export * from './size';
export * from './strings'; export * from './strings';

View File

@ -1,6 +1,6 @@
import { formatRange, formatValue } from './deal-ticket-fee-details'; import { formatRange, formatValue } from './range';
describe('formatRange, formatValue', () => { describe('formatValue', () => {
it.each([ it.each([
{ v: 123000, d: 5, o: '1.23' }, { v: 123000, d: 5, o: '1.23' },
{ v: 123000, d: 3, o: '123.00' }, { v: 123000, d: 3, o: '123.00' },
@ -35,7 +35,8 @@ describe('formatRange, formatValue', () => {
expect(formatValue(v.toString(), d, q)).toStrictEqual(o); expect(formatValue(v.toString(), d, q)).toStrictEqual(o);
} }
); );
});
describe('formatRange', () => {
it.each([ it.each([
{ min: 123000, max: 12300011111, d: 5, o: '1.23 - 123,000.111', q: '0.1' }, { min: 123000, max: 12300011111, d: 5, o: '1.23 - 123,000.111', q: '0.1' },
{ {

View File

@ -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;
};