feat(positions): positions losses and maintenance warnings (#2985)
This commit is contained in:
parent
56f48671cc
commit
f54a629179
@ -4,6 +4,8 @@ fragment PositionFields on Position {
|
|||||||
unrealisedPNL
|
unrealisedPNL
|
||||||
averageEntryPrice
|
averageEntryPrice
|
||||||
updatedAt
|
updatedAt
|
||||||
|
positionStatus
|
||||||
|
lossSocializationAmount
|
||||||
market {
|
market {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
@ -30,6 +32,8 @@ subscription PositionsSubscription($partyId: ID!) {
|
|||||||
averageEntryPrice
|
averageEntryPrice
|
||||||
updatedAt
|
updatedAt
|
||||||
marketId
|
marketId
|
||||||
|
lossSocializationAmount
|
||||||
|
positionStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
10
libs/positions/src/lib/__generated__/Positions.ts
generated
10
libs/positions/src/lib/__generated__/Positions.ts
generated
@ -3,21 +3,21 @@ import * as Types from '@vegaprotocol/types';
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
import * as Apollo from '@apollo/client';
|
import * as Apollo from '@apollo/client';
|
||||||
const defaultOptions = {} as const;
|
const defaultOptions = {} as const;
|
||||||
export type PositionFieldsFragment = { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, market: { __typename?: 'Market', id: string } };
|
export type PositionFieldsFragment = { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, positionStatus: Types.PositionStatus, lossSocializationAmount: string, market: { __typename?: 'Market', id: string } };
|
||||||
|
|
||||||
export type PositionsQueryVariables = Types.Exact<{
|
export type PositionsQueryVariables = Types.Exact<{
|
||||||
partyId: Types.Scalars['ID'];
|
partyId: Types.Scalars['ID'];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type PositionsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, positionsConnection?: { __typename?: 'PositionConnection', edges?: Array<{ __typename?: 'PositionEdge', node: { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, market: { __typename?: 'Market', id: string } } }> | null } | null } | null };
|
export type PositionsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, positionsConnection?: { __typename?: 'PositionConnection', edges?: Array<{ __typename?: 'PositionEdge', node: { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, positionStatus: Types.PositionStatus, lossSocializationAmount: string, market: { __typename?: 'Market', id: string } } }> | null } | null } | null };
|
||||||
|
|
||||||
export type PositionsSubscriptionSubscriptionVariables = Types.Exact<{
|
export type PositionsSubscriptionSubscriptionVariables = Types.Exact<{
|
||||||
partyId: Types.Scalars['ID'];
|
partyId: Types.Scalars['ID'];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type PositionsSubscriptionSubscription = { __typename?: 'Subscription', positions: Array<{ __typename?: 'PositionUpdate', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, marketId: string }> };
|
export type PositionsSubscriptionSubscription = { __typename?: 'Subscription', positions: Array<{ __typename?: 'PositionUpdate', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, marketId: string, lossSocializationAmount: string, positionStatus: Types.PositionStatus }> };
|
||||||
|
|
||||||
export type MarginFieldsFragment = { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } };
|
export type MarginFieldsFragment = { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } };
|
||||||
|
|
||||||
@ -42,6 +42,8 @@ export const PositionFieldsFragmentDoc = gql`
|
|||||||
unrealisedPNL
|
unrealisedPNL
|
||||||
averageEntryPrice
|
averageEntryPrice
|
||||||
updatedAt
|
updatedAt
|
||||||
|
positionStatus
|
||||||
|
lossSocializationAmount
|
||||||
market {
|
market {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
@ -112,6 +114,8 @@ export const PositionsSubscriptionDocument = gql`
|
|||||||
averageEntryPrice
|
averageEntryPrice
|
||||||
updatedAt
|
updatedAt
|
||||||
marketId
|
marketId
|
||||||
|
lossSocializationAmount
|
||||||
|
positionStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -6,6 +6,7 @@ import type {
|
|||||||
MarginFieldsFragment,
|
MarginFieldsFragment,
|
||||||
} from './__generated__/Positions';
|
} from './__generated__/Positions';
|
||||||
import { getMetrics, rejoinPositionData } from './positions-data-providers';
|
import { getMetrics, rejoinPositionData } from './positions-data-providers';
|
||||||
|
import { PositionStatus } from '@vegaprotocol/types';
|
||||||
|
|
||||||
const accounts = [
|
const accounts = [
|
||||||
{
|
{
|
||||||
@ -78,6 +79,8 @@ const positions: PositionFieldsFragment[] = [
|
|||||||
__typename: 'Market',
|
__typename: 'Market',
|
||||||
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
|
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
|
||||||
},
|
},
|
||||||
|
lossSocializationAmount: '0',
|
||||||
|
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
__typename: 'Position',
|
__typename: 'Position',
|
||||||
@ -90,6 +93,8 @@ const positions: PositionFieldsFragment[] = [
|
|||||||
__typename: 'Market',
|
__typename: 'Market',
|
||||||
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
|
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
|
||||||
},
|
},
|
||||||
|
lossSocializationAmount: '100',
|
||||||
|
positionStatus: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -225,6 +230,10 @@ describe('getMetrics && rejoinPositionData', () => {
|
|||||||
expect(metrics[0].totalBalance).toEqual('926178496');
|
expect(metrics[0].totalBalance).toEqual('926178496');
|
||||||
expect(metrics[0].unrealisedPNL).toEqual('43804770');
|
expect(metrics[0].unrealisedPNL).toEqual('43804770');
|
||||||
expect(metrics[0].updatedAt).toEqual('2022-07-28T14:53:54.725477Z');
|
expect(metrics[0].updatedAt).toEqual('2022-07-28T14:53:54.725477Z');
|
||||||
|
expect(metrics[0].lossSocializationAmount).toEqual(
|
||||||
|
positions[0].lossSocializationAmount
|
||||||
|
);
|
||||||
|
expect(metrics[0].status).toEqual(positions[0].positionStatus);
|
||||||
|
|
||||||
expect(metrics[1].assetSymbol).toEqual('tDAI');
|
expect(metrics[1].assetSymbol).toEqual('tDAI');
|
||||||
expect(metrics[1].averageEntryPrice).toEqual('840158');
|
expect(metrics[1].averageEntryPrice).toEqual('840158');
|
||||||
@ -248,5 +257,9 @@ describe('getMetrics && rejoinPositionData', () => {
|
|||||||
expect(metrics[1].totalBalance).toEqual('896098819');
|
expect(metrics[1].totalBalance).toEqual('896098819');
|
||||||
expect(metrics[1].unrealisedPNL).toEqual('-9112700');
|
expect(metrics[1].unrealisedPNL).toEqual('-9112700');
|
||||||
expect(metrics[1].updatedAt).toEqual('2022-07-28T15:09:34.441143Z');
|
expect(metrics[1].updatedAt).toEqual('2022-07-28T15:09:34.441143Z');
|
||||||
|
expect(metrics[1].lossSocializationAmount).toEqual(
|
||||||
|
positions[1].lossSocializationAmount
|
||||||
|
);
|
||||||
|
expect(metrics[1].status).toEqual(positions[1].positionStatus);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
PositionsSubscriptionDocument,
|
PositionsSubscriptionDocument,
|
||||||
} from './__generated__/Positions';
|
} from './__generated__/Positions';
|
||||||
import { marginsDataProvider } from './margin-data-provider';
|
import { marginsDataProvider } from './margin-data-provider';
|
||||||
|
import type { PositionStatus } from '@vegaprotocol/types';
|
||||||
|
|
||||||
type PositionMarginLevel = Pick<
|
type PositionMarginLevel = Pick<
|
||||||
MarginFieldsFragment,
|
MarginFieldsFragment,
|
||||||
@ -38,6 +39,8 @@ interface PositionRejoined {
|
|||||||
updatedAt?: string | null;
|
updatedAt?: string | null;
|
||||||
market: MarketMaybeWithData | null;
|
market: MarketMaybeWithData | null;
|
||||||
margins: PositionMarginLevel | null;
|
margins: PositionMarginLevel | null;
|
||||||
|
lossSocializationAmount: string | null;
|
||||||
|
status: PositionStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Position {
|
export interface Position {
|
||||||
@ -62,6 +65,8 @@ export interface Position {
|
|||||||
unrealisedPNL: string;
|
unrealisedPNL: string;
|
||||||
searchPrice: string | undefined;
|
searchPrice: string | undefined;
|
||||||
updatedAt: string | null;
|
updatedAt: string | null;
|
||||||
|
lossSocializationAmount: string;
|
||||||
|
status: PositionStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Data {
|
export interface Data {
|
||||||
@ -178,6 +183,8 @@ export const getMetrics = (
|
|||||||
? searchPrice.multipliedBy(10 ** marketDecimalPlaces).toFixed(0)
|
? searchPrice.multipliedBy(10 ** marketDecimalPlaces).toFixed(0)
|
||||||
: undefined,
|
: undefined,
|
||||||
updatedAt: position.updatedAt || null,
|
updatedAt: position.updatedAt || null,
|
||||||
|
lossSocializationAmount: position.lossSocializationAmount || '0',
|
||||||
|
status: position.status,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return metrics;
|
return metrics;
|
||||||
@ -201,6 +208,8 @@ export const update = (
|
|||||||
openVolume: delta.openVolume,
|
openVolume: delta.openVolume,
|
||||||
averageEntryPrice: delta.averageEntryPrice,
|
averageEntryPrice: delta.averageEntryPrice,
|
||||||
updatedAt: delta.updatedAt,
|
updatedAt: delta.updatedAt,
|
||||||
|
lossSocializationAmount: delta.lossSocializationAmount,
|
||||||
|
positionStatus: delta.positionStatus,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
draft.unshift({
|
draft.unshift({
|
||||||
@ -267,6 +276,8 @@ export const rejoinPositionData = (
|
|||||||
market:
|
market:
|
||||||
marketsData?.find((market) => market.id === node.market.id) || null,
|
marketsData?.find((market) => market.id === node.market.id) || null,
|
||||||
margins: upgradeMarginsConnection(node.market.id, margins),
|
margins: upgradeMarginsConnection(node.market.id, margins),
|
||||||
|
lossSocializationAmount: node.lossSocializationAmount,
|
||||||
|
status: node.positionStatus,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import type { RenderResult } from '@testing-library/react';
|
import type { RenderResult } from '@testing-library/react';
|
||||||
import { act, render, screen } from '@testing-library/react';
|
import { act, render, screen, within } from '@testing-library/react';
|
||||||
import PositionsTable from './positions-table';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import PositionsTable, { OpenVolumeCell, PNLCell } from './positions-table';
|
||||||
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';
|
||||||
|
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
|
||||||
|
import type { ICellRendererParams } from 'ag-grid-community';
|
||||||
|
|
||||||
const singleRow: Position = {
|
const singleRow: Position = {
|
||||||
marketName: 'ETH/BTC (31 july 2022)',
|
marketName: 'ETH/BTC (31 july 2022)',
|
||||||
@ -26,6 +29,8 @@ const singleRow: Position = {
|
|||||||
searchPrice: '0',
|
searchPrice: '0',
|
||||||
updatedAt: '2022-07-27T15:02:58.400Z',
|
updatedAt: '2022-07-27T15:02:58.400Z',
|
||||||
marginAccountBalance: '12345600',
|
marginAccountBalance: '12345600',
|
||||||
|
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
|
||||||
|
lossSocializationAmount: '0',
|
||||||
};
|
};
|
||||||
|
|
||||||
const singleRowData = [singleRow];
|
const singleRowData = [singleRow];
|
||||||
@ -219,3 +224,97 @@ it('do not display close button if openVolume is zero', async () => {
|
|||||||
const cells = screen.getAllByRole('gridcell');
|
const cells = screen.getAllByRole('gridcell');
|
||||||
expect(cells[12].textContent).toEqual('');
|
expect(cells[12].textContent).toEqual('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('PNLCell', () => {
|
||||||
|
const props = {
|
||||||
|
data: undefined,
|
||||||
|
valueFormatted: '100',
|
||||||
|
};
|
||||||
|
it('renders a dash if no data', () => {
|
||||||
|
render(<PNLCell {...(props as ICellRendererParams)} />);
|
||||||
|
expect(screen.getByText('-')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders value if no loss socialisation has occurred', () => {
|
||||||
|
const props = {
|
||||||
|
data: {
|
||||||
|
...singleRow,
|
||||||
|
lossSocialisationAmount: '0',
|
||||||
|
},
|
||||||
|
valueFormatted: '100',
|
||||||
|
};
|
||||||
|
render(<PNLCell {...(props as ICellRendererParams)} />);
|
||||||
|
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders value with warning tooltip if loss socialisation occurred', async () => {
|
||||||
|
const props = {
|
||||||
|
data: {
|
||||||
|
...singleRow,
|
||||||
|
lossSocializationAmount: '500',
|
||||||
|
decimals: 2,
|
||||||
|
},
|
||||||
|
valueFormatted: '100',
|
||||||
|
};
|
||||||
|
render(<PNLCell {...(props as ICellRendererParams)} />);
|
||||||
|
const content = screen.getByText(props.valueFormatted);
|
||||||
|
expect(content).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.hover(content);
|
||||||
|
const tooltip = await screen.findByRole('tooltip');
|
||||||
|
expect(tooltip).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
// using within as radix renders tooltip content twice
|
||||||
|
within(tooltip).getByText('Lifetime loss socialisation deductions: 5.00')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OpenVolumeCell', () => {
|
||||||
|
const props = {
|
||||||
|
data: undefined,
|
||||||
|
valueFormatted: '100',
|
||||||
|
};
|
||||||
|
it('renders a dash if no data', () => {
|
||||||
|
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||||
|
expect(screen.getByText('-')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders value if no status is normal', () => {
|
||||||
|
const props = {
|
||||||
|
data: {
|
||||||
|
...singleRow,
|
||||||
|
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
|
||||||
|
},
|
||||||
|
valueFormatted: '100',
|
||||||
|
};
|
||||||
|
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||||
|
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders status with warning tooltip if not normal', async () => {
|
||||||
|
const props = {
|
||||||
|
data: {
|
||||||
|
...singleRow,
|
||||||
|
status: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
|
||||||
|
},
|
||||||
|
valueFormatted: '100',
|
||||||
|
};
|
||||||
|
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||||
|
const content = screen.getByText(props.valueFormatted);
|
||||||
|
expect(content).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||||
|
await userEvent.hover(content);
|
||||||
|
const tooltip = await screen.findByRole('tooltip');
|
||||||
|
expect(tooltip).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
// using within as radix renders tooltip content twice
|
||||||
|
within(tooltip).getByText(
|
||||||
|
`Status: ${PositionStatusMapping[props.data.status]}`
|
||||||
|
)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties, ReactNode } from 'react';
|
||||||
import type { CellRendererSelectorResult } from 'ag-grid-community';
|
import type { CellRendererSelectorResult } from 'ag-grid-community';
|
||||||
import type {
|
import type {
|
||||||
VegaValueFormatterParams,
|
VegaValueFormatterParams,
|
||||||
@ -15,6 +15,15 @@ import {
|
|||||||
signedNumberCssClass,
|
signedNumberCssClass,
|
||||||
signedNumberCssClassRules,
|
signedNumberCssClassRules,
|
||||||
} from '@vegaprotocol/datagrid';
|
} from '@vegaprotocol/datagrid';
|
||||||
|
import {
|
||||||
|
ButtonLink,
|
||||||
|
Tooltip,
|
||||||
|
TooltipCellComponent,
|
||||||
|
Link,
|
||||||
|
ExternalLink,
|
||||||
|
Icon,
|
||||||
|
ProgressBarCell,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import {
|
import {
|
||||||
volumePrefix,
|
volumePrefix,
|
||||||
toBigNum,
|
toBigNum,
|
||||||
@ -27,13 +36,9 @@ 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';
|
||||||
import {
|
|
||||||
ButtonLink,
|
|
||||||
Link,
|
|
||||||
ProgressBarCell,
|
|
||||||
TooltipCellComponent,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { getRowId } from './use-positions-data';
|
import { getRowId } from './use-positions-data';
|
||||||
|
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
|
||||||
|
import { useEnvironment } from '@vegaprotocol/environment';
|
||||||
|
|
||||||
interface Props extends TypedDataAgGrid<Position> {
|
interface Props extends TypedDataAgGrid<Position> {
|
||||||
onClose?: (data: Position) => void;
|
onClose?: (data: Position) => void;
|
||||||
@ -167,6 +172,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
cellRenderer={OpenVolumeCell}
|
||||||
/>
|
/>
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
headerName={t('Mark price')}
|
headerName={t('Mark price')}
|
||||||
@ -311,7 +317,6 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
|||||||
}}
|
}}
|
||||||
valueFormatter={({
|
valueFormatter={({
|
||||||
data,
|
data,
|
||||||
node,
|
|
||||||
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>):
|
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>):
|
||||||
| string
|
| string
|
||||||
| undefined => {
|
| undefined => {
|
||||||
@ -329,7 +334,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
|||||||
field="realisedPNL"
|
field="realisedPNL"
|
||||||
type="rightAligned"
|
type="rightAligned"
|
||||||
cellClassRules={signedNumberCssClassRules}
|
cellClassRules={signedNumberCssClassRules}
|
||||||
cellClass="text-right font-mono"
|
cellClass="font-mono text-right"
|
||||||
filter="agNumberColumnFilter"
|
filter="agNumberColumnFilter"
|
||||||
valueGetter={({
|
valueGetter={({
|
||||||
data,
|
data,
|
||||||
@ -348,13 +353,14 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
headerName={t('Unrealised PNL')}
|
headerName={t('Unrealised PNL')}
|
||||||
field="unrealisedPNL"
|
field="unrealisedPNL"
|
||||||
type="rightAligned"
|
type="rightAligned"
|
||||||
cellClassRules={signedNumberCssClassRules}
|
cellClassRules={signedNumberCssClassRules}
|
||||||
cellClass="text-right font-mono"
|
cellClass="font-mono text-right"
|
||||||
filter="agNumberColumnFilter"
|
filter="agNumberColumnFilter"
|
||||||
valueGetter={({
|
valueGetter={({
|
||||||
data,
|
data,
|
||||||
@ -373,6 +379,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
headerName={t('Updated')}
|
headerName={t('Updated')}
|
||||||
@ -409,3 +416,106 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export default PositionsTable;
|
export default PositionsTable;
|
||||||
|
|
||||||
|
export const PNLCell = ({
|
||||||
|
valueFormatted,
|
||||||
|
data,
|
||||||
|
}: VegaICellRendererParams<Position, 'realisedPNL'>) => {
|
||||||
|
const { VEGA_DOCS_URL } = useEnvironment();
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return <>-</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const losses = parseInt(data?.lossSocializationAmount ?? '0');
|
||||||
|
if (losses <= 0) {
|
||||||
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
|
return <>{valueFormatted}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lossesFormatted = addDecimalsFormatNumber(
|
||||||
|
data.lossSocializationAmount,
|
||||||
|
data.decimals
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WarningCell
|
||||||
|
tooltipContent={
|
||||||
|
<>
|
||||||
|
<p className="mb-2">
|
||||||
|
{t('Lifetime loss socialisation deductions: %s', lossesFormatted)}
|
||||||
|
</p>
|
||||||
|
{VEGA_DOCS_URL && (
|
||||||
|
<ExternalLink href={VEGA_DOCS_URL}>
|
||||||
|
{t('Read more about loss socialisation')}
|
||||||
|
</ExternalLink>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{valueFormatted}
|
||||||
|
</WarningCell>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OpenVolumeCell = ({
|
||||||
|
valueFormatted,
|
||||||
|
data,
|
||||||
|
}: VegaICellRendererParams<Position, 'openVolume'>) => {
|
||||||
|
const { VEGA_DOCS_URL } = useEnvironment();
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return <>-</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === PositionStatus.POSITION_STATUS_UNSPECIFIED) {
|
||||||
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
|
return <>{valueFormatted}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WarningCell
|
||||||
|
tooltipContent={
|
||||||
|
<>
|
||||||
|
<p className="mb-2">
|
||||||
|
{t('Your position was affected by market conditions')}
|
||||||
|
</p>
|
||||||
|
<p className="mb-2">
|
||||||
|
{t(
|
||||||
|
'Status: %s',
|
||||||
|
PositionStatusMapping[
|
||||||
|
PositionStatus.POSITION_STATUS_ORDERS_CLOSED
|
||||||
|
]
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
{VEGA_DOCS_URL && (
|
||||||
|
<ExternalLink href={VEGA_DOCS_URL}>
|
||||||
|
{t('Read more about position resolution')}
|
||||||
|
</ExternalLink>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{valueFormatted}
|
||||||
|
</WarningCell>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const WarningCell = ({
|
||||||
|
children,
|
||||||
|
tooltipContent,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
tooltipContent: ReactNode;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Tooltip description={tooltipContent}>
|
||||||
|
<div className="w-full flex items-center justify-between underline decoration-dashed underline-offest-2">
|
||||||
|
<span className="text-black dark:text-white mr-1">
|
||||||
|
<Icon name="warning-sign" size={3} />
|
||||||
|
</span>
|
||||||
|
<span className="text-ellipsis overflow-hidden">{children}</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { PositionStatus } from '@vegaprotocol/types';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import type { PartialDeep } from 'type-fest';
|
import type { PartialDeep } from 'type-fest';
|
||||||
import type {
|
import type {
|
||||||
@ -58,6 +59,8 @@ const positionFields: PositionFieldsFragment[] = [
|
|||||||
id: 'market-0',
|
id: 'market-0',
|
||||||
__typename: 'Market',
|
__typename: 'Market',
|
||||||
},
|
},
|
||||||
|
lossSocializationAmount: '0',
|
||||||
|
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
__typename: 'Position',
|
__typename: 'Position',
|
||||||
@ -70,8 +73,11 @@ const positionFields: PositionFieldsFragment[] = [
|
|||||||
id: 'market-1',
|
id: 'market-1',
|
||||||
__typename: 'Market',
|
__typename: 'Market',
|
||||||
},
|
},
|
||||||
|
lossSocializationAmount: '0',
|
||||||
|
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
__typename: 'Position',
|
||||||
realisedPNL: '230000',
|
realisedPNL: '230000',
|
||||||
openVolume: '1',
|
openVolume: '1',
|
||||||
unrealisedPNL: '-22519',
|
unrealisedPNL: '-22519',
|
||||||
@ -81,7 +87,8 @@ const positionFields: PositionFieldsFragment[] = [
|
|||||||
id: 'market-2',
|
id: 'market-2',
|
||||||
__typename: 'Market',
|
__typename: 'Market',
|
||||||
},
|
},
|
||||||
__typename: 'Position',
|
lossSocializationAmount: '0',
|
||||||
|
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1 +1,4 @@
|
|||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
|
import ResizeObserver from 'resize-observer-polyfill';
|
||||||
|
|
||||||
|
global.ResizeObserver = ResizeObserver;
|
||||||
|
@ -12,6 +12,7 @@ import type {
|
|||||||
OrderStatus,
|
OrderStatus,
|
||||||
OrderTimeInForce,
|
OrderTimeInForce,
|
||||||
OrderType,
|
OrderType,
|
||||||
|
PositionStatus,
|
||||||
ProposalRejectionReason,
|
ProposalRejectionReason,
|
||||||
ProposalState,
|
ProposalState,
|
||||||
Side,
|
Side,
|
||||||
@ -445,3 +446,11 @@ export const DispatchMetricLabels: DispatchMetricLabel = {
|
|||||||
DISPATCH_METRIC_MAKER_FEES_RECEIVED: 'Price maker fees earned',
|
DISPATCH_METRIC_MAKER_FEES_RECEIVED: 'Price maker fees earned',
|
||||||
DISPATCH_METRIC_MARKET_VALUE: 'Total market Value',
|
DISPATCH_METRIC_MARKET_VALUE: 'Total market Value',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PositionStatusMapping: {
|
||||||
|
[T in PositionStatus]: string;
|
||||||
|
} = {
|
||||||
|
POSITION_STATUS_CLOSED_OUT: 'Closed by network',
|
||||||
|
POSITION_STATUS_ORDERS_CLOSED: 'Maintained by network',
|
||||||
|
POSITION_STATUS_UNSPECIFIED: 'Normal',
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user