fix(positions): rework of the liquidation tooltip (#6070)

Co-authored-by: bwallacee <ben@vega.xyz>
This commit is contained in:
Art 2024-03-22 14:36:05 +01:00 committed by GitHub
parent 891e0d3d2f
commit 01e87443ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 229 additions and 160 deletions

View File

@ -341,31 +341,33 @@ describe('Closed', () => {
oracleDataMock, oracleDataMock,
]); ]);
const actionCell = screen await waitFor(async () => {
.getAllByRole('gridcell') const actionCell = screen
.find((el) => el.getAttribute('col-id') === 'market-actions'); .getAllByRole('gridcell')
.find((el) => el.getAttribute('col-id') === 'market-actions');
await userEvent.click( await userEvent.click(
within(actionCell as HTMLElement).getByTestId('dropdown-menu') within(actionCell as HTMLElement).getByTestId('dropdown-menu')
); );
expect(screen.getByRole('menu')).toBeInTheDocument(); expect(screen.getByRole('menu')).toBeInTheDocument();
expect( expect(
screen.getByRole('menuitem', { name: 'Copy Market ID' }) screen.getByRole('menuitem', { name: 'Copy Market ID' })
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole('menuitem', { name: 'View on Explorer' }) screen.getByRole('menuitem', { name: 'View on Explorer' })
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole('menuitem', { name: 'View settlement asset details' }) screen.getByRole('menuitem', { name: 'View settlement asset details' })
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole('menuitem', { name: 'View parent market' }) screen.getByRole('menuitem', { name: 'View parent market' })
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole('menuitem', { name: 'View successor market' }) screen.getByRole('menuitem', { name: 'View successor market' })
).toBeInTheDocument(); ).toBeInTheDocument();
});
}); });
it('successor market should be visible', async () => { it('successor market should be visible', async () => {

View File

@ -1,6 +1,6 @@
import { OrderbookManager } from '@vegaprotocol/market-depth'; import { OrderbookManager } from '@vegaprotocol/market-depth';
import { ViewType, useSidebar } from '../sidebar'; import { ViewType, useSidebar } from '../sidebar';
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket'; import { useDealTicketFormValues } from '@vegaprotocol/react-helpers';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export const OrderbookContainer = ({ marketId }: { marketId: string }) => { export const OrderbookContainer = ({ marketId }: { marketId: string }) => {

View File

@ -4,7 +4,9 @@ from vega_sim.null_service import VegaServiceNull
from datetime import datetime, timedelta from datetime import datetime, timedelta
from conftest import init_vega, cleanup_container from conftest import init_vega, cleanup_container
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
from actions.utils import wait_for_toast_confirmation from actions.vega import submit_order
from actions.utils import wait_for_toast_confirmation, change_keys
from wallet_config import PARTY_C, MM_WALLET
order_size = "order-size" order_size = "order-size"
order_price = "order-price" order_price = "order-price"
@ -149,3 +151,27 @@ def test_connect_vega_wallet(continuous_market, page: Page):
# TODO: accept wallet connection and assert wallet is connected. # TODO: accept wallet connection and assert wallet is connected.
expect(page.get_by_test_id("order-type-Limit")).to_be_checked() expect(page.get_by_test_id("order-type-Limit")).to_be_checked()
expect(page.get_by_test_id("order-price")).to_have_value("101") expect(page.get_by_test_id("order-price")).to_have_value("101")
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_liquidated_tooltip(continuous_market, vega: VegaServiceNull, page: Page):
tdai_id = vega.find_asset_id(symbol="tDAI")
vega.mint(
PARTY_C.name,
asset=tdai_id,
amount=20,
)
vega.wait_fn(1)
vega.wait_for_total_catchup()
submit_order(vega, PARTY_C.name, continuous_market, "SIDE_BUY", 1, 110)
submit_order(vega, "Key 1", continuous_market, "SIDE_SELL", 1, 110)
vega.wait_fn(1)
vega.wait_for_total_catchup()
page.goto(f"/#/markets/{continuous_market}")
change_keys(page, vega, PARTY_C.name)
submit_order(vega, MM_WALLET.name, continuous_market, "SIDE_BUY", 100, 90)
submit_order(vega, "Key 1", continuous_market, "SIDE_SELL", 100, 90)
vega.wait_fn(1)
vega.wait_for_total_catchup()
page.locator('[id="cell-openVolume-0"]').hover()
expect(page.get_by_test_id("tooltip-content").first).to_contain_text("")

View File

@ -1,7 +1,11 @@
import BigNumber from 'bignumber.js';
export const positiveClassNames = export const positiveClassNames =
'text-market-green-600 dark:text-market-green'; 'text-market-green-600 dark:text-market-green';
export const negativeClassNames = 'text-market-red dark:text-market-red'; export const negativeClassNames = 'text-market-red dark:text-market-red';
export const zeroClassNames = 'text-vega-orange dark:text-vega-orange';
const isPositive = ({ value }: { value: string | bigint | number }) => const isPositive = ({ value }: { value: string | bigint | number }) =>
!!value && !!value &&
((typeof value === 'string' && !value.startsWith('-')) || ((typeof value === 'string' && !value.startsWith('-')) ||
@ -12,6 +16,9 @@ const isNegative = ({ value }: { value: string | bigint | number }) =>
((typeof value === 'string' && value.startsWith('-')) || ((typeof value === 'string' && value.startsWith('-')) ||
((typeof value === 'number' || typeof value === 'bigint') && value < 0)); ((typeof value === 'number' || typeof value === 'bigint') && value < 0));
export const isZero = ({ value }: { value: string | bigint | number }) =>
BigNumber(value.toString()).isZero();
export const signedNumberCssClass = (value: string | bigint | number) => { export const signedNumberCssClass = (value: string | bigint | number) => {
if (isPositive({ value })) { if (isPositive({ value })) {
return positiveClassNames; return positiveClassNames;

View File

@ -2,7 +2,7 @@ import { useVegaTransactionStore } from '@vegaprotocol/web3';
import { import {
isStopOrderType, isStopOrderType,
useDealTicketFormValues, useDealTicketFormValues,
} from '../../hooks/use-form-values'; } from '@vegaprotocol/react-helpers';
import { StopOrder } from './deal-ticket-stop-order'; import { StopOrder } from './deal-ticket-stop-order';
import { import {
useStaticMarketData, useStaticMarketData,

View File

@ -1,6 +1,6 @@
import { Controller, type Control } from 'react-hook-form'; import { Controller, type Control } from 'react-hook-form';
import type { Market } from '@vegaprotocol/markets'; import type { Market } from '@vegaprotocol/markets';
import type { OrderFormValues } from '../../hooks/use-form-values'; import type { OrderFormValues } from '@vegaprotocol/react-helpers';
import { determinePriceStep, useValidateAmount } from '@vegaprotocol/utils'; import { determinePriceStep, useValidateAmount } from '@vegaprotocol/utils';
import { import {
TradingFormGroup, TradingFormGroup,

View File

@ -1,6 +1,6 @@
import { Controller, type Control } from 'react-hook-form'; import { Controller, type Control } from 'react-hook-form';
import type { Market } from '@vegaprotocol/markets'; import type { Market } from '@vegaprotocol/markets';
import type { OrderFormValues } from '../../hooks/use-form-values'; import type { OrderFormValues } from '@vegaprotocol/react-helpers';
import { useValidateAmount } from '@vegaprotocol/utils'; import { useValidateAmount } from '@vegaprotocol/utils';
import { import {
TradingFormGroup, TradingFormGroup,

View File

@ -5,11 +5,11 @@ import { generateMarket } from '../../test-helpers';
import { StopOrder } from './deal-ticket-stop-order'; import { StopOrder } from './deal-ticket-stop-order';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import type { StopOrderFormValues } from '../../hooks/use-form-values';
import { import {
type StopOrderFormValues,
DealTicketType, DealTicketType,
useDealTicketFormValues, useDealTicketFormValues,
} from '../../hooks/use-form-values'; } from '@vegaprotocol/react-helpers';
import { useFeatureFlags } from '@vegaprotocol/environment'; import { useFeatureFlags } from '@vegaprotocol/environment';
import { formatForInput } from '@vegaprotocol/utils'; import { formatForInput } from '@vegaprotocol/utils';
import { import {

View File

@ -48,8 +48,8 @@ import {
DealTicketType, DealTicketType,
dealTicketTypeToOrderType, dealTicketTypeToOrderType,
isStopOrderType, isStopOrderType,
} from '../../hooks/use-form-values'; type StopOrderFormValues,
import { type StopOrderFormValues } from '../../hooks/use-form-values'; } from '@vegaprotocol/react-helpers';
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-submission'; import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-submission';
import { DealTicketFeeDetails } from './deal-ticket-fee-details'; import { DealTicketFeeDetails } from './deal-ticket-fee-details';
import { validateExpiration } from '../../utils'; import { validateExpiration } from '../../utils';

View File

@ -16,7 +16,7 @@ import type { OrdersQuery } from '@vegaprotocol/orders';
import { import {
DealTicketType, DealTicketType,
useDealTicketFormValues, useDealTicketFormValues,
} from '../../hooks/use-form-values'; } from '@vegaprotocol/react-helpers';
import * as positionsTools from '@vegaprotocol/positions'; import * as positionsTools from '@vegaprotocol/positions';
import { OrdersDocument } from '@vegaprotocol/orders'; import { OrdersDocument } from '@vegaprotocol/orders';
import { formatForInput } from '@vegaprotocol/utils'; import { formatForInput } from '@vegaprotocol/utils';

View File

@ -67,14 +67,7 @@ import {
marginModeDataProvider, marginModeDataProvider,
} from '@vegaprotocol/accounts'; } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import { type OrderFormValues } from '../../hooks'; import { usePositionEstimate } from '../../hooks';
import {
DealTicketType,
dealTicketTypeToOrderType,
isStopOrderType,
useDealTicketFormValues,
usePositionEstimate,
} from '../../hooks';
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg'; import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
import noop from 'lodash/noop'; import noop from 'lodash/noop';
import { isNonPersistentOrder } from '../../utils/time-in-force-persistence'; import { isNonPersistentOrder } from '../../utils/time-in-force-persistence';
@ -85,6 +78,13 @@ import { DealTicketPriceTakeProfitStopLoss } from './deal-ticket-price-tp-sl';
import uniqueId from 'lodash/uniqueId'; import uniqueId from 'lodash/uniqueId';
import { determinePriceStep, determineSizeStep } from '@vegaprotocol/utils'; import { determinePriceStep, determineSizeStep } from '@vegaprotocol/utils';
import { useMaxSize } from '../../hooks/use-max-size'; import { useMaxSize } from '../../hooks/use-max-size';
import {
DealTicketType,
type OrderFormValues,
dealTicketTypeToOrderType,
isStopOrderType,
useDealTicketFormValues,
} from '@vegaprotocol/react-helpers';
export const REDUCE_ONLY_TOOLTIP = export const REDUCE_ONLY_TOOLTIP =
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.'; '"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.';

View File

@ -15,7 +15,7 @@ import {
import type { Market, StaticMarketData } from '@vegaprotocol/markets'; import type { Market, StaticMarketData } from '@vegaprotocol/markets';
import { compileGridData } from '../trading-mode-tooltip'; import { compileGridData } from '../trading-mode-tooltip';
import { MarketModeValidationType } from '../../constants'; import { MarketModeValidationType } from '../../constants';
import { DealTicketType } from '../../hooks/use-form-values'; import { DealTicketType } from '@vegaprotocol/react-helpers';
import * as RadioGroup from '@radix-ui/react-radio-group'; import * as RadioGroup from '@radix-ui/react-radio-group';
import classNames from 'classnames'; import classNames from 'classnames';
import { useFeatureFlags } from '@vegaprotocol/environment'; import { useFeatureFlags } from '@vegaprotocol/environment';

View File

@ -1,4 +1,3 @@
export * from './__generated__/EstimateOrder'; export * from './__generated__/EstimateOrder';
export * from './use-estimate-fees'; export * from './use-estimate-fees';
export * from './use-form-values';
export * from './use-position-estimate'; export * from './use-position-estimate';

View File

@ -6,7 +6,7 @@ import type {
import type { import type {
OrderFormValues, OrderFormValues,
StopOrderFormValues, StopOrderFormValues,
} from '../hooks/use-form-values'; } from '@vegaprotocol/react-helpers';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils'; import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
import { isPersistentOrder } from './time-in-force-persistence'; import { isPersistentOrder } from './time-in-force-persistence';

View File

@ -8,7 +8,7 @@ import {
} from './map-form-values-to-submission'; } from './map-form-values-to-submission';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types'; import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
import type { OrderFormValues } from '../hooks'; import type { OrderFormValues } from '@vegaprotocol/react-helpers';
import { type MarketFieldsFragment } from '@vegaprotocol/markets'; import { type MarketFieldsFragment } from '@vegaprotocol/markets';
describe('mapFormValuesToOrderSubmission', () => { describe('mapFormValuesToOrderSubmission', () => {

View File

@ -28,9 +28,9 @@
"View settlement asset details": "View settlement asset details", "View settlement asset details": "View settlement asset details",
"Worst case": "Worst case", "Worst case": "Worst case",
"Worst case liquidation price": "Worst case liquidation price", "Worst case liquidation price": "Worst case liquidation price",
"You did not have enough {{assetSymbol}} collateral to meet the maintenance margin requirements for your position, so it was closed by the network.": "You did not have enough {{assetSymbol}} collateral to meet the maintenance margin requirements for your position, so it was closed by the network.",
"You received less {{assetSymbol}} in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.": "You received less {{assetSymbol}} in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.", "You received less {{assetSymbol}} in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.": "You received less {{assetSymbol}} in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.",
"Your open orders were cancelled.": "Your open orders were cancelled.", "Your open orders were cancelled.": "Your open orders were cancelled.",
"Your position is distressed.": "Your position is distressed.", "Your position is distressed.": "Your position is distressed.",
"Your position was closed.": "Your position was closed." "Your position was closed.": "Your position was closed.",
"You did not have enough {{assetSymbol}} to meet the margin required for your position, so it was liquidated by the network at {{price}}.": "You did not have enough {{assetSymbol}} to meet the margin required for your position, so it was liquidated by the network at {{price}}."
} }

View File

@ -7,6 +7,7 @@ import { PositionStatus } from '@vegaprotocol/types';
import type { ICellRendererParams } from 'ag-grid-community'; import type { ICellRendererParams } from 'ag-grid-community';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { singleRow } from './positions.mock'; import { singleRow } from './positions.mock';
import { useLatestTrade } from '@vegaprotocol/trades';
jest.mock('./liquidation-price', () => ({ jest.mock('./liquidation-price', () => ({
LiquidationPrice: () => ( LiquidationPrice: () => (
@ -262,26 +263,15 @@ describe('Positions', () => {
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument(); expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
}); });
it('renders status with warning tooltip if orders were closed', () => {
const props = {
data: {
...singleRow,
status: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
},
valueFormatted: '100',
} as ICellRendererParams;
render(<OpenVolumeCell {...props} />);
const content = screen.getByText(props.valueFormatted as string);
expect(content).toBeInTheDocument();
expect(screen.getByTestId(/icon-/)).toBeInTheDocument();
});
it('renders status with warning tooltip if position was closed out', async () => { it('renders status with warning tooltip if position was closed out', async () => {
const props = { (useLatestTrade as jest.Mock).mockReturnValue({
data: { data: {
...singleRow, type: 'TYPE_NETWORK_CLOSE_OUT_BAD',
status: PositionStatus.POSITION_STATUS_CLOSED_OUT, price: '100',
}, },
});
const props = {
data: singleRow,
valueFormatted: '100', valueFormatted: '100',
} as ICellRendererParams; } as ICellRendererParams;
render(<OpenVolumeCell {...props} />); render(<OpenVolumeCell {...props} />);
@ -303,31 +293,23 @@ describe('Positions', () => {
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
}); });
it.each([ it('renders tooltip when positions has been closed out (liquidated)', async () => {
{ (useLatestTrade as jest.Mock).mockReturnValue({
status: PositionStatus.POSITION_STATUS_CLOSED_OUT, data: {
text: 'Your position was closed.', type: 'TYPE_NETWORK_CLOSE_OUT_BAD',
}, price: '100',
{ },
status: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
text: 'Your open orders were cancelled.',
},
{
status: PositionStatus.POSITION_STATUS_DISTRESSED,
text: 'Your position is distressed.',
},
])('renders content for $status', async (data) => {
await renderComponent({
...singleRow,
status: data.status,
}); });
await renderComponent(singleRow);
const cells = screen.getAllByRole('gridcell'); const cells = screen.getAllByRole('gridcell');
const cell = cells[1]; const cell = cells[1];
const tooltipTrigger = cell.querySelector('[data-state="closed"]'); const tooltipTrigger = cell.querySelector('[data-state="closed"]');
expect(tooltipTrigger).not.toBeNull(); expect(tooltipTrigger).not.toBeNull();
await userEvent.hover(tooltipTrigger as Element); await userEvent.hover(tooltipTrigger as Element);
const tooltip = within(await screen.findByRole('tooltip')); const tooltip = within(await screen.findByRole('tooltip'));
expect(tooltip.getByText(data.text)).toBeInTheDocument(); expect(
tooltip.getByText('Your position was closed.')
).toBeInTheDocument();
}); });
}); });

View File

@ -13,6 +13,8 @@ import {
type VegaValueGetterParams, type VegaValueGetterParams,
type TypedDataAgGrid, type TypedDataAgGrid,
type VegaICellRendererParams, type VegaICellRendererParams,
zeroClassNames,
isZero,
} from '@vegaprotocol/datagrid'; } from '@vegaprotocol/datagrid';
import { import {
ButtonLink, ButtonLink,
@ -36,6 +38,7 @@ import {
MarketTradingMode, MarketTradingMode,
PositionStatus, PositionStatus,
PositionStatusMapping, PositionStatusMapping,
TradeType,
} from '@vegaprotocol/types'; } from '@vegaprotocol/types';
import { DocsLinks, useFeatureFlags } from '@vegaprotocol/environment'; import { DocsLinks, useFeatureFlags } from '@vegaprotocol/environment';
import { PositionActionsDropdown } from './position-actions-dropdown'; import { PositionActionsDropdown } from './position-actions-dropdown';
@ -43,6 +46,7 @@ import { LiquidationPrice } from './liquidation-price';
import { useT } from '../use-t'; import { useT } from '../use-t';
import classnames from 'classnames'; import classnames from 'classnames';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { useLatestTrade } from '@vegaprotocol/trades';
interface Props extends TypedDataAgGrid<Position> { interface Props extends TypedDataAgGrid<Position> {
onClose?: (data: Position) => void; onClose?: (data: Position) => void;
@ -258,7 +262,10 @@ export const PositionsTable = ({
field: 'openVolume', field: 'openVolume',
type: 'rightAligned', type: 'rightAligned',
cellClass: 'font-mono text-right', cellClass: 'font-mono text-right',
cellClassRules: signedNumberCssClassRules, cellClassRules: {
...signedNumberCssClassRules,
[zeroClassNames]: isZero,
},
filter: 'agNumberColumnFilter', filter: 'agNumberColumnFilter',
sortable: false, sortable: false,
filterValueGetter: ({ data }: { data: Position }) => { filterValueGetter: ({ data }: { data: Position }) => {
@ -616,49 +623,51 @@ export const OpenVolumeCell = ({
data, data,
}: VegaICellRendererParams<Position, 'openVolume'>) => { }: VegaICellRendererParams<Position, 'openVolume'>) => {
const t = useT(); const t = useT();
const { data: latestTrade } = useLatestTrade(data?.marketId, data?.partyId);
if (!valueFormatted || !data || !data.notional) { if (!valueFormatted || !data || !data.notional) {
return <>-</>; return <>-</>;
} }
const POSITION_RESOLUTION_LINK = DocsLinks?.POSITION_RESOLUTION ?? ''; let positionStatus = PositionStatus.POSITION_STATUS_UNSPECIFIED;
let primaryTooltip; if (latestTrade?.type === TradeType.TYPE_NETWORK_CLOSE_OUT_BAD) {
switch (data.status) { positionStatus = PositionStatus.POSITION_STATUS_CLOSED_OUT;
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
primaryTooltip = t('Your position was closed.');
break;
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
primaryTooltip = t('Your open orders were cancelled.');
break;
case PositionStatus.POSITION_STATUS_DISTRESSED:
primaryTooltip = t('Your position is distressed.');
break;
} }
let secondaryTooltip; const POSITION_RESOLUTION_LINK = DocsLinks?.POSITION_RESOLUTION ?? '';
switch (data.status) {
case PositionStatus.POSITION_STATUS_CLOSED_OUT: const notional = addDecimalsFormatNumber(
secondaryTooltip = t( data.notional,
`You did not have enough {{assetSymbol}} collateral to meet the maintenance margin requirements for your position, so it was closed by the network.`, data.marketDecimalPlaces
{ assetSymbol: data.assetSymbol } );
);
break; const cellContent = (
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED: <StackedCell primary={valueFormatted} secondary={notional} />
secondaryTooltip = t( );
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
); if (positionStatus !== PositionStatus.POSITION_STATUS_CLOSED_OUT) {
break; // eslint-disable-next-line react/jsx-no-useless-fragment
case PositionStatus.POSITION_STATUS_DISTRESSED: return <>{cellContent}</>;
secondaryTooltip = t(
'The position was distressed, but could not be closed out - orders were removed from the book, and the open volume will be closed out once there is sufficient volume on the book.'
);
break;
default:
secondaryTooltip = t('Maintained by network');
} }
const description = (
const closeOutPrice = addDecimalsFormatNumber(
latestTrade?.price || '0',
data.marketDecimalPlaces
);
const description = positionStatus ===
PositionStatus.POSITION_STATUS_CLOSED_OUT && (
<> <>
<p className="mb-2">{primaryTooltip}</p> <p className="mb-2">{t('Your position was closed.')}</p>
<p className="mb-2">{secondaryTooltip}</p> <p className="mb-2">
{t(
'You did not have enough {{assetSymbol}} to meet the margin required for your position, so it was liquidated by the network at {{price}}.',
{
assetSymbol: data.assetSymbol,
price: closeOutPrice,
}
)}
</p>
<p className="mb-2"> <p className="mb-2">
{t('Status: {{status}}', { {t('Status: {{status}}', {
nsSeparator: '*', nsSeparator: '*',
@ -675,20 +684,6 @@ export const OpenVolumeCell = ({
</> </>
); );
const notional = addDecimalsFormatNumber(
data.notional,
data.marketDecimalPlaces
);
const cellContent = (
<StackedCell primary={valueFormatted} secondary={notional} />
);
if (data.status === PositionStatus.POSITION_STATUS_UNSPECIFIED) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{cellContent}</>;
}
return ( return (
<Tooltip description={description}> <Tooltip description={description}>
<div> <div>

View File

@ -15,3 +15,12 @@ i18n.use(initReactI18next).init({
}); });
global.ResizeObserver = ResizeObserver; global.ResizeObserver = ResizeObserver;
jest.mock('@vegaprotocol/trades', () => ({
...jest.requireActual('@vegaprotocol/trades'),
useLatestTrade: jest.fn(() => ({
data: undefined,
loading: false,
error: undefined,
})),
}));

View File

@ -9,5 +9,5 @@ export default {
}, },
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/react-helpers', coverageDirectory: '../../coverage/libs/react-helpers',
setupFilesAfterEnv: ['./jest.setup.js'], setupFilesAfterEnv: ['./src/setup-tests.ts'],
}; };

View File

@ -15,3 +15,4 @@ export * from './use-previous';
export { useScript } from './use-script'; export { useScript } from './use-script';
export { useUserAgent } from './use-user-agent'; export { useUserAgent } from './use-user-agent';
export * from './use-duration'; export * from './use-duration';
export * from './use-form-values';

View File

@ -0,0 +1,17 @@
import { useDataProvider } from '@vegaprotocol/data-provider';
import { tradesProvider } from '../lib/trades-data-provider';
import first from 'lodash/first';
export const useLatestTrade = (marketId?: string, partyId?: string) => {
const { data, loading, error } = useDataProvider({
dataProvider: tradesProvider,
variables: {
marketIds: [marketId || ''],
partyIds: [partyId || ''],
},
skip: !marketId || !partyId,
});
const latest = first(data);
return { data: latest, loading, error };
};

View File

@ -1,2 +1,3 @@
export * from './lib/trades-manager'; export * from './lib/trades-manager';
export * from './lib/__generated__/Trades'; export * from './lib/__generated__/Trades';
export * from './hooks/use-latest-trade';

View File

@ -7,10 +7,14 @@ fragment TradeFields on Trade {
market { market {
id id
} }
type
} }
query Trades($marketId: ID!, $pagination: Pagination) { query Trades($marketIds: [ID!], $partyIds: [ID!], $pagination: Pagination) {
trades(filter: { marketIds: [$marketId] }, pagination: $pagination) { trades(
filter: { marketIds: $marketIds, partyIds: $partyIds }
pagination: $pagination
) {
edges { edges {
node { node {
...TradeFields ...TradeFields
@ -33,10 +37,11 @@ fragment TradeUpdateFields on TradeUpdate {
createdAt createdAt
marketId marketId
aggressor aggressor
type
} }
subscription TradesUpdate($marketId: ID!) { subscription TradesUpdate($marketIds: [ID!], $partyIds: [ID!]) {
tradesStream(filter: { marketIds: [$marketId] }) { tradesStream(filter: { marketIds: $marketIds, partyIds: $partyIds }) {
...TradeUpdateFields ...TradeUpdateFields
} }
} }

View File

@ -3,24 +3,26 @@ 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 TradeFieldsFragment = { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, aggressor: Types.Side, market: { __typename?: 'Market', id: string } }; export type TradeFieldsFragment = { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, aggressor: Types.Side, type: Types.TradeType, market: { __typename?: 'Market', id: string } };
export type TradesQueryVariables = Types.Exact<{ export type TradesQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID']; marketIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
partyIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
pagination?: Types.InputMaybe<Types.Pagination>; pagination?: Types.InputMaybe<Types.Pagination>;
}>; }>;
export type TradesQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, aggressor: Types.Side, market: { __typename?: 'Market', id: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null }; export type TradesQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, aggressor: Types.Side, type: Types.TradeType, market: { __typename?: 'Market', id: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null };
export type TradeUpdateFieldsFragment = { __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side }; export type TradeUpdateFieldsFragment = { __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side, type: Types.TradeType };
export type TradesUpdateSubscriptionVariables = Types.Exact<{ export type TradesUpdateSubscriptionVariables = Types.Exact<{
marketId: Types.Scalars['ID']; marketIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
partyIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
}>; }>;
export type TradesUpdateSubscription = { __typename?: 'Subscription', tradesStream?: Array<{ __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side }> | null }; export type TradesUpdateSubscription = { __typename?: 'Subscription', tradesStream?: Array<{ __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side, type: Types.TradeType }> | null };
export const TradeFieldsFragmentDoc = gql` export const TradeFieldsFragmentDoc = gql`
fragment TradeFields on Trade { fragment TradeFields on Trade {
@ -32,6 +34,7 @@ export const TradeFieldsFragmentDoc = gql`
market { market {
id id
} }
type
} }
`; `;
export const TradeUpdateFieldsFragmentDoc = gql` export const TradeUpdateFieldsFragmentDoc = gql`
@ -42,11 +45,15 @@ export const TradeUpdateFieldsFragmentDoc = gql`
createdAt createdAt
marketId marketId
aggressor aggressor
type
} }
`; `;
export const TradesDocument = gql` export const TradesDocument = gql`
query Trades($marketId: ID!, $pagination: Pagination) { query Trades($marketIds: [ID!], $partyIds: [ID!], $pagination: Pagination) {
trades(filter: {marketIds: [$marketId]}, pagination: $pagination) { trades(
filter: {marketIds: $marketIds, partyIds: $partyIds}
pagination: $pagination
) {
edges { edges {
node { node {
...TradeFields ...TradeFields
@ -75,12 +82,13 @@ export const TradesDocument = gql`
* @example * @example
* const { data, loading, error } = useTradesQuery({ * const { data, loading, error } = useTradesQuery({
* variables: { * variables: {
* marketId: // value for 'marketId' * marketIds: // value for 'marketIds'
* partyIds: // value for 'partyIds'
* pagination: // value for 'pagination' * pagination: // value for 'pagination'
* }, * },
* }); * });
*/ */
export function useTradesQuery(baseOptions: Apollo.QueryHookOptions<TradesQuery, TradesQueryVariables>) { export function useTradesQuery(baseOptions?: Apollo.QueryHookOptions<TradesQuery, TradesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<TradesQuery, TradesQueryVariables>(TradesDocument, options); return Apollo.useQuery<TradesQuery, TradesQueryVariables>(TradesDocument, options);
} }
@ -92,8 +100,8 @@ export type TradesQueryHookResult = ReturnType<typeof useTradesQuery>;
export type TradesLazyQueryHookResult = ReturnType<typeof useTradesLazyQuery>; export type TradesLazyQueryHookResult = ReturnType<typeof useTradesLazyQuery>;
export type TradesQueryResult = Apollo.QueryResult<TradesQuery, TradesQueryVariables>; export type TradesQueryResult = Apollo.QueryResult<TradesQuery, TradesQueryVariables>;
export const TradesUpdateDocument = gql` export const TradesUpdateDocument = gql`
subscription TradesUpdate($marketId: ID!) { subscription TradesUpdate($marketIds: [ID!], $partyIds: [ID!]) {
tradesStream(filter: {marketIds: [$marketId]}) { tradesStream(filter: {marketIds: $marketIds, partyIds: $partyIds}) {
...TradeUpdateFields ...TradeUpdateFields
} }
} }
@ -111,11 +119,12 @@ export const TradesUpdateDocument = gql`
* @example * @example
* const { data, loading, error } = useTradesUpdateSubscription({ * const { data, loading, error } = useTradesUpdateSubscription({
* variables: { * variables: {
* marketId: // value for 'marketId' * marketIds: // value for 'marketIds'
* partyIds: // value for 'partyIds'
* }, * },
* }); * });
*/ */
export function useTradesUpdateSubscription(baseOptions: Apollo.SubscriptionHookOptions<TradesUpdateSubscription, TradesUpdateSubscriptionVariables>) { export function useTradesUpdateSubscription(baseOptions?: Apollo.SubscriptionHookOptions<TradesUpdateSubscription, TradesUpdateSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<TradesUpdateSubscription, TradesUpdateSubscriptionVariables>(TradesUpdateDocument, options); return Apollo.useSubscription<TradesUpdateSubscription, TradesUpdateSubscriptionVariables>(TradesUpdateDocument, options);
} }

View File

@ -23,10 +23,14 @@ export const MAX_TRADES = 500;
const getData = ( const getData = (
responseData: TradesQuery | null responseData: TradesQuery | null
): (TradeFieldsFragment & Cursor)[] => ): (TradeFieldsFragment & Cursor)[] =>
responseData?.trades?.edges.map<TradeFieldsFragment & Cursor>((edge) => ({ orderBy(
...edge.node, responseData?.trades?.edges.map<TradeFieldsFragment & Cursor>((edge) => ({
cursor: edge.cursor, ...edge.node,
})) || []; cursor: edge.cursor,
})),
'createdAt',
'desc'
) || [];
const getDelta = (subscriptionData: TradesUpdateSubscription) => const getDelta = (subscriptionData: TradesUpdateSubscription) =>
subscriptionData?.tradesStream || []; subscriptionData?.tradesStream || [];
@ -101,7 +105,10 @@ export const tradesProvider = makeDataProvider<
last: MAX_TRADES, last: MAX_TRADES,
}, },
fetchPolicy: 'no-cache', fetchPolicy: 'no-cache',
getSubscriptionVariables: ({ marketId }) => ({ marketId }), getSubscriptionVariables: ({ marketIds, partyIds }) => ({
marketIds,
partyIds,
}),
}); });
export const tradesWithMarketProvider = makeDerivedDataProvider< export const tradesWithMarketProvider = makeDerivedDataProvider<

View File

@ -1,7 +1,7 @@
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import { tradesWithMarketProvider } from './trades-data-provider'; import { tradesWithMarketProvider } from './trades-data-provider';
import { TradesTable } from './trades-table'; import { TradesTable } from './trades-table';
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket'; import { useDealTicketFormValues } from '@vegaprotocol/react-helpers';
import type { useDataGridEvents } from '@vegaprotocol/datagrid'; import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { useT } from './use-t'; import { useT } from './use-t';
@ -19,7 +19,7 @@ export const TradesManager = ({
const { data, error } = useDataProvider({ const { data, error } = useDataProvider({
dataProvider: tradesWithMarketProvider, dataProvider: tradesWithMarketProvider,
variables: { marketId }, variables: { marketIds: [marketId] },
}); });
return ( return (

View File

@ -2,7 +2,7 @@ import { act, render, screen } from '@testing-library/react';
import { getTimeFormat } from '@vegaprotocol/utils'; import { getTimeFormat } from '@vegaprotocol/utils';
import { SELL_CLASS, TradesTable, BUY_CLASS } from './trades-table'; import { SELL_CLASS, TradesTable, BUY_CLASS } from './trades-table';
import type { Trade } from './trades-data-provider'; import type { Trade } from './trades-data-provider';
import { Side } from '@vegaprotocol/types'; import { Side, TradeType } from '@vegaprotocol/types';
const trade: Trade = { const trade: Trade = {
__typename: 'Trade', __typename: 'Trade',
@ -17,6 +17,7 @@ const trade: Trade = {
decimalPlaces: 2, decimalPlaces: 2,
positionDecimalPlaces: 2, positionDecimalPlaces: 2,
} as Trade['market'], } as Trade['market'],
type: TradeType.TYPE_DEFAULT,
}; };
describe('TradesTable', () => { describe('TradesTable', () => {

View File

@ -1,4 +1,4 @@
import { Side } from '@vegaprotocol/types'; import { Side, TradeType } 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 {
@ -45,6 +45,7 @@ export const tradesUpdateSubscription = (
createdAt: '2022-04-06T16:19:42.692598951Z', createdAt: '2022-04-06T16:19:42.692598951Z',
marketId: 'market-0', marketId: 'market-0',
aggressor: Side.SIDE_BUY, aggressor: Side.SIDE_BUY,
type: TradeType.TYPE_DEFAULT,
}, },
], ],
}; };
@ -62,6 +63,7 @@ const trades: TradeFieldsFragment[] = [
id: 'market-0', id: 'market-0',
__typename: 'Market', __typename: 'Market',
}, },
type: TradeType.TYPE_DEFAULT,
__typename: 'Trade', __typename: 'Trade',
}, },
{ {
@ -74,6 +76,7 @@ const trades: TradeFieldsFragment[] = [
id: 'market-0', id: 'market-0',
__typename: 'Market', __typename: 'Market',
}, },
type: TradeType.TYPE_DEFAULT,
__typename: 'Trade', __typename: 'Trade',
}, },
{ {
@ -86,6 +89,7 @@ const trades: TradeFieldsFragment[] = [
id: 'market-0', id: 'market-0',
__typename: 'Market', __typename: 'Market',
}, },
type: TradeType.TYPE_DEFAULT,
__typename: 'Trade', __typename: 'Trade',
}, },
]; ];

View File

@ -106,6 +106,7 @@ export const DepositStatusMapping: {
} = { } = {
STATUS_CANCELLED: 'Cancelled', STATUS_CANCELLED: 'Cancelled',
STATUS_FINALIZED: 'Finalized', STATUS_FINALIZED: 'Finalized',
STATUS_DUPLICATE_REJECTED: 'Duplicate rejected',
STATUS_OPEN: 'Open', STATUS_OPEN: 'Open',
STATUS_DUPLICATE_REJECTED: 'Rejected due to duplicate', STATUS_DUPLICATE_REJECTED: 'Rejected due to duplicate',
}; };

View File

@ -1,10 +1,13 @@
import BigNumber from 'bignumber.js';
/** /**
* Returns a number prefixed with either a '-' or a '+'. The open volume field * Returns a number prefixed with either a '-' or a '+'. The open volume field
* already comes with a '-' if negative so we only need to actually prefix if * already comes with a '-' if negative so we only need to actually prefix if
* its a positive value * its a positive value
*/ */
export function volumePrefix(value: string): string { export function volumePrefix(value: string): string {
if (value === '0' || value.startsWith('-')) { const isZero = BigNumber(value).isZero();
if (isZero || value.startsWith('-')) {
return value; return value;
} }