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,333 +121,344 @@ 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)
pubKeys.find((key) => key.publicKey === data.partyId)?.name) || ?.name) ||
data?.partyId data?.partyId,
} minWidth: 190,
minWidth={190} }
/> : null,
) : null} {
<AgGridColumn headerName: t('Market'),
headerName={t('Market')} field: 'marketName',
field="marketName" cellRenderer: 'MarketNameCell',
cellRenderer="MarketNameCell" cellRendererParams: { idPath: 'marketId', onMarketClick },
cellRendererParams={{ idPath: 'marketId', onMarketClick }} minWidth: 190,
minWidth={190} },
/> {
<AgGridColumn headerName: t('Notional'),
headerName={t('Notional')} headerTooltip: t('Mark price x open volume.'),
headerTooltip={t('Mark price x open volume.')} field: 'notional',
field="notional" type: 'rightAligned',
type="rightAligned" cellClass: 'font-mono text-right',
cellClass="font-mono text-right" filter: 'agNumberColumnFilter',
filter="agNumberColumnFilter" valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
valueGetter={({ return !data?.notional
data, ? undefined
}: VegaValueGetterParams<Position, 'notional'>) => { : toBigNum(
return !data?.notional data.notional,
? undefined data.marketDecimalPlaces
: toBigNum(data.notional, data.marketDecimalPlaces).toNumber(); ).toNumber();
}} },
valueFormatter={({ valueFormatter: ({
data, data,
}: VegaValueFormatterParams<Position, 'notional'>) => { }: VegaValueFormatterParams<Position, 'notional'>) => {
return !data || !data.notional return !data || !data.notional
? '-' ? '-'
: addDecimalsFormatNumber( : addDecimalsFormatNumber(
data.notional, 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<Position>) => {
return data?.openVolume === undefined
? undefined
: toBigNum(
data?.openVolume,
data.positionDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'openVolume'>): 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<Position>) => {
return !data ||
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
? undefined
: toBigNum(
data.markPrice,
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data) {
return '';
}
if (
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
) {
return '-';
}
return addDecimalsFormatNumber(
data.markPrice,
data.marketDecimalPlaces data.marketDecimalPlaces
); );
}} },
minWidth={80} minWidth: 100,
/> },
<AgGridColumn {
headerName={t('Open volume')} headerName: t('Liquidation price'),
field="openVolume" colId: 'liquidationPrice',
type="rightAligned" type: 'rightAligned',
cellClass="font-mono text-right" cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
cellClassRules={signedNumberCssClassRules} if (!data) return null;
filter="agNumberColumnFilter" return (
valueGetter={({ <LiquidationPrice
data, marketId={data.marketId}
}: VegaValueGetterParams<Position, 'openVolume'>) => { openVolume={data.openVolume}
return data?.openVolume === undefined collateralAvailable={data.totalBalance}
? undefined decimalPlaces={data.decimals}
: toBigNum( formatDecimals={data.marketDecimalPlaces}
data?.openVolume, />
data.positionDecimalPlaces
).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'openVolume'>):
| string
| undefined => {
return data?.openVolume === undefined
? undefined
: volumePrefix(
addDecimalsFormatNumber(
data.openVolume,
data.positionDecimalPlaces
)
); );
}} },
cellRenderer={OpenVolumeCell} },
minWidth={100} {
/> headerName: t('Settlement asset'),
<AgGridColumn field: 'assetSymbol',
headerName={t('Mark price')} colId: 'asset',
field="markPrice" minWidth: 100,
type="rightAligned" cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
cellRendererSelector={(): CellRendererSelectorResult => { if (!data) return null;
return { return (
component: PriceFlashCell, <ButtonLink
}; onClick={(e) => {
}} openAssetDetailsDialog(
filter="agNumberColumnFilter" data.assetId,
valueGetter={({ e.target as HTMLElement
data, );
}: VegaValueGetterParams<Position, 'markPrice'>) => { }}
return !data || >
!data.markPrice || {data?.assetSymbol}
data.marketTradingMode === </ButtonLink>
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION );
? undefined },
: toBigNum(data.markPrice, data.marketDecimalPlaces).toNumber(); },
}} {
valueFormatter={({ headerName: t('Entry price'),
data, field: 'averageEntryPrice',
}: VegaValueFormatterParams<Position, 'markPrice'>) => { type: 'rightAligned',
if (!data) { cellRenderer: PriceFlashCell,
return undefined; filter: 'agNumberColumnFilter',
} valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
if ( return data?.markPrice === undefined || !data
!data.markPrice || ? undefined
data.marketTradingMode === : toBigNum(
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION data.averageEntryPrice,
) { data.marketDecimalPlaces
return '-'; ).toNumber();
} },
return addDecimalsFormatNumber( valueFormatter: ({
data.markPrice, data,
data.marketDecimalPlaces }: VegaValueFormatterParams<
); Position,
}} 'averageEntryPrice'
minWidth={100} >): string => {
/> if (!data) {
<AgGridColumn return '';
headerName={t('Settlement asset')} }
field="assetSymbol" return addDecimalsFormatNumber(
colId="asset"
minWidth={100}
cellRenderer={({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(data.assetId, e.target as HTMLElement);
}}
>
{data?.assetSymbol}
</ButtonLink>
);
}}
/>
<AgGridColumn
headerName={t('Entry price')}
field="averageEntryPrice"
type="rightAligned"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'averageEntryPrice'>) => {
return data?.markPrice === undefined || !data
? undefined
: toBigNum(
data.averageEntryPrice, data.averageEntryPrice,
data.marketDecimalPlaces data.marketDecimalPlaces
).toNumber(); );
}} },
valueFormatter={({ minWidth: 100,
data, },
}: VegaValueFormatterParams<Position, 'averageEntryPrice'>): multipleKeys
| string ? null
| undefined => { : {
if (!data) { headerName: t('Leverage'),
return undefined; field: 'currentLeverage',
} type: 'rightAligned',
return addDecimalsFormatNumber( filter: 'agNumberColumnFilter',
data.averageEntryPrice, cellRenderer: PriceFlashCell,
data.marketDecimalPlaces valueFormatter: ({
); value,
}} }: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
minWidth={100} value === undefined
/> ? ''
{multipleKeys ? null : ( : formatNumber(value.toString(), 1),
<AgGridColumn minWidth: 100,
headerName={t('Leverage')} },
field="currentLeverage" multipleKeys
type="rightAligned" ? null
filter="agNumberColumnFilter" : {
cellRendererSelector={(): CellRendererSelectorResult => { headerName: t('Margin allocated'),
return { field: 'marginAccountBalance',
component: PriceFlashCell, type: 'rightAligned',
}; filter: 'agNumberColumnFilter',
}} cellRenderer: PriceFlashCell,
valueFormatter={({ valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
value, return !data
}: VegaValueFormatterParams<Position, 'currentLeverage'>) => ? undefined
value === undefined : toBigNum(
? undefined data.marginAccountBalance,
: formatNumber(value.toString(), 1) data.decimals
} ).toNumber();
minWidth={100} },
/> valueFormatter: ({
)} data,
{multipleKeys ? null : ( }: VegaValueFormatterParams<
<AgGridColumn Position,
headerName={t('Margin allocated')} 'marginAccountBalance'
field="marginAccountBalance" >): string => {
type="rightAligned" if (!data) {
filter="agNumberColumnFilter" return '';
cellRendererSelector={(): CellRendererSelectorResult => { }
return { return addDecimalsFormatNumber(
component: PriceFlashCell, data.marginAccountBalance,
}; data.decimals
}} );
valueGetter={({ },
data, minWidth: 100,
}: VegaValueGetterParams<Position, 'marginAccountBalance'>) => { },
return !data {
? undefined headerName: t('Realised PNL'),
: toBigNum(data.marginAccountBalance, data.decimals).toNumber(); field: 'realisedPNL',
}} type: 'rightAligned',
valueFormatter={({ cellClassRules: signedNumberCssClassRules,
data, cellClass: 'font-mono text-right',
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>): filter: 'agNumberColumnFilter',
| string valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
| undefined => { return !data
if (!data) { ? undefined
return undefined; : toBigNum(data.realisedPNL, data.decimals).toNumber();
} },
return addDecimalsFormatNumber( valueFormatter: ({
data.marginAccountBalance, data,
data.decimals }: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
); return !data
}} ? ''
minWidth={100} : addDecimalsFormatNumber(data.realisedPNL, data.decimals);
/> },
)} headerTooltip: t(
<AgGridColumn '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.'
headerName={t('Realised PNL')} ),
field="realisedPNL" cellRenderer: PNLCell,
type="rightAligned" minWidth: 100,
cellClassRules={signedNumberCssClassRules} },
cellClass="font-mono text-right" {
filter="agNumberColumnFilter" headerName: t('Unrealised PNL'),
valueGetter={({ field: 'unrealisedPNL',
data, type: 'rightAligned',
}: VegaValueGetterParams<Position, 'realisedPNL'>) => { cellClassRules: signedNumberCssClassRules,
return !data cellClass: 'font-mono text-right',
? undefined filter: 'agNumberColumnFilter',
: toBigNum(data.realisedPNL, data.decimals).toNumber(); valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
}} return !data
valueFormatter={({ ? undefined
data, : toBigNum(data.unrealisedPNL, data.decimals).toNumber();
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => { },
return !data valueFormatter: ({
? undefined data,
: addDecimalsFormatNumber(data.realisedPNL, data.decimals); }: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
}} !data
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.' : addDecimalsFormatNumber(data.unrealisedPNL, data.decimals),
)} headerTooltip: t(
cellRenderer={PNLCell} 'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
minWidth={100} ),
/> cellRenderer: PNLCell,
<AgGridColumn minWidth: 100,
headerName={t('Unrealised PNL')} },
field="unrealisedPNL" {
type="rightAligned" headerName: t('Updated'),
cellClassRules={signedNumberCssClassRules} field: 'updatedAt',
cellClass="font-mono text-right" type: 'rightAligned',
filter="agNumberColumnFilter" filter: DateRangeFilter,
valueGetter={({ valueFormatter: ({
data, value,
}: VegaValueGetterParams<Position, 'unrealisedPNL'>) => { }: VegaValueFormatterParams<Position, 'updatedAt'>) => {
return !data if (!value) {
? undefined return '';
: toBigNum(data.unrealisedPNL, data.decimals).toNumber(); }
}} return getDateTimeFormat().format(new Date(value));
valueFormatter={({ },
data, minWidth: 150,
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) => },
!data onClose && !isReadOnly
? undefined ? {
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals) ...COL_DEFS.actions,
} cellRenderer: ({
headerTooltip={t( data,
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.' }: VegaICellRendererParams<Position>) => {
)} return (
cellRenderer={PNLCell} <div className="flex gap-2 items-center justify-end">
minWidth={100} {data?.openVolume &&
/> data?.openVolume !== '0' &&
<AgGridColumn data.partyId === pubKey ? (
headerName={t('Updated')} <ButtonLink
field="updatedAt" data-testid="close-position"
type="rightAligned" onClick={() => data && onClose(data)}
filter={DateRangeFilter} >
valueFormatter={({ {t('Close')}
value, </ButtonLink>
}: VegaValueFormatterParams<Position, 'updatedAt'>) => { ) : null}
if (!value) { {data?.assetId && (
return value; <PositionTableActions assetId={data?.assetId} />
} )}
return getDateTimeFormat().format(new Date(value)); </div>
}} );
minWidth={150} },
/> minWidth: 90,
{onClose && !isReadOnly ? ( maxWidth: 90,
<AgGridColumn }
{...COL_DEFS.actions} : null,
cellRenderer={({ data }: VegaICellRendererParams<Position>) => { ];
return ( return columnDefs.filter<ColDef>(
<div className="flex gap-2 items-center justify-end"> (colDef: ColDef | null): colDef is ColDef => colDef !== null
{data?.openVolume && );
data?.openVolume !== '0' && }, [
data.partyId === pubKey ? ( isReadOnly,
<ButtonLink multipleKeys,
data-testid="close-position" onClose,
onClick={() => data && onClose(data)} onMarketClick,
> openAssetDetailsDialog,
{t('Close')} pubKey,
</ButtonLink> pubKeys,
) : null} ])}
{data?.assetId && ( />
<PositionTableActions assetId={data?.assetId} />
)}
</div>
);
}}
minWidth={90}
maxWidth={90}
/>
) : 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;
};