feat(trading): show positions for all connected keys (#3858)

This commit is contained in:
Bartłomiej Głownia 2023-05-25 12:59:08 +02:00 committed by GitHub
parent 1ae1fdff5e
commit f364dabe2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 424 additions and 419 deletions

View File

@ -1,6 +1,7 @@
import { checkSorting } from '@vegaprotocol/cypress';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { marketsDataQuery } from '@vegaprotocol/mock';
import { positionsQuery } from '@vegaprotocol/mock';
beforeEach(() => {
cy.mockTradingPage();
@ -16,9 +17,27 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
});
it('renders positions on portfolio page', () => {
cy.mockGQL((req) => {
const positions = positionsQuery();
if (positions.positions?.edges) {
positions.positions.edges.push(
...positions.positions.edges.map((edge) => ({
...edge,
node: {
...edge.node,
party: {
...edge.node.party,
id: 'vega-1',
},
},
}))
);
}
aliasGQLQuery(req, 'Positions', positions);
});
cy.visit('/#/portfolio');
cy.getByTestId('Positions').click();
validatePositionsDisplayed();
validatePositionsDisplayed(true);
});
describe('renders position among some graphql errors', () => {
it('rows should be displayed despite errors', () => {
@ -56,7 +75,9 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
cy.getByTestId('tab-positions')
.first()
.within(() => {
cy.get('[row-id="market-2"]')
cy.get(
'[row-id="02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65-market-2"]'
)
.eq(1)
.within(() => {
emptyCells.forEach((cell) => {
@ -103,9 +124,11 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
const marketsSortedDefault = [
'ACTIVE MARKET',
'Apple Monthly (30 Jun 2022)',
'SUSPENDED MARKET',
];
const marketsSortedAsc = ['ACTIVE MARKET', 'Apple Monthly (30 Jun 2022)'];
const marketsSortedDesc = [
'SUSPENDED MARKET',
'Apple Monthly (30 Jun 2022)',
'ACTIVE MARKET',
];
@ -119,9 +142,13 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
});
it('sorting by notional', () => {
cy.visit('/#/markets/market-0');
const marketsSortedDefault = ['276,761.40348', '46,126.90058'];
const marketsSortedAsc = ['46,126.90058', '276,761.40348'];
const marketsSortedDesc = ['276,761.40348', '46,126.90058'];
const marketsSortedDefault = [
'276,761.40348',
'46,126.90058',
'1,688.20',
];
const marketsSortedAsc = ['1,688.20', '46,126.90058', '276,761.40348'];
const marketsSortedDesc = ['276,761.40348', '46,126.90058', '1,688.20'];
cy.getByTestId('Positions').click();
checkSorting(
'notional',
@ -132,9 +159,9 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
});
it('sorting by unrealisedPNL', () => {
cy.visit('/#/markets/market-0');
const marketsSortedDefault = ['8.95', '-0.22519'];
const marketsSortedAsc = ['-0.22519', '8.95'];
const marketsSortedDesc = ['8.95', '-0.22519'];
const marketsSortedDefault = ['8.95', '-0.22519', '8.95'];
const marketsSortedAsc = ['-0.22519', '8.95', '8.95'];
const marketsSortedDesc = ['8.95', '8.95', '-0.22519'];
cy.getByTestId('Positions').click();
checkSorting(
'unrealisedPNL',
@ -145,7 +172,7 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
});
});
function validatePositionsDisplayed() {
function validatePositionsDisplayed(multiKey = false) {
cy.getByTestId('tab-positions').should('be.visible');
cy.getByTestId('tab-positions').within(() => {
cy.get('[col-id="marketName"]')
@ -165,10 +192,11 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
cy.wrap($prices).invoke('text').should('not.be.empty');
});
if (!multiKey) {
cy.get('[col-id="currentLeverage"]').should('contain.text', '2.846.1');
cy.get('[col-id="marginAccountBalance"]') // margin allocated
.should('contain.text', '0.01');
}
cy.get('[col-id="unrealisedPNL"]').each(($unrealisedPnl) => {
cy.wrap($unrealisedPnl).invoke('text').should('not.be.empty');
@ -184,6 +212,6 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
cy.get('.ag-popup').should('contain.text', 'Mark price x open volume');
});
cy.getByTestId('close-position').should('be.visible').and('have.length', 2);
cy.getByTestId('close-position').should('be.visible').and('have.length', 3);
}
});

View File

@ -175,6 +175,10 @@ describe('Closed', () => {
__typename: 'Market',
id: marketId,
},
party: {
__typename: 'Party',
id: pubKey,
},
};
};
const position = createPosition();
@ -182,21 +186,17 @@ describe('Closed', () => {
request: {
query: PositionsDocument,
variables: {
partyId: pubKey,
partyIds: [pubKey],
},
},
result: {
data: {
party: {
__typename: 'Party',
id: pubKey,
positionsConnection: {
positions: {
__typename: 'PositionConnection',
edges: [{ __typename: 'PositionEdge', node: position }],
},
},
},
},
};
beforeAll(() => {

View File

@ -64,7 +64,7 @@ export const Closed = () => {
});
const { data: positionData } = usePositionsQuery({
variables: {
partyId: pubKey || '',
partyIds: pubKey ? [pubKey] : [],
},
skip: !pubKey,
});
@ -72,11 +72,9 @@ export const Closed = () => {
// find a position for each market and add the realised pnl to
// a normalized object
const rowData = compact(marketData).map((market) => {
const position = positionData?.party?.positionsConnection?.edges?.find(
(edge) => {
const position = positionData?.positions?.edges?.find((edge) => {
return edge.node.market.id === market.id;
}
);
});
const instrument = market.tradableInstrument.instrument;

View File

@ -54,6 +54,7 @@ export const Portfolio = () => {
onMarketClick={onMarketClick}
noBottomPlaceholder
storeKey="portfolioPositions"
allKeys
/>
</VegaWalletContainer>
</Tab>

View File

@ -89,6 +89,9 @@ const cacheConfig: InMemoryCacheConfig = {
Party: {
keyFields: false,
},
Position: {
keyFields: ['market', ['id'], 'party', ['id']],
},
Fees: {
keyFields: false,
},

View File

@ -1,10 +1,9 @@
import { useRef, useMemo, memo, useCallback } from 'react';
import { useRef, useMemo, memo } from 'react';
import { t } from '@vegaprotocol/i18n';
import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import type { AccountFields } from './accounts-data-provider';
import { aggregatedAccountsDataProvider } from './accounts-data-provider';
import type { PinnedAsset } from './accounts-table';
import { AccountTable } from './accounts-table';
@ -36,16 +35,8 @@ export const AccountManager = ({
dataProvider: aggregatedAccountsDataProvider,
variables,
});
const setId = useCallback(
(data: AccountFields, id: string) => ({
...data,
asset: { ...data.asset, id },
}),
[]
);
const bottomPlaceholderProps = useBottomPlaceholder<AccountFields>({
const bottomPlaceholderProps = useBottomPlaceholder({
gridRef,
setId,
disabled: noBottomPlaceholder,
});

View File

@ -154,7 +154,11 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
{...props}
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No accounts')}
getRowId={({ data }: { data: AccountFields }) => data.asset.id}
getRowId={({
data,
}: {
data: AccountFields & { isLastPlaceholder?: boolean; id?: string };
}) => (data.isLastPlaceholder && data.id ? data.id : data.asset.id)}
ref={ref}
tooltipShowDelay={500}
rowData={props.rowData?.filter(

View File

@ -164,7 +164,9 @@ interface DataProviderParams<
Data,
SubscriptionData,
Delta,
Variables extends OperationVariables | undefined = undefined
Variables extends OperationVariables | undefined = undefined,
SubscriptionVariables extends OperationVariables | undefined = Variables,
QueryVariables extends OperationVariables | undefined = Variables
> {
query: Query<QueryData>;
subscriptionQuery?: Query<SubscriptionData>;
@ -181,6 +183,10 @@ interface DataProviderParams<
resetDelay?: number;
additionalContext?: Record<string, unknown>;
errorPolicyGuard?: (graphqlErrors: GraphQLErrors) => boolean;
getQueryVariables?: (variables: Variables) => QueryVariables;
getSubscriptionVariables?: (
variables: Variables
) => SubscriptionVariables | SubscriptionVariables[];
}
/**
@ -199,7 +205,9 @@ function makeDataProviderInternal<
Data,
SubscriptionData,
Delta,
Variables extends OperationVariables | undefined = undefined
Variables extends OperationVariables | undefined = undefined,
QueryVariables extends OperationVariables | undefined = Variables,
SubscriptionVariables extends OperationVariables | undefined = Variables
>({
query,
subscriptionQuery,
@ -211,12 +219,16 @@ function makeDataProviderInternal<
resetDelay,
additionalContext,
errorPolicyGuard,
getQueryVariables,
getSubscriptionVariables,
}: DataProviderParams<
QueryData,
Data,
SubscriptionData,
Delta,
Variables
Variables,
QueryVariables,
SubscriptionVariables
>): Subscribe<Data, Delta, Variables> {
// list of callbacks passed through subscribe call
const callbacks: UpdateCallback<Data, Delta>[] = [];
@ -230,7 +242,7 @@ function makeDataProviderInternal<
let loading = true;
let loaded = false;
let client: ApolloClient<object>;
let subscription: Subscription | undefined;
let subscription: Subscription[] | undefined;
let pageInfo: PageInfo | null = null;
let totalCount: number | undefined;
@ -263,7 +275,7 @@ function makeDataProviderInternal<
.query<QueryData>({
query,
variables: {
...variables,
...(getQueryVariables ? getQueryVariables(variables) : variables),
...(pagination && {
// let the variables pagination be prior to provider param
pagination: {
@ -343,6 +355,33 @@ function makeDataProviderInternal<
}
};
const subscriptionSubscribe = () => {
if (!subscriptionQuery || !getDelta || !update) {
return;
}
const subscriptionVariables = getSubscriptionVariables
? getSubscriptionVariables(variables)
: variables;
subscription = ([] as (OperationVariables | undefined)[])
.concat(subscriptionVariables)
.map((variables) =>
client
.subscribe<SubscriptionData>({
query: subscriptionQuery,
variables,
fetchPolicy,
})
.subscribe(onNext, onError)
);
};
const subscriptionUnsubscribe = () => {
if (subscription) {
subscription.forEach((subscription) => subscription.unsubscribe());
}
subscription = undefined;
};
const initialFetch = async (isUpdate = false) => {
if (!client) {
return;
@ -392,10 +431,7 @@ function makeDataProviderInternal<
}
// if error will occur data provider stops subscription
error = e as Error;
if (subscription) {
subscription.unsubscribe();
}
subscription = undefined;
subscriptionUnsubscribe();
} finally {
loading = false;
notifyAll({ isUpdate });
@ -439,10 +475,7 @@ function makeDataProviderInternal<
const onError = (e: Error) => {
error = e;
if (subscription) {
subscription.unsubscribe();
subscription = undefined;
}
subscriptionUnsubscribe();
notifyAll();
};
@ -460,13 +493,7 @@ function makeDataProviderInternal<
return;
}
if (subscriptionQuery && getDelta && update) {
subscription = client
.subscribe<SubscriptionData>({
query: subscriptionQuery,
variables,
fetchPolicy,
})
.subscribe(onNext, onError);
subscriptionSubscribe();
}
await initialFetch();
};
@ -475,8 +502,7 @@ function makeDataProviderInternal<
if (!subscription) {
return;
}
subscription.unsubscribe();
subscription = undefined;
subscriptionUnsubscribe();
data = null;
error = undefined;
loading = false;
@ -590,14 +616,18 @@ export function makeDataProvider<
Data,
SubscriptionData,
Delta,
Variables extends OperationVariables | undefined = undefined
Variables extends OperationVariables | undefined = undefined,
SubscriptionVariables extends OperationVariables | undefined = Variables,
QueryVariables extends OperationVariables | undefined = Variables
>(
params: DataProviderParams<
QueryData,
Data,
SubscriptionData,
Delta,
Variables
Variables,
SubscriptionVariables,
QueryVariables
>
): Subscribe<Data, Delta, Variables> {
const getInstance = memoize<Data, Delta, Variables>(() =>

View File

@ -4,30 +4,23 @@ import type { AgGridReact } from 'ag-grid-react';
import type { IsFullWidthRowParams, RowHeightParams } from 'ag-grid-community';
const NO_HOVER_CSS_RULE = { 'no-hover': 'data?.isLastPlaceholder' };
const ROW_ID = 'bottomPlaceholder';
const ROW_ID = 'bottom-placeholder';
const fullWidthCellRenderer = () => null;
const isFullWidthRow = (params: IsFullWidthRowParams) =>
params.rowNode.data?.isLastPlaceholder;
interface Props<T> {
interface Props {
gridRef: RefObject<AgGridReact>;
setId?: (data: T, id: string) => T;
disabled?: boolean;
}
// eslint-disable-next-line @typescript-eslint/ban-types
export const useBottomPlaceholder = <T extends {}>({
gridRef,
setId,
disabled,
}: Props<T>) => {
export const useBottomPlaceholder = ({ gridRef, disabled }: Props) => {
const onBodyScrollEnd = useCallback(() => {
const rowCont = gridRef.current?.api.getDisplayedRowCount() ?? 0;
if (rowCont) {
const lastRow = gridRef.current?.api.getDisplayedRowAtIndex(rowCont - 1);
if (lastRow && lastRow.data) {
const placeholderRow = setId
? setId({ ...lastRow.data, isLastPlaceholder: true }, ROW_ID)
: {
const placeholderRow = {
...lastRow.data,
isLastPlaceholder: true,
id: ROW_ID,
@ -38,7 +31,7 @@ export const useBottomPlaceholder = <T extends {}>({
gridRef.current?.api.applyTransaction(transaction);
}
}
}, [gridRef, setId]);
}, [gridRef]);
const onRowsChanged = useCallback(() => {
const placeholderNode = gridRef.current?.api.getRowNode(ROW_ID);

View File

@ -5,7 +5,6 @@ import { t } from '@vegaprotocol/i18n';
import { FillsTable } from './fills-table';
import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community';
import { useFillsList } from './use-fills-list';
import type { Trade } from './fills-data-provider';
import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
interface FillsManagerProps {
@ -65,7 +64,7 @@ export const FillsManager = ({
}, []);
const { isFullWidthRow, fullWidthCellRenderer, rowClassRules, getRowHeight } =
useBottomPlaceholder<Trade>({
useBottomPlaceholder({
gridRef,
});

View File

@ -187,7 +187,7 @@ const marketFieldsFragments: MarketFieldsFragment[] = [
name: 'Apple Monthly (30 Jun 2022)',
product: {
settlementAsset: {
id: 'asset-2',
id: 'asset-id',
name: '',
symbol: 'tUSDC',
decimals: 5,

View File

@ -87,7 +87,7 @@ export const OrderListManager = ({
const {
onFilterChanged: bottomPlaceholderOnFilterChanged,
...bottomPlaceholderProps
} = useBottomPlaceholder<Order>({
} = useBottomPlaceholder({
gridRef,
disabled: !enforceBottomPlaceholder && !isReadOnly && !hasAmendableOrder,
});

View File

@ -9,12 +9,13 @@ fragment PositionFields on Position {
market {
id
}
party {
id
}
}
query Positions($partyId: ID!) {
party(id: $partyId) {
id
positionsConnection {
query Positions($partyIds: [ID!]!) {
positions(filter: { partyIds: $partyIds }) {
edges {
node {
...PositionFields
@ -22,7 +23,6 @@ query Positions($partyId: ID!) {
}
}
}
}
subscription PositionsSubscription($partyId: ID!) {
positions(partyId: $partyId) {
@ -34,6 +34,7 @@ subscription PositionsSubscription($partyId: ID!) {
marketId
lossSocializationAmount
positionStatus
partyId
}
}

View File

@ -3,21 +3,21 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
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 PositionFieldsFragment = { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, positionStatus: Types.PositionStatus, lossSocializationAmount: string, market: { __typename?: 'Market', id: string }, party: { __typename?: 'Party', id: string } };
export type PositionsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
partyIds: Array<Types.Scalars['ID']> | 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, positionStatus: Types.PositionStatus, lossSocializationAmount: string, market: { __typename?: 'Market', id: string } } }> | null } | null } | null };
export type PositionsQuery = { __typename?: 'Query', positions?: { __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 }, party: { __typename?: 'Party', id: string } } }> | null } | null };
export type PositionsSubscriptionSubscriptionVariables = Types.Exact<{
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, lossSocializationAmount: string, positionStatus: Types.PositionStatus }> };
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, partyId: string }> };
export type MarginFieldsFragment = { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } };
@ -57,6 +57,9 @@ export const PositionFieldsFragmentDoc = gql`
market {
id
}
party {
id
}
}
`;
export const MarginFieldsFragmentDoc = gql`
@ -74,17 +77,14 @@ export const MarginFieldsFragmentDoc = gql`
}
`;
export const PositionsDocument = gql`
query Positions($partyId: ID!) {
party(id: $partyId) {
id
positionsConnection {
query Positions($partyIds: [ID!]!) {
positions(filter: {partyIds: $partyIds}) {
edges {
node {
...PositionFields
}
}
}
}
}
${PositionFieldsFragmentDoc}`;
@ -100,7 +100,7 @@ export const PositionsDocument = gql`
* @example
* const { data, loading, error } = usePositionsQuery({
* variables: {
* partyId: // value for 'partyId'
* partyIds: // value for 'partyIds'
* },
* });
*/
@ -126,6 +126,7 @@ export const PositionsSubscriptionDocument = gql`
marketId
lossSocializationAmount
positionStatus
partyId
}
}
`;

View File

@ -7,12 +7,14 @@ export const PositionsContainer = ({
onMarketClick,
noBottomPlaceholder,
storeKey,
allKeys,
}: {
onMarketClick?: (marketId: string) => void;
noBottomPlaceholder?: boolean;
storeKey?: string;
allKeys?: boolean;
}) => {
const { pubKey, isReadOnly } = useVegaWallet();
const { pubKey, pubKeys, isReadOnly } = useVegaWallet();
if (!pubKey) {
return (
@ -21,9 +23,19 @@ export const PositionsContainer = ({
</Splash>
);
}
const partyIds = [pubKey];
if (allKeys && pubKeys) {
partyIds.push(
...pubKeys
.map(({ publicKey }) => publicKey)
.filter((publicKey) => publicKey !== pubKey)
);
}
return (
<PositionsManager
partyId={pubKey}
partyIds={partyIds}
onMarketClick={onMarketClick}
isReadOnly={isReadOnly}
noBottomPlaceholder={noBottomPlaceholder}

View File

@ -1,10 +1,7 @@
import * as Schema from '@vegaprotocol/types';
import type { Account } from '@vegaprotocol/accounts';
import type { MarketWithData } from '@vegaprotocol/markets';
import type {
PositionFieldsFragment,
MarginFieldsFragment,
} from './__generated__/Positions';
import type { PositionFieldsFragment } from './__generated__/Positions';
import { getMetrics, rejoinPositionData } from './positions-data-providers';
import { PositionStatus } from '@vegaprotocol/types';
@ -79,6 +76,9 @@ const positions: PositionFieldsFragment[] = [
__typename: 'Market',
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
},
party: {
id: 'partyId',
},
lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},
@ -93,6 +93,9 @@ const positions: PositionFieldsFragment[] = [
__typename: 'Market',
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
},
party: {
id: 'partyId',
},
lossSocializationAmount: '100',
positionStatus: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
},
@ -113,6 +116,8 @@ const marketsData = [
product: {
settlementAsset: {
symbol: 'tDAI',
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
decimals: 5,
},
},
},
@ -140,6 +145,8 @@ const marketsData = [
product: {
settlementAsset: {
symbol: 'tDAI',
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
decimals: 5,
},
},
},
@ -155,65 +162,23 @@ const marketsData = [
},
] as MarketWithData[];
const margins: MarginFieldsFragment[] = [
{
__typename: 'MarginLevels',
maintenanceLevel: '0',
searchLevel: '0',
initialLevel: '0',
collateralReleaseLevel: '0',
market: {
__typename: 'Market',
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
},
asset: {
__typename: 'Asset',
id: 'tDAI-id',
},
},
{
__typename: 'MarginLevels',
maintenanceLevel: '0',
searchLevel: '0',
initialLevel: '0',
collateralReleaseLevel: '0',
market: {
__typename: 'Market',
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
},
asset: {
__typename: 'Asset',
id: 'tDAI-id',
},
},
];
describe('getMetrics && rejoinPositionData', () => {
it('returns positions metrics', () => {
const positionsRejoined = rejoinPositionData(
positions,
marketsData,
margins
);
const positionsRejoined = rejoinPositionData(positions, marketsData);
const metrics = getMetrics(positionsRejoined, accounts || null);
expect(metrics.length).toEqual(2);
});
it('calculates metrics', () => {
const positionsRejoined = rejoinPositionData(
positions,
marketsData,
margins
);
const positionsRejoined = rejoinPositionData(positions, marketsData);
const metrics = getMetrics(positionsRejoined, accounts || null);
expect(metrics[0].assetSymbol).toEqual('tDAI');
expect(metrics[0].averageEntryPrice).toEqual('8993727');
expect(metrics[0].capitalUtilisation).toEqual(4);
expect(metrics[0].currentLeverage).toBeCloseTo(1.02);
expect(metrics[0].marketDecimalPlaces).toEqual(5);
expect(metrics[0].positionDecimalPlaces).toEqual(0);
expect(metrics[0].decimals).toEqual(5);
expect(metrics[0].lowMarginLevel).toEqual(false);
expect(metrics[0].markPrice).toEqual('9431775');
expect(metrics[0].marketId).toEqual(
'5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8'
@ -225,7 +190,6 @@ describe('getMetrics && rejoinPositionData', () => {
expect(metrics[0].notional).toEqual('943177500');
expect(metrics[0].openVolume).toEqual('100');
expect(metrics[0].realisedPNL).toEqual('0');
expect(metrics[0].searchPrice).toEqual('9098238');
expect(metrics[0].totalBalance).toEqual('926178496');
expect(metrics[0].unrealisedPNL).toEqual('43804770');
expect(metrics[0].updatedAt).toEqual('2022-07-28T14:53:54.725477Z');
@ -236,12 +200,10 @@ describe('getMetrics && rejoinPositionData', () => {
expect(metrics[1].assetSymbol).toEqual('tDAI');
expect(metrics[1].averageEntryPrice).toEqual('840158');
expect(metrics[1].capitalUtilisation).toEqual(0);
expect(metrics[1].currentLeverage).toBeCloseTo(0.097);
expect(metrics[1].marketDecimalPlaces).toEqual(5);
expect(metrics[1].positionDecimalPlaces).toEqual(0);
expect(metrics[1].decimals).toEqual(5);
expect(metrics[1].lowMarginLevel).toEqual(false);
expect(metrics[1].markPrice).toEqual('869762');
expect(metrics[1].marketId).toEqual(
'10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e'
@ -251,7 +213,6 @@ describe('getMetrics && rejoinPositionData', () => {
expect(metrics[1].notional).toEqual('86976200');
expect(metrics[1].openVolume).toEqual('-100');
expect(metrics[1].realisedPNL).toEqual('0');
expect(metrics[1].searchPrice).toEqual('902503');
expect(metrics[1].totalBalance).toEqual('896098819');
expect(metrics[1].unrealisedPNL).toEqual('-9112700');
expect(metrics[1].updatedAt).toEqual('2022-07-28T15:09:34.441143Z');

View File

@ -19,66 +19,41 @@ import type {
PositionsQuery,
PositionFieldsFragment,
PositionsSubscriptionSubscription,
MarginFieldsFragment,
PositionsQueryVariables,
PositionsSubscriptionSubscriptionVariables,
} from './__generated__/Positions';
import {
PositionsDocument,
PositionsSubscriptionDocument,
} from './__generated__/Positions';
import { marginsDataProvider } from './margin-data-provider';
import type { PositionStatus } from '@vegaprotocol/types';
type PositionMarginLevel = Pick<
MarginFieldsFragment,
'maintenanceLevel' | 'searchLevel' | 'initialLevel'
>;
interface PositionRejoined {
realisedPNL: string;
openVolume: string;
unrealisedPNL: string;
averageEntryPrice: string;
updatedAt?: string | null;
market: MarketMaybeWithData | null;
margins: PositionMarginLevel | null;
lossSocializationAmount: string | null;
status: PositionStatus;
}
export interface Position {
marketName: string;
assetId: string;
assetSymbol: string;
averageEntryPrice: string;
marginAccountBalance: string;
capitalUtilisation: number;
currentLeverage: number | undefined;
decimals: number;
lossSocializationAmount: string;
marginAccountBalance: string;
marketDecimalPlaces: number;
positionDecimalPlaces: number;
totalBalance: string;
assetSymbol: string;
assetId: string;
lowMarginLevel: boolean;
marketId: string;
marketName: string;
marketTradingMode: Schema.MarketTradingMode;
markPrice: string | undefined;
notional: string | undefined;
openVolume: string;
partyId: string;
positionDecimalPlaces: number;
realisedPNL: string;
unrealisedPNL: string;
searchPrice: string | undefined;
updatedAt: string | null;
lossSocializationAmount: string;
status: PositionStatus;
}
export interface Data {
party: PositionsQuery['party'] | null;
positions: Position[] | null;
totalBalance: string;
unrealisedPNL: string;
updatedAt: string | null;
}
export const getMetrics = (
data: PositionRejoined[] | null,
data: ReturnType<typeof rejoinPositionData> | null,
accounts: Account[] | null
): Position[] => {
if (!data || !data?.length) {
@ -87,25 +62,32 @@ export const getMetrics = (
const metrics: Position[] = [];
data.forEach((position) => {
const market = position.market;
if (!market) {
return;
}
const marketData = market?.data;
const marginLevel = position.margins;
const marginAccount = accounts?.find((account) => {
return account.market?.id === market?.id;
});
if (!marginAccount || !marginLevel || !market) {
return;
}
const {
decimals,
id: assetId,
symbol: assetSymbol,
} = market.tradableInstrument.instrument.product.settlementAsset;
const generalAccount = accounts?.find(
(account) =>
account.asset.id === marginAccount.asset.id &&
account.asset.id === assetId &&
account.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL
);
const decimals = marginAccount.asset.decimals;
const { positionDecimalPlaces, decimalPlaces: marketDecimalPlaces } =
market;
const openVolume = toBigNum(position.openVolume, positionDecimalPlaces);
const marginAccountBalance = toBigNum(marginAccount.balance ?? 0, decimals);
const marginAccountBalance = toBigNum(
marginAccount?.balance ?? 0,
decimals
);
const generalAccountBalance = toBigNum(
generalAccount?.balance ?? 0,
decimals
@ -126,54 +108,30 @@ export const getMetrics = (
? new BigNumber(0)
: notional.dividedBy(totalBalance)
: undefined;
const capitalUtilisation = totalBalance.isEqualTo(0)
? new BigNumber(0)
: marginAccountBalance.dividedBy(totalBalance).multipliedBy(100);
const marginSearch = toBigNum(marginLevel.searchLevel, decimals);
const marginInitial = toBigNum(marginLevel.initialLevel, decimals);
const searchPrice = markPrice
? marginSearch
.minus(marginAccountBalance)
.dividedBy(openVolume)
.plus(markPrice)
: undefined;
const lowMarginLevel =
marginAccountBalance.isLessThan(
marginSearch.plus(marginInitial.minus(marginSearch).dividedBy(2))
) && generalAccountBalance.isLessThan(marginInitial.minus(marginSearch));
metrics.push({
marketName: market.tradableInstrument.instrument.name,
assetId,
assetSymbol,
averageEntryPrice: position.averageEntryPrice,
marginAccountBalance: marginAccount.balance,
capitalUtilisation: Math.round(capitalUtilisation.toNumber()),
currentLeverage: currentLeverage ? currentLeverage.toNumber() : undefined,
marketDecimalPlaces,
positionDecimalPlaces,
decimals,
assetSymbol:
market.tradableInstrument.instrument.product.settlementAsset.symbol,
assetId: market.tradableInstrument.instrument.product.settlementAsset.id,
totalBalance: totalBalance.multipliedBy(10 ** decimals).toFixed(),
lowMarginLevel,
lossSocializationAmount: position.lossSocializationAmount || '0',
marginAccountBalance: marginAccount?.balance ?? '0',
marketDecimalPlaces,
marketId: market.id,
marketName: market.tradableInstrument.instrument.name,
marketTradingMode: market.tradingMode,
markPrice: marketData ? marketData.markPrice : undefined,
notional: notional
? notional.multipliedBy(10 ** marketDecimalPlaces).toFixed(0)
: undefined,
openVolume: position.openVolume,
partyId: position.party.id,
positionDecimalPlaces,
realisedPNL: position.realisedPNL,
status: position.positionStatus,
totalBalance: totalBalance.multipliedBy(10 ** decimals).toFixed(),
unrealisedPNL: position.unrealisedPNL,
searchPrice: searchPrice
? searchPrice.multipliedBy(10 ** marketDecimalPlaces).toFixed(0)
: undefined,
updatedAt: position.updatedAt || null,
lossSocializationAmount: position.lossSocializationAmount || '0',
status: position.status,
});
});
return metrics;
@ -186,7 +144,8 @@ export const update = (
return produce(data || [], (draft) => {
deltas.forEach((delta) => {
const index = draft.findIndex(
(node) => node.market.id === delta.marketId
(node) =>
node.market.id === delta.marketId && node.party.id === delta.partyId
);
if (index !== -1) {
const currNode = draft[index];
@ -208,49 +167,39 @@ export const update = (
__typename: 'Market',
id: delta.marketId,
},
party: {
id: delta.partyId,
},
});
}
});
});
};
export const positionsDataProvider = makeDataProvider<
const getSubscriptionVariables = (
variables: PositionsQueryVariables
): PositionsSubscriptionSubscriptionVariables[] =>
([] as string[]).concat(variables.partyIds).map((partyId) => ({ partyId }));
const positionsDataProvider = makeDataProvider<
PositionsQuery,
PositionFieldsFragment[],
PositionsSubscriptionSubscription,
PositionsSubscriptionSubscription['positions'],
PositionsQueryVariables
PositionsQueryVariables,
PositionsSubscriptionSubscriptionVariables
>({
query: PositionsDocument,
subscriptionQuery: PositionsSubscriptionDocument,
update,
getData: (responseData: PositionsQuery | null) =>
removePaginationWrapper(responseData?.party?.positionsConnection?.edges) ||
[],
removePaginationWrapper(responseData?.positions?.edges) || [],
getDelta: (subscriptionData: PositionsSubscriptionSubscription) =>
subscriptionData.positions,
getSubscriptionVariables,
});
const upgradeMarginsConnection = (
marketId: string,
margins: MarginFieldsFragment[] | null
) => {
if (marketId && margins) {
const index =
margins.findIndex((node) => node.market.id === marketId) ?? -1;
if (index >= 0) {
const marginLevel = margins[index];
return {
maintenanceLevel: marginLevel.maintenanceLevel,
searchLevel: marginLevel.searchLevel,
initialLevel: marginLevel.initialLevel,
};
}
}
return null;
};
export const positionDataProvider = makeDerivedDataProvider<
const positionDataProvider = makeDerivedDataProvider<
PositionFieldsFragment,
never,
PositionsQueryVariables & MarketDataQueryVariables
@ -258,7 +207,7 @@ export const positionDataProvider = makeDerivedDataProvider<
[
(callback, client, variables) =>
positionsDataProvider(callback, client, {
partyId: variables?.partyId || '',
partyIds: variables.partyIds,
}),
],
(data, variables) =>
@ -278,22 +227,18 @@ export const openVolumeDataProvider = makeDerivedDataProvider<
export const rejoinPositionData = (
positions: PositionFieldsFragment[] | null,
marketsData: MarketMaybeWithData[] | null,
margins: MarginFieldsFragment[] | null
): PositionRejoined[] | null => {
if (positions && marketsData && margins) {
marketsData: MarketMaybeWithData[] | null
):
| (Omit<PositionFieldsFragment, 'market'> & {
market: MarketMaybeWithData | null;
})[]
| null => {
if (positions && marketsData) {
return positions.map((node) => {
return {
realisedPNL: node.realisedPNL,
openVolume: node.openVolume,
unrealisedPNL: node.unrealisedPNL,
averageEntryPrice: node.averageEntryPrice,
updatedAt: node.updatedAt,
...node,
market:
marketsData?.find((market) => market.id === node.market.id) || null,
margins: upgradeMarginsConnection(node.market.id, margins),
lossSocializationAmount: node.lossSocializationAmount,
status: node.positionStatus,
};
});
}
@ -307,13 +252,17 @@ export const positionsMetricsProvider = makeDerivedDataProvider<
>(
[
positionsDataProvider,
accountsDataProvider,
(callback, client, variables) =>
accountsDataProvider(callback, client, {
partyId: Array.isArray(variables.partyIds)
? variables.partyIds[0]
: variables.partyIds,
}),
(callback, client) =>
allMarketsWithDataProvider(callback, client, undefined),
marginsDataProvider,
],
([positions, accounts, marketsData, margins], variables) => {
const positionsData = rejoinPositionData(positions, marketsData, margins);
([positions, accounts, marketsData], variables) => {
const positionsData = rejoinPositionData(positions, marketsData);
if (!variables) {
return [];
}

View File

@ -1,6 +1,5 @@
import { useCallback, useRef, useState } from 'react';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import type { Position } from './positions-data-providers';
import { usePositionsData } from './use-positions-data';
import { PositionsTable } from './positions-table';
import type { AgGridReact } from 'ag-grid-react';
@ -8,9 +7,10 @@ import * as Schema from '@vegaprotocol/types';
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
import { t } from '@vegaprotocol/i18n';
import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
import { useVegaWallet } from '@vegaprotocol/wallet';
interface PositionsManagerProps {
partyId: string;
partyIds: string[];
onMarketClick?: (marketId: string) => void;
isReadOnly: boolean;
noBottomPlaceholder?: boolean;
@ -18,14 +18,15 @@ interface PositionsManagerProps {
}
export const PositionsManager = ({
partyId,
partyIds,
onMarketClick,
isReadOnly,
noBottomPlaceholder,
storeKey,
}: PositionsManagerProps) => {
const { pubKeys, pubKey } = useVegaWallet();
const gridRef = useRef<AgGridReact | null>(null);
const { data, error, loading, reload } = usePositionsData(partyId, gridRef);
const { data, error, loading, reload } = usePositionsData(partyIds, gridRef);
const [dataCount, setDataCount] = useState(data?.length ?? 0);
const create = useVegaTransactionStore((store) => store.create);
const onClose = ({
@ -58,23 +59,19 @@ export const PositionsManager = ({
},
});
const setId = useCallback((data: Position, id: string) => {
return {
...data,
marketId: id,
};
}, []);
const bottomPlaceholderProps = useBottomPlaceholder<Position>({
const bottomPlaceholderProps = useBottomPlaceholder({
gridRef,
setId,
disabled: noBottomPlaceholder,
});
const updateRowCount = useCallback(() => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, []);
return (
<div className="h-full relative">
<PositionsTable
pubKey={pubKey}
pubKeys={pubKeys}
rowData={error ? [] : data}
ref={gridRef}
onMarketClick={onMarketClick}
@ -86,6 +83,7 @@ export const PositionsManager = ({
onRowDataUpdated={updateRowCount}
{...bottomPlaceholderProps}
storeKey={storeKey}
multipleKeys={partyIds.length > 1}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer

View File

@ -8,29 +8,27 @@ import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import type { ICellRendererParams } from 'ag-grid-community';
const singleRow: Position = {
marketName: 'ETH/BTC (31 july 2022)',
averageEntryPrice: '133',
capitalUtilisation: 11,
currentLeverage: 1.1,
marketDecimalPlaces: 1,
positionDecimalPlaces: 0,
decimals: 2,
totalBalance: '123456',
assetSymbol: 'BTC',
partyId: 'partyId',
assetId: 'asset-id',
lowMarginLevel: false,
assetSymbol: 'BTC',
averageEntryPrice: '133',
currentLeverage: 1.1,
decimals: 2,
lossSocializationAmount: '0',
marginAccountBalance: '12345600',
marketDecimalPlaces: 1,
marketId: 'string',
marketName: 'ETH/BTC (31 july 2022)',
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
markPrice: '123',
notional: '12300',
openVolume: '100',
positionDecimalPlaces: 0,
realisedPNL: '123',
unrealisedPNL: '456',
searchPrice: '0',
updatedAt: '2022-07-27T15:02:58.400Z',
marginAccountBalance: '12345600',
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
lossSocializationAmount: '0',
totalBalance: '123456',
unrealisedPNL: '456',
updatedAt: '2022-07-27T15:02:58.400Z',
};
const singleRowData = [singleRow];
@ -175,6 +173,7 @@ it('displays close button', async () => {
render(
<PositionsTable
rowData={singleRowData}
pubKey={singleRowData[0].partyId}
onClose={() => {
return;
}}

View File

@ -15,69 +15,51 @@ const Template: Story = (args) => (
export const Primary = Template.bind({});
const longPosition: Position = {
marketName: 'BTC/USD (31 july 2022)',
assetId: 'BTC',
assetSymbol: 'BTC',
averageEntryPrice: '1134564',
capitalUtilisation: 10,
currentLeverage: 11,
decimals: 2,
marketDecimalPlaces: 2,
positionDecimalPlaces: 2,
// generalAccountBalance: '0',
totalBalance: '45353',
assetSymbol: 'BTC',
// leverageInitial: '0',
// leverageMaintenance: '0',
// leverageRelease: '0',
// leverageSearch: '0',
lowMarginLevel: false,
lossSocializationAmount: '0',
marginAccountBalance: new BigNumber('0').toString(),
// marginMaintenance: '0',
// marginSearch: '0',
// marginInitial: '0',
marketDecimalPlaces: 2,
marketId: 'marketId1',
marketName: 'BTC/USD (31 july 2022)',
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
markPrice: '1131894',
notional: '46667989',
openVolume: '4123',
positionDecimalPlaces: 2,
realisedPNL: '45',
unrealisedPNL: '45',
searchPrice: '1132123',
updatedAt: '2022-07-27T15:02:58.400Z',
lossSocializationAmount: '0',
status: Schema.PositionStatus.POSITION_STATUS_UNSPECIFIED,
totalBalance: '45353',
unrealisedPNL: '45',
updatedAt: '2022-07-27T15:02:58.400Z',
partyId: 'partyId',
};
const shortPosition: Position = {
marketName: 'ETH/USD (31 august 2022)',
assetId: 'ETH',
assetSymbol: 'ETH',
averageEntryPrice: '23976',
capitalUtilisation: 87,
currentLeverage: 7,
decimals: 2,
marketDecimalPlaces: 2,
positionDecimalPlaces: 2,
// generalAccountBalance: '0',
totalBalance: '3856',
assetSymbol: 'ETH',
// leverageInitial: '0',
// leverageMaintenance: '0',
// leverageRelease: '0',
// leverageSearch: '0',
lowMarginLevel: false,
lossSocializationAmount: '0',
marginAccountBalance: new BigNumber('0').toString(),
// marginMaintenance: '0',
// marginSearch: '0',
// marginInitial: '0',
marketDecimalPlaces: 2,
marketId: 'marketId2',
marketName: 'ETH/USD (31 august 2022)',
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
markPrice: '24123',
notional: '836344',
openVolume: '-3467',
positionDecimalPlaces: 2,
realisedPNL: '0',
unrealisedPNL: '0',
searchPrice: '0',
updatedAt: '2022-07-26T14:01:34.800Z',
lossSocializationAmount: '0',
status: Schema.PositionStatus.POSITION_STATUS_UNSPECIFIED,
totalBalance: '3856',
unrealisedPNL: '0',
updatedAt: '2022-07-26T14:01:34.800Z',
partyId: 'partyId',
};
Primary.args = {

View File

@ -42,6 +42,7 @@ import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import { DocsLinks } from '@vegaprotocol/environment';
import { PositionTableActions } from './position-actions-dropdown';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
interface Props extends TypedDataAgGrid<Position> {
onClose?: (data: Position) => void;
@ -49,6 +50,9 @@ interface Props extends TypedDataAgGrid<Position> {
style?: CSSProperties;
isReadOnly: boolean;
storeKey?: string;
multipleKeys?: boolean;
pubKeys?: VegaWalletContextShape['pubKeys'];
pubKey?: VegaWalletContextShape['pubKey'];
}
export interface AmountCellProps {
@ -83,7 +87,18 @@ export const AmountCell = ({ valueFormatted }: AmountCellProps) => {
AmountCell.displayName = 'AmountCell';
export const PositionsTable = forwardRef<AgGridReact, Props>(
({ onClose, onMarketClick, ...props }, ref) => {
(
{
onClose,
onMarketClick,
multipleKeys,
isReadOnly,
pubKeys,
pubKey,
...props
},
ref
) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return (
<AgGrid
@ -107,6 +122,21 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}}
{...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"
@ -267,6 +297,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}}
minWidth={100}
/>
{multipleKeys ? null : (
<AgGridColumn
headerName={t('Leverage')}
field="currentLeverage"
@ -280,10 +311,14 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
valueFormatter={({
value,
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined ? undefined : formatNumber(value.toString(), 1)
value === undefined
? undefined
: formatNumber(value.toString(), 1)
}
minWidth={100}
/>
)}
{multipleKeys ? null : (
<AgGridColumn
headerName={t('Margin allocated')}
field="marginAccountBalance"
@ -316,6 +351,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}}
minWidth={100}
/>
)}
<AgGridColumn
headerName={t('Realised PNL')}
field="realisedPNL"
@ -385,13 +421,15 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}}
minWidth={150}
/>
{onClose && !props.isReadOnly ? (
{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?.openVolume &&
data?.openVolume !== '0' &&
data.partyId === pubKey ? (
<ButtonLink
data-testid="close-position"
onClick={() => data && onClose(data)}

View File

@ -12,17 +12,13 @@ export const positionsQuery = (
override?: PartialDeep<PositionsQuery>
): PositionsQuery => {
const defaultResult: PositionsQuery = {
party: {
__typename: 'Party',
id: 'vega-0', // VEGA PUBLIC KEY
positionsConnection: {
positions: {
__typename: 'PositionConnection',
edges: positionFields.map((node) => ({
__typename: 'PositionEdge',
node,
})),
},
},
};
return merge(defaultResult, override);
@ -59,6 +55,10 @@ const positionFields: PositionFieldsFragment[] = [
id: 'market-0',
__typename: 'Market',
},
party: {
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
__typename: 'Party',
},
lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},
@ -73,6 +73,10 @@ const positionFields: PositionFieldsFragment[] = [
id: 'market-1',
__typename: 'Market',
},
party: {
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
__typename: 'Party',
},
lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},
@ -87,6 +91,10 @@ const positionFields: PositionFieldsFragment[] = [
id: 'market-2',
__typename: 'Market',
},
party: {
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
__typename: 'Party',
},
lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},

View File

@ -14,7 +14,7 @@ export const useOpenVolume = (
useDataProvider({
dataProvider: openVolumeDataProvider,
update,
variables: { partyId: partyId || '', marketId },
variables: { partyIds: partyId ? [partyId] : [], marketId },
skip: !partyId,
});
return openVolume;

View File

@ -59,7 +59,7 @@ describe('usePositionData Hook', () => {
.mockImplementation((id: string) =>
mockData.find((position) => position.marketId === id)
);
const partyId = 'partyId';
const partyIds = ['partyId'];
const anUpdatedOne = {
marketId: 'market-1',
openVolume: '1',
@ -75,14 +75,14 @@ describe('usePositionData Hook', () => {
};
it('should return proper data', async () => {
const { result } = renderHook(() => usePositionsData(partyId, gridRef), {
const { result } = renderHook(() => usePositionsData(partyIds, gridRef), {
wrapper: MockedProvider,
});
expect(result.current.data?.length ?? 0).toEqual(5);
});
it('should call mockRefreshInfiniteCache', async () => {
renderHook(() => usePositionsData(partyId, gridRef), {
renderHook(() => usePositionsData(partyIds, gridRef), {
wrapper: MockedProvider,
});
await waitFor(() => {
@ -99,7 +99,7 @@ describe('usePositionData Hook', () => {
data: mockData,
loading: false,
};
const { result } = renderHook(() => usePositionsData(partyId, gridRef), {
const { result } = renderHook(() => usePositionsData(partyIds, gridRef), {
wrapper: MockedProvider,
});
expect(result.current.data).toEqual([]);

View File

@ -9,15 +9,22 @@ import { useDataProvider } from '@vegaprotocol/data-provider';
import type { GetRowsParams } from '@vegaprotocol/datagrid';
import isEqual from 'lodash/isEqual';
export const getRowId = ({ data }: { data: Position }) => data.marketId;
export const getRowId = ({
data,
}: {
data: Position & { isLastPlaceholder?: boolean; id?: string };
}) =>
data.isLastPlaceholder && data.id
? data.id
: `${data.partyId}-${data.marketId}`;
export const usePositionsData = (
partyId: string,
partyIds: string[],
gridRef: RefObject<AgGridReact>
) => {
const variables = useMemo<PositionsQueryVariables>(
() => ({ partyId }),
[partyId]
() => ({ partyIds }),
[partyIds]
);
const dataRef = useRef<Position[] | null>(null);
const update = useCallback(
@ -28,14 +35,16 @@ export const usePositionsData = (
const update: Position[] = [];
const add: Position[] = [];
data?.forEach((d) => {
const rowNode = gridRef.current?.api?.getRowNode(d.marketId);
data?.forEach((row) => {
const rowNode = gridRef.current?.api?.getRowNode(
getRowId({ data: row })
);
if (rowNode) {
if (!isEqual(rowNode.data, d)) {
update.push(d);
if (!isEqual(rowNode.data, row)) {
update.push(row);
}
} else {
add.push(d);
add.push(row);
}
});
gridRef.current?.api?.applyTransaction({