feat(positions): add liquidation to positions view (#4115)
This commit is contained in:
parent
e451dc54b3
commit
597e07608f
@ -77,7 +77,7 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
|
||||
hide={window.innerWidth <= BREAKPOINT_MD}
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<MarketFieldsFragment, 'state'>) => {
|
||||
}: VegaValueGetterParams<MarketFieldsFragment>) => {
|
||||
return data?.state ? MarketStateMapping[data?.state] : '-';
|
||||
}}
|
||||
/>
|
||||
|
@ -24,11 +24,13 @@ export type VegaValueFormatterParams<TRow, TField extends Field> = RowHelper<
|
||||
TField
|
||||
>;
|
||||
|
||||
export type VegaValueGetterParams<TRow, TField extends Field> = RowHelper<
|
||||
export type VegaValueGetterParams<TRow> = Omit<
|
||||
ValueGetterParams,
|
||||
TRow,
|
||||
TField
|
||||
>;
|
||||
'data' | 'node'
|
||||
> & {
|
||||
data?: TRow;
|
||||
node: (Omit<RowNode, 'data'> & { data?: TRow }) | null;
|
||||
};
|
||||
|
||||
export type VegaICellRendererParams<TRow, TField extends Field = string> = Omit<
|
||||
RowHelper<ICellRendererParams, TRow, TField>,
|
||||
|
@ -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}
|
||||
<DealTicketFeeDetail
|
||||
label={t('Liquidation price estimate')}
|
||||
value={liquidationPriceEstimate}
|
||||
formattedValue={liquidationPriceEstimateFormatted}
|
||||
formattedValue={liquidationPriceEstimate}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT}
|
||||
/>
|
||||
|
@ -76,9 +76,7 @@ export const useColumnDefs = ({ onMarketClick }: Props) => {
|
||||
type: 'rightAligned',
|
||||
cellRenderer: 'PriceFlashCell',
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueGetter: ({
|
||||
data,
|
||||
}: VegaValueGetterParams<MarketMaybeWithData, 'data.bestBidPrice'>) => {
|
||||
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
|
||||
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<MarketMaybeWithData>) => {
|
||||
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<MarketMaybeWithData, 'data.markPrice'>) => {
|
||||
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
|
||||
return data?.data?.markPrice === undefined
|
||||
? undefined
|
||||
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();
|
||||
|
60
libs/positions/src/lib/liquidation-price.tsx
Normal file
60
libs/positions/src/lib/liquidation-price.tsx
Normal 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>;
|
||||
};
|
@ -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,
|
||||
|
@ -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: () => (
|
||||
<span data-testid="liquidation-price">liquidation price</span>
|
||||
),
|
||||
}));
|
||||
|
||||
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(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[4].textContent).toEqual('liquidation price');
|
||||
});
|
||||
|
||||
it('displays leverage', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
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(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
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(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
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', () => {
|
||||
|
@ -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<Position> {
|
||||
onClose?: (data: Position) => void;
|
||||
@ -121,333 +121,344 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
MarketNameCell,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{multipleKeys ? (
|
||||
<AgGridColumn
|
||||
headerName={t('Vega key')}
|
||||
field="partyId"
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<Position, 'partyId'>) =>
|
||||
(data?.partyId &&
|
||||
pubKeys &&
|
||||
pubKeys.find((key) => key.publicKey === data.partyId)?.name) ||
|
||||
data?.partyId
|
||||
}
|
||||
minWidth={190}
|
||||
/>
|
||||
) : null}
|
||||
<AgGridColumn
|
||||
headerName={t('Market')}
|
||||
field="marketName"
|
||||
cellRenderer="MarketNameCell"
|
||||
cellRendererParams={{ idPath: 'marketId', onMarketClick }}
|
||||
minWidth={190}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Notional')}
|
||||
headerTooltip={t('Mark price x open volume.')}
|
||||
field="notional"
|
||||
type="rightAligned"
|
||||
cellClass="font-mono text-right"
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<Position, 'notional'>) => {
|
||||
return !data?.notional
|
||||
? undefined
|
||||
: toBigNum(data.notional, data.marketDecimalPlaces).toNumber();
|
||||
}}
|
||||
valueFormatter={({
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'notional'>) => {
|
||||
return !data || !data.notional
|
||||
? '-'
|
||||
: addDecimalsFormatNumber(
|
||||
data.notional,
|
||||
columnDefs={useMemo<ColDef[]>(() => {
|
||||
const columnDefs: (ColDef | null)[] = [
|
||||
multipleKeys
|
||||
? {
|
||||
headerName: t('Vega key'),
|
||||
field: 'partyId',
|
||||
valueGetter: ({ data }: VegaValueGetterParams<Position>) =>
|
||||
(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<Position>) => {
|
||||
return !data?.notional
|
||||
? undefined
|
||||
: toBigNum(
|
||||
data.notional,
|
||||
data.marketDecimalPlaces
|
||||
).toNumber();
|
||||
},
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'notional'>) => {
|
||||
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<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
|
||||
);
|
||||
}}
|
||||
minWidth={80}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Open volume')}
|
||||
field="openVolume"
|
||||
type="rightAligned"
|
||||
cellClass="font-mono text-right"
|
||||
cellClassRules={signedNumberCssClassRules}
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<Position, 'openVolume'>) => {
|
||||
return data?.openVolume === undefined
|
||||
? undefined
|
||||
: toBigNum(
|
||||
data?.openVolume,
|
||||
data.positionDecimalPlaces
|
||||
).toNumber();
|
||||
}}
|
||||
valueFormatter={({
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'openVolume'>):
|
||||
| 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<Position>) => {
|
||||
if (!data) return null;
|
||||
return (
|
||||
<LiquidationPrice
|
||||
marketId={data.marketId}
|
||||
openVolume={data.openVolume}
|
||||
collateralAvailable={data.totalBalance}
|
||||
decimalPlaces={data.decimals}
|
||||
formatDecimals={data.marketDecimalPlaces}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
cellRenderer={OpenVolumeCell}
|
||||
minWidth={100}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Mark price')}
|
||||
field="markPrice"
|
||||
type="rightAligned"
|
||||
cellRendererSelector={(): CellRendererSelectorResult => {
|
||||
return {
|
||||
component: PriceFlashCell,
|
||||
};
|
||||
}}
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<Position, 'markPrice'>) => {
|
||||
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 undefined;
|
||||
}
|
||||
if (
|
||||
!data.markPrice ||
|
||||
data.marketTradingMode ===
|
||||
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
|
||||
) {
|
||||
return '-';
|
||||
}
|
||||
return addDecimalsFormatNumber(
|
||||
data.markPrice,
|
||||
data.marketDecimalPlaces
|
||||
);
|
||||
}}
|
||||
minWidth={100}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Settlement asset')}
|
||||
field="assetSymbol"
|
||||
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(
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Settlement asset'),
|
||||
field: 'assetSymbol',
|
||||
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>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Entry price'),
|
||||
field: 'averageEntryPrice',
|
||||
type: 'rightAligned',
|
||||
cellRenderer: PriceFlashCell,
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
|
||||
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<Position, 'averageEntryPrice'>):
|
||||
| string
|
||||
| undefined => {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
return addDecimalsFormatNumber(
|
||||
data.averageEntryPrice,
|
||||
data.marketDecimalPlaces
|
||||
);
|
||||
}}
|
||||
minWidth={100}
|
||||
/>
|
||||
{multipleKeys ? null : (
|
||||
<AgGridColumn
|
||||
headerName={t('Leverage')}
|
||||
field="currentLeverage"
|
||||
type="rightAligned"
|
||||
filter="agNumberColumnFilter"
|
||||
cellRendererSelector={(): CellRendererSelectorResult => {
|
||||
return {
|
||||
component: PriceFlashCell,
|
||||
};
|
||||
}}
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
|
||||
value === undefined
|
||||
? undefined
|
||||
: formatNumber(value.toString(), 1)
|
||||
}
|
||||
minWidth={100}
|
||||
/>
|
||||
)}
|
||||
{multipleKeys ? null : (
|
||||
<AgGridColumn
|
||||
headerName={t('Margin allocated')}
|
||||
field="marginAccountBalance"
|
||||
type="rightAligned"
|
||||
filter="agNumberColumnFilter"
|
||||
cellRendererSelector={(): CellRendererSelectorResult => {
|
||||
return {
|
||||
component: PriceFlashCell,
|
||||
};
|
||||
}}
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<Position, 'marginAccountBalance'>) => {
|
||||
return !data
|
||||
? undefined
|
||||
: toBigNum(data.marginAccountBalance, data.decimals).toNumber();
|
||||
}}
|
||||
valueFormatter={({
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>):
|
||||
| string
|
||||
| undefined => {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
return addDecimalsFormatNumber(
|
||||
data.marginAccountBalance,
|
||||
data.decimals
|
||||
);
|
||||
}}
|
||||
minWidth={100}
|
||||
/>
|
||||
)}
|
||||
<AgGridColumn
|
||||
headerName={t('Realised PNL')}
|
||||
field="realisedPNL"
|
||||
type="rightAligned"
|
||||
cellClassRules={signedNumberCssClassRules}
|
||||
cellClass="font-mono text-right"
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<Position, 'realisedPNL'>) => {
|
||||
return !data
|
||||
? undefined
|
||||
: toBigNum(data.realisedPNL, data.decimals).toNumber();
|
||||
}}
|
||||
valueFormatter={({
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
|
||||
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}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Unrealised PNL')}
|
||||
field="unrealisedPNL"
|
||||
type="rightAligned"
|
||||
cellClassRules={signedNumberCssClassRules}
|
||||
cellClass="font-mono text-right"
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<Position, 'unrealisedPNL'>) => {
|
||||
return !data
|
||||
? undefined
|
||||
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
|
||||
}}
|
||||
valueFormatter={({
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
|
||||
!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}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Updated')}
|
||||
field="updatedAt"
|
||||
type="rightAligned"
|
||||
filter={DateRangeFilter}
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<Position, 'updatedAt'>) => {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
return getDateTimeFormat().format(new Date(value));
|
||||
}}
|
||||
minWidth={150}
|
||||
/>
|
||||
{onClose && !isReadOnly ? (
|
||||
<AgGridColumn
|
||||
{...COL_DEFS.actions}
|
||||
cellRenderer={({ data }: VegaICellRendererParams<Position>) => {
|
||||
return (
|
||||
<div className="flex gap-2 items-center justify-end">
|
||||
{data?.openVolume &&
|
||||
data?.openVolume !== '0' &&
|
||||
data.partyId === pubKey ? (
|
||||
<ButtonLink
|
||||
data-testid="close-position"
|
||||
onClick={() => data && onClose(data)}
|
||||
>
|
||||
{t('Close')}
|
||||
</ButtonLink>
|
||||
) : null}
|
||||
{data?.assetId && (
|
||||
<PositionTableActions assetId={data?.assetId} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
minWidth={90}
|
||||
maxWidth={90}
|
||||
/>
|
||||
) : null}
|
||||
</AgGrid>
|
||||
);
|
||||
},
|
||||
minWidth: 100,
|
||||
},
|
||||
multipleKeys
|
||||
? null
|
||||
: {
|
||||
headerName: t('Leverage'),
|
||||
field: 'currentLeverage',
|
||||
type: 'rightAligned',
|
||||
filter: 'agNumberColumnFilter',
|
||||
cellRenderer: PriceFlashCell,
|
||||
valueFormatter: ({
|
||||
value,
|
||||
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
|
||||
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<Position>) => {
|
||||
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<Position>) => {
|
||||
return !data
|
||||
? undefined
|
||||
: toBigNum(data.realisedPNL, data.decimals).toNumber();
|
||||
},
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
|
||||
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<Position>) => {
|
||||
return !data
|
||||
? undefined
|
||||
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
|
||||
},
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
|
||||
!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<Position, 'updatedAt'>) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return getDateTimeFormat().format(new Date(value));
|
||||
},
|
||||
minWidth: 150,
|
||||
},
|
||||
onClose && !isReadOnly
|
||||
? {
|
||||
...COL_DEFS.actions,
|
||||
cellRenderer: ({
|
||||
data,
|
||||
}: VegaICellRendererParams<Position>) => {
|
||||
return (
|
||||
<div className="flex gap-2 items-center justify-end">
|
||||
{data?.openVolume &&
|
||||
data?.openVolume !== '0' &&
|
||||
data.partyId === pubKey ? (
|
||||
<ButtonLink
|
||||
data-testid="close-position"
|
||||
onClick={() => data && onClose(data)}
|
||||
>
|
||||
{t('Close')}
|
||||
</ButtonLink>
|
||||
) : null}
|
||||
{data?.assetId && (
|
||||
<PositionTableActions assetId={data?.assetId} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
minWidth: 90,
|
||||
maxWidth: 90,
|
||||
}
|
||||
: null,
|
||||
];
|
||||
return columnDefs.filter<ColDef>(
|
||||
(colDef: ColDef | null): colDef is ColDef => colDef !== null
|
||||
);
|
||||
}, [
|
||||
isReadOnly,
|
||||
multipleKeys,
|
||||
onClose,
|
||||
onMarketClick,
|
||||
openAssetDetailsDialog,
|
||||
pubKey,
|
||||
pubKeys,
|
||||
])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
export * from './date';
|
||||
export * from './number';
|
||||
export * from './range';
|
||||
export * from './size';
|
||||
export * from './strings';
|
||||
|
@ -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' },
|
||||
{
|
38
libs/utils/src/lib/format/range.ts
Normal file
38
libs/utils/src/lib/format/range.ts
Normal 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;
|
||||
};
|
Loading…
Reference in New Issue
Block a user