fix(#2144): fix positions table notional value and remove total row (#2155)

This commit is contained in:
m.ray 2022-11-17 16:35:29 -05:00 committed by GitHub
parent 19d0df8b1e
commit b1fe10e6d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 60 additions and 143 deletions

View File

@ -42,22 +42,20 @@ describe('positions', { tags: '@smoke' }, () => {
cy.wrap($prices).invoke('text').should('not.be.empty'); cy.wrap($prices).invoke('text').should('not.be.empty');
}); });
cy.get('[col-id="liquidationPrice"]') cy.get('[col-id="liquidationPrice"]').should('contain.text', '0'); // liquidation price
.should('contain.text', '85,093.38') // entry price
.should('contain.text', '0.00'); // liquidation price
cy.get('[col-id="currentLeverage"]').should('contain.text', '0.8'); cy.get('[col-id="currentLeverage"]').should('contain.text', '0.8');
cy.get('[col-id="marginAccountBalance"]') // margin allocated cy.get('[col-id="marginAccountBalance"]') // margin allocated
.should('contain.text', '1,000.00000'); .should('contain.text', '1,000');
cy.get('[col-id="unrealisedPNL"]').each(($unrealisedPnl) => { cy.get('[col-id="unrealisedPNL"]').each(($unrealisedPnl) => {
cy.wrap($unrealisedPnl).invoke('text').should('not.be.empty'); cy.wrap($unrealisedPnl).invoke('text').should('not.be.empty');
}); });
cy.get('[col-id="notional"]').should('contain.text', '276,761.40348'); // Total tDAI position cy.get('[col-id="notional"]').should('contain.text', '276,761.40348'); // Total tDAI position
cy.get('[col-id="realisedPNL"]').should('contain.text', '0.00100'); // Total Realised PNL cy.get('[col-id="realisedPNL"]').should('contain.text', '0.001'); // Total Realised PNL
cy.get('[col-id="unrealisedPNL"]').should('contain.text', '8.95000'); // Total Unrealised PNL cy.get('[col-id="unrealisedPNL"]').should('contain.text', '8.95'); // Total Unrealised PNL
}); });
cy.getByTestId('close-position').should('be.visible').and('have.length', 3); cy.getByTestId('close-position').should('be.visible').and('have.length', 3);

View File

@ -1,11 +1,6 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { AsyncRenderer, Icon, Intent } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, Icon, Intent } from '@vegaprotocol/ui-toolkit';
import { import { useClosePosition, usePositionsData, PositionsTable } from '../';
useClosePosition,
usePositionsData,
PositionsTable,
getSummaryRowData,
} from '../';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { Requested } from './close-position-dialog/requested'; import { Requested } from './close-position-dialog/requested';
import { Complete } from './close-position-dialog/complete'; import { Complete } from './close-position-dialog/complete';
@ -34,7 +29,6 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
<PositionsTable <PositionsTable
ref={gridRef} ref={gridRef}
rowData={data} rowData={data}
pinnedBottomRowData={data ? [getSummaryRowData(data)] : []}
onClose={(position) => submit(position)} onClose={(position) => submit(position)}
/> />
</AsyncRenderer> </AsyncRenderer>

View File

@ -92,7 +92,7 @@ it('add color and sign to amount, displays positive notional value', async () =>
expect(cells[2].classList.contains('text-vega-green-dark')).toBeTruthy(); expect(cells[2].classList.contains('text-vega-green-dark')).toBeTruthy();
expect(cells[2].classList.contains('text-vega-red-dark')).toBeFalsy(); expect(cells[2].classList.contains('text-vega-red-dark')).toBeFalsy();
expect(cells[2].textContent).toEqual('+100'); expect(cells[2].textContent).toEqual('+100');
expect(cells[1].textContent).toEqual('123.00'); expect(cells[1].textContent).toEqual('1,230');
await act(async () => { await act(async () => {
result.rerender( result.rerender(
<PositionsTable rowData={[{ ...singleRow, openVolume: '-100' }]} /> <PositionsTable rowData={[{ ...singleRow, openVolume: '-100' }]} />
@ -102,7 +102,7 @@ it('add color and sign to amount, displays positive notional value', async () =>
expect(cells[2].classList.contains('text-vega-green-dark')).toBeFalsy(); expect(cells[2].classList.contains('text-vega-green-dark')).toBeFalsy();
expect(cells[2].classList.contains('text-vega-red-dark')).toBeTruthy(); expect(cells[2].classList.contains('text-vega-red-dark')).toBeTruthy();
expect(cells[2].textContent?.startsWith('-100')).toBeTruthy(); expect(cells[2].textContent?.startsWith('-100')).toBeTruthy();
expect(cells[1].textContent).toEqual('123.00'); expect(cells[1].textContent).toEqual('1,230');
}); });
it('displays mark price', async () => { it('displays mark price', async () => {
@ -139,23 +139,13 @@ it("displays properly entry, liquidation price and liquidation bar and it's inte
}); });
let cells = screen.getAllByRole('gridcell'); let cells = screen.getAllByRole('gridcell');
const entryPrice = cells[5].firstElementChild?.firstElementChild?.textContent; const entryPrice = cells[5].firstElementChild?.firstElementChild?.textContent;
const liquidationPrice =
cells[6].firstElementChild?.lastElementChild?.textContent;
const progressBarTrack = cells[6].lastElementChild;
let progressBar = progressBarTrack?.firstElementChild as HTMLElement;
const progressBarWidth = progressBar?.style?.width;
expect(entryPrice).toEqual('13.3'); expect(entryPrice).toEqual('13.3');
expect(liquidationPrice).toEqual('8.3');
expect(progressBar.classList.contains('bg-warning')).toEqual(false);
expect(progressBarWidth).toEqual('20%');
await act(async () => { await act(async () => {
result.rerender( result.rerender(
<PositionsTable rowData={[{ ...singleRow, lowMarginLevel: true }]} /> <PositionsTable rowData={[{ ...singleRow, lowMarginLevel: true }]} />
); );
}); });
cells = screen.getAllByRole('gridcell'); cells = screen.getAllByRole('gridcell');
progressBar = cells[6].lastElementChild?.firstElementChild as HTMLElement;
expect(progressBar?.classList.contains('bg-warning')).toEqual(true);
}); });
it('displays leverage', async () => { it('displays leverage', async () => {
@ -172,7 +162,7 @@ it('displays allocated margin', async () => {
}); });
const cells = screen.getAllByRole('gridcell'); const cells = screen.getAllByRole('gridcell');
const cell = cells[8]; const cell = cells[8];
expect(cell.textContent).toEqual('123,456.00'); expect(cell.textContent).toEqual('123,456');
}); });
it('displays realised and unrealised PNL', async () => { it('displays realised and unrealised PNL', async () => {

View File

@ -1,20 +1,16 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import type { CellRendererSelectorResult } from 'ag-grid-community';
import type { import type {
ICellRendererParams,
CellRendererSelectorResult,
} from 'ag-grid-community';
import type {
ValueProps as PriceCellProps,
VegaValueFormatterParams, VegaValueFormatterParams,
VegaValueGetterParams, VegaValueGetterParams,
TypedDataAgGrid, TypedDataAgGrid,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { EmptyCell, ProgressBarCell } from '@vegaprotocol/ui-toolkit'; import { ProgressBarCell } from '@vegaprotocol/ui-toolkit';
import { import {
PriceFlashCell, PriceFlashCell,
addDecimalsFormatNumber, addDecimalsNormalizeNumber,
volumePrefix, volumePrefix,
t, t,
toBigNum, toBigNum,
@ -29,7 +25,7 @@ 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 { Schema } from '@vegaprotocol/types'; import { Schema } from '@vegaprotocol/types';
import { Intent, Button, TooltipCellComponent } from '@vegaprotocol/ui-toolkit'; import { Button, TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import { getRowId } from './use-positions-data'; import { getRowId } from './use-positions-data';
interface Props extends TypedDataAgGrid<Position> { interface Props extends TypedDataAgGrid<Position> {
@ -72,11 +68,11 @@ export const AmountCell = ({ valueFormatted }: AmountCellProps) => {
className={classNames('text-right', signedNumberCssClass(openVolume))} className={classNames('text-right', signedNumberCssClass(openVolume))}
> >
{volumePrefix( {volumePrefix(
addDecimalsFormatNumber(openVolume, positionDecimalPlaces) addDecimalsNormalizeNumber(openVolume, positionDecimalPlaces)
)} )}
</div> </div>
<div className="text-right"> <div className="text-right">
{addDecimalsFormatNumber(notional, marketDecimalPlaces)} {addDecimalsNormalizeNumber(notional, marketDecimalPlaces)}
</div> </div>
</div> </div>
) : null; ) : null;
@ -102,27 +98,6 @@ const ButtonCell = ({
); );
}; };
const progressBarValueFormatter = ({
data,
node,
}: VegaValueFormatterParams<Position, 'liquidationPrice'>):
| PriceCellProps['valueFormatted']
| undefined => {
if (!data || node?.rowPinned) {
return undefined;
}
const min = BigInt(data.averageEntryPrice);
const max = BigInt(data.liquidationPrice);
const mid = BigInt(data.markPrice);
const range = max - min;
return {
low: addDecimalsFormatNumber(min.toString(), data.marketDecimalPlaces),
high: addDecimalsFormatNumber(max.toString(), data.marketDecimalPlaces),
value: range ? Number(((mid - min) * BigInt(100)) / range) : 0,
intent: data.lowMarginLevel ? Intent.Warning : undefined,
};
};
export const PositionsTable = forwardRef<AgGridReact, Props>( export const PositionsTable = forwardRef<AgGridReact, Props>(
({ onClose, ...props }, ref) => { ({ onClose, ...props }, ref) => {
return ( return (
@ -173,14 +148,17 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}: VegaValueGetterParams<Position, 'notional'>) => { }: VegaValueGetterParams<Position, 'notional'>) => {
return data?.notional === undefined return data?.notional === undefined
? undefined ? undefined
: toBigNum(data?.notional, data.decimals).toNumber(); : toBigNum(data?.notional, data.marketDecimalPlaces).toNumber();
}} }}
valueFormatter={({ valueFormatter={({
data, data,
}: VegaValueFormatterParams<Position, 'notional'>) => { }: VegaValueFormatterParams<Position, 'notional'>) => {
return !data return !data
? undefined ? undefined
: addDecimalsFormatNumber(data.notional, data.decimals); : addDecimalsNormalizeNumber(
data.notional,
data.marketDecimalPlaces
);
}} }}
/> />
<AgGridColumn <AgGridColumn
@ -205,7 +183,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
return data?.openVolume === undefined return data?.openVolume === undefined
? undefined ? undefined
: volumePrefix( : volumePrefix(
addDecimalsFormatNumber( addDecimalsNormalizeNumber(
data.openVolume, data.openVolume,
data.positionDecimalPlaces data.positionDecimalPlaces
) )
@ -216,11 +194,9 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
headerName={t('Mark price')} headerName={t('Mark price')}
field="markPrice" field="markPrice"
type="rightAligned" type="rightAligned"
cellRendererSelector={( cellRendererSelector={(): CellRendererSelectorResult => {
params: ICellRendererParams
): CellRendererSelectorResult => {
return { return {
component: params.node.rowPinned ? EmptyCell : PriceFlashCell, component: PriceFlashCell,
}; };
}} }}
filter="agNumberColumnFilter" filter="agNumberColumnFilter"
@ -237,7 +213,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
data, data,
node, node,
}: VegaValueFormatterParams<Position, 'markPrice'>) => { }: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data || node?.rowPinned) { if (!data) {
return undefined; return undefined;
} }
if ( if (
@ -246,7 +222,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
) { ) {
return '-'; return '-';
} }
return addDecimalsFormatNumber( return addDecimalsNormalizeNumber(
data.markPrice, data.markPrice,
data.marketDecimalPlaces data.marketDecimalPlaces
); );
@ -257,11 +233,9 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
headerName={t('Entry price')} headerName={t('Entry price')}
field="averageEntryPrice" field="averageEntryPrice"
type="rightAligned" type="rightAligned"
cellRendererSelector={( cellRendererSelector={(): CellRendererSelectorResult => {
params: ICellRendererParams
): CellRendererSelectorResult => {
return { return {
component: params.node.rowPinned ? EmptyCell : PriceFlashCell, component: PriceFlashCell,
}; };
}} }}
filter="agNumberColumnFilter" filter="agNumberColumnFilter"
@ -281,10 +255,10 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}: VegaValueFormatterParams<Position, 'averageEntryPrice'>): }: VegaValueFormatterParams<Position, 'averageEntryPrice'>):
| string | string
| undefined => { | undefined => {
if (!data || node?.rowPinned) { if (!data) {
return undefined; return undefined;
} }
return addDecimalsFormatNumber( return addDecimalsNormalizeNumber(
data.averageEntryPrice, data.averageEntryPrice,
data.marketDecimalPlaces data.marketDecimalPlaces
); );
@ -293,40 +267,45 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
<AgGridColumn <AgGridColumn
headerName={t('Liquidation price (est)')} headerName={t('Liquidation price (est)')}
field="liquidationPrice" field="liquidationPrice"
flex={2} type="rightAligned"
headerTooltip={t( cellRendererSelector={(): CellRendererSelectorResult => {
'Liquidation prices are based on the amount of collateral you have available, the risk of your position and the liquidity on the order book. They can change rapidly based on the profit and loss of your positions and any changes to collateral from opening/closing other positions and making deposits/withdrawals.' return {
)} component: PriceFlashCell,
};
}}
filter="agNumberColumnFilter" filter="agNumberColumnFilter"
valueGetter={({ valueGetter={({
data, data,
}: VegaValueGetterParams<Position, 'liquidationPrice'>) => { }: VegaValueGetterParams<Position, 'liquidationPrice'>) => {
return !data return data?.liquidationPrice === undefined || !data
? undefined ? undefined
: toBigNum( : toBigNum(
data?.liquidationPrice, data.liquidationPrice,
data.marketDecimalPlaces data.marketDecimalPlaces
).toNumber(); ).toNumber();
}} }}
cellRendererSelector={( valueFormatter={({
params: ICellRendererParams data,
): CellRendererSelectorResult => { }: VegaValueFormatterParams<Position, 'liquidationPrice'>):
return { | string
component: params.node.rowPinned ? EmptyCell : ProgressBarCell, | undefined => {
}; if (!data) {
return undefined;
}
return addDecimalsNormalizeNumber(
data.liquidationPrice,
data.marketDecimalPlaces
);
}} }}
valueFormatter={progressBarValueFormatter}
/> />
<AgGridColumn <AgGridColumn
headerName={t('Leverage')} headerName={t('Leverage')}
field="currentLeverage" field="currentLeverage"
type="rightAligned" type="rightAligned"
filter="agNumberColumnFilter" filter="agNumberColumnFilter"
cellRendererSelector={( cellRendererSelector={(): CellRendererSelectorResult => {
params: ICellRendererParams
): CellRendererSelectorResult => {
return { return {
component: params.node.rowPinned ? EmptyCell : PriceFlashCell, component: PriceFlashCell,
}; };
}} }}
valueFormatter={({ valueFormatter={({
@ -340,11 +319,9 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
field="marginAccountBalance" field="marginAccountBalance"
type="rightAligned" type="rightAligned"
filter="agNumberColumnFilter" filter="agNumberColumnFilter"
cellRendererSelector={( cellRendererSelector={(): CellRendererSelectorResult => {
params: ICellRendererParams
): CellRendererSelectorResult => {
return { return {
component: params.node.rowPinned ? EmptyCell : PriceFlashCell, component: PriceFlashCell,
}; };
}} }}
valueGetter={({ valueGetter={({
@ -360,10 +337,10 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>): }: VegaValueFormatterParams<Position, 'marginAccountBalance'>):
| string | string
| undefined => { | undefined => {
if (!data || node?.rowPinned) { if (!data) {
return undefined; return undefined;
} }
return addDecimalsFormatNumber( return addDecimalsNormalizeNumber(
data.marginAccountBalance, data.marginAccountBalance,
data.decimals data.decimals
); );
@ -387,7 +364,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => { }: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
return !data return !data
? undefined ? undefined
: addDecimalsFormatNumber(data.realisedPNL, data.decimals); : addDecimalsNormalizeNumber(data.realisedPNL, data.decimals);
}} }}
cellRenderer="PriceFlashCell" cellRenderer="PriceFlashCell"
headerTooltip={t( headerTooltip={t(
@ -412,7 +389,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) => }: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
!data !data
? undefined ? undefined
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals) : addDecimalsNormalizeNumber(data.unrealisedPNL, data.decimals)
} }
cellRenderer="PriceFlashCell" cellRenderer="PriceFlashCell"
headerTooltip={t( headerTooltip={t(
@ -435,11 +412,9 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
/> />
{onClose ? ( {onClose ? (
<AgGridColumn <AgGridColumn
cellRendererSelector={( cellRendererSelector={(): CellRendererSelectorResult => {
params: ICellRendererParams
): CellRendererSelectorResult => {
return { return {
component: params.node.rowPinned ? EmptyCell : ButtonCell, component: ButtonCell,
}; };
}} }}
cellRendererParams={{ onClick: onClose }} cellRendererParams={{ onClick: onClose }}

View File

@ -1,41 +1,13 @@
import { useCallback, useMemo, useRef } from 'react'; import { useCallback, useMemo, useRef } from 'react';
import type { RefObject } from 'react'; import type { RefObject } from 'react';
import { BigNumber } from 'bignumber.js';
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 { positionsMetricsProvider } from './positions-data-providers'; import { positionsMetricsProvider } from './positions-data-providers';
import type { PositionsMetricsProviderVariables } from './positions-data-providers'; import type { PositionsMetricsProviderVariables } from './positions-data-providers';
import { t, toBigNum, useDataProvider } from '@vegaprotocol/react-helpers'; import { useDataProvider } from '@vegaprotocol/react-helpers';
export const getRowId = ({ data }: { data: Position }) => data.marketId; export const getRowId = ({ data }: { data: Position }) => data.marketId;
export const getSummaryRowData = (positions: Position[]) => {
const summaryRow = {
notional: new BigNumber(0),
realisedPNL: BigInt(0),
unrealisedPNL: BigInt(0),
};
positions.forEach((position) => {
summaryRow.notional = summaryRow.notional.plus(
toBigNum(position.notional, position.marketDecimalPlaces)
);
summaryRow.realisedPNL += BigInt(position.realisedPNL);
summaryRow.unrealisedPNL += BigInt(position.unrealisedPNL);
});
const decimals = positions[0]?.decimals || 0;
return {
marketName: t('Total'),
// we are using asset decimals instead of market decimals because each market can have different decimals
notional: summaryRow.notional
.multipliedBy(10 ** decimals)
.toFixed()
.toString(),
realisedPNL: summaryRow.realisedPNL.toString(),
unrealisedPNL: summaryRow.unrealisedPNL.toString(),
decimals,
};
};
export const usePositionsData = ( export const usePositionsData = (
partyId: string, partyId: string,
gridRef: RefObject<AgGridReact> gridRef: RefObject<AgGridReact>
@ -76,18 +48,6 @@ export const usePositionsData = (
add, add,
addIndex: 0, addIndex: 0,
}); });
const summaryRowNode = gridRef.current.api.getPinnedBottomRow(0);
if (summaryRowNode && dataRef.current) {
summaryRowNode.data = getSummaryRowData(dataRef.current);
gridRef.current.api.refreshCells({
force: true,
rowNodes: [summaryRowNode],
});
} else {
gridRef.current.api.setPinnedBottomRowData(
dataRef.current ? [getSummaryRowData(dataRef.current)] : []
);
}
} }
return true; return true;
}, },

View File

@ -36,6 +36,6 @@ export const progressBarCellRendererSelector = (
params: ICellRendererParams params: ICellRendererParams
): CellRendererSelectorResult => { ): CellRendererSelectorResult => {
return { return {
component: params.node.rowPinned ? EmptyCell : ProgressBarCell, component: ProgressBarCell,
}; };
}; };