2022-08-16 07:18:55 +00:00
|
|
|
import { gql } from '@apollo/client';
|
|
|
|
import produce from 'immer';
|
|
|
|
import BigNumber from 'bignumber.js';
|
|
|
|
import sortBy from 'lodash/sortBy';
|
2022-08-26 15:39:40 +00:00
|
|
|
import type { Accounts_party_accounts } from '@vegaprotocol/accounts';
|
|
|
|
import { accountsDataProvider } from '@vegaprotocol/accounts';
|
|
|
|
import { toBigNum } from '@vegaprotocol/react-helpers';
|
|
|
|
import type { Positions, Positions_party } from './__generated__/Positions';
|
|
|
|
import {
|
|
|
|
makeDataProvider,
|
|
|
|
makeDerivedDataProvider,
|
|
|
|
} from '@vegaprotocol/react-helpers';
|
2022-08-16 07:18:55 +00:00
|
|
|
|
|
|
|
import type {
|
2022-08-26 15:39:40 +00:00
|
|
|
PositionsSubscription,
|
|
|
|
PositionsSubscription_positions,
|
|
|
|
} from './__generated__/PositionsSubscription';
|
2022-08-16 07:18:55 +00:00
|
|
|
|
|
|
|
import { AccountType } from '@vegaprotocol/types';
|
|
|
|
import type { MarketTradingMode } from '@vegaprotocol/types';
|
|
|
|
|
|
|
|
export interface Position {
|
|
|
|
marketName: string;
|
|
|
|
averageEntryPrice: string;
|
|
|
|
capitalUtilisation: number;
|
|
|
|
currentLeverage: number;
|
2022-09-02 20:53:00 +00:00
|
|
|
decimals: number;
|
2022-08-16 07:18:55 +00:00
|
|
|
marketDecimalPlaces: number;
|
|
|
|
positionDecimalPlaces: number;
|
|
|
|
totalBalance: string;
|
|
|
|
assetSymbol: string;
|
|
|
|
liquidationPrice: string;
|
|
|
|
lowMarginLevel: boolean;
|
|
|
|
marketId: string;
|
|
|
|
marketTradingMode: MarketTradingMode;
|
|
|
|
markPrice: string;
|
|
|
|
notional: string;
|
|
|
|
openVolume: string;
|
|
|
|
realisedPNL: string;
|
|
|
|
unrealisedPNL: string;
|
|
|
|
searchPrice: string;
|
|
|
|
updatedAt: string | null;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface Data {
|
2022-08-26 15:39:40 +00:00
|
|
|
party: Positions_party | null;
|
2022-08-16 07:18:55 +00:00
|
|
|
positions: Position[] | null;
|
|
|
|
}
|
|
|
|
|
2022-08-26 15:39:40 +00:00
|
|
|
const POSITION_FIELDS = gql`
|
|
|
|
fragment PositionFields on Position {
|
2022-08-16 07:18:55 +00:00
|
|
|
realisedPNL
|
|
|
|
openVolume
|
|
|
|
unrealisedPNL
|
|
|
|
averageEntryPrice
|
|
|
|
updatedAt
|
2022-08-26 15:39:40 +00:00
|
|
|
marginsConnection {
|
|
|
|
edges {
|
|
|
|
node {
|
|
|
|
market {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
maintenanceLevel
|
|
|
|
searchLevel
|
|
|
|
initialLevel
|
|
|
|
collateralReleaseLevel
|
|
|
|
asset {
|
|
|
|
symbol
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-08-16 07:18:55 +00:00
|
|
|
market {
|
|
|
|
id
|
|
|
|
decimalPlaces
|
|
|
|
positionDecimalPlaces
|
|
|
|
tradingMode
|
|
|
|
tradableInstrument {
|
|
|
|
instrument {
|
|
|
|
name
|
|
|
|
}
|
|
|
|
}
|
|
|
|
data {
|
|
|
|
markPrice
|
2022-08-26 15:39:40 +00:00
|
|
|
market {
|
|
|
|
id
|
|
|
|
}
|
2022-08-16 07:18:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
2022-08-26 15:39:40 +00:00
|
|
|
export const POSITIONS_QUERY = gql`
|
|
|
|
${POSITION_FIELDS}
|
|
|
|
query Positions($partyId: ID!) {
|
2022-08-16 07:18:55 +00:00
|
|
|
party(id: $partyId) {
|
|
|
|
id
|
|
|
|
positionsConnection {
|
|
|
|
edges {
|
|
|
|
node {
|
2022-08-26 15:39:40 +00:00
|
|
|
...PositionFields
|
2022-08-16 07:18:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
2022-08-26 15:39:40 +00:00
|
|
|
export const POSITIONS_SUBSCRIPTION = gql`
|
|
|
|
${POSITION_FIELDS}
|
|
|
|
subscription PositionsSubscription($partyId: ID!) {
|
2022-08-16 07:18:55 +00:00
|
|
|
positions(partyId: $partyId) {
|
2022-08-26 15:39:40 +00:00
|
|
|
...PositionFields
|
2022-08-16 07:18:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
2022-08-26 15:39:40 +00:00
|
|
|
export const getMetrics = (
|
|
|
|
data: Positions_party | null,
|
|
|
|
accounts: Accounts_party_accounts[] | null
|
|
|
|
): Position[] => {
|
|
|
|
if (!data || !data?.positionsConnection.edges) {
|
2022-08-16 07:18:55 +00:00
|
|
|
return [];
|
|
|
|
}
|
|
|
|
const metrics: Position[] = [];
|
2022-08-26 15:39:40 +00:00
|
|
|
data?.positionsConnection.edges.forEach((position) => {
|
2022-08-16 07:18:55 +00:00
|
|
|
const market = position.node.market;
|
|
|
|
const marketData = market.data;
|
2022-08-26 15:39:40 +00:00
|
|
|
const marginLevel = position.node.marginsConnection.edges?.find(
|
2022-08-16 07:18:55 +00:00
|
|
|
(margin) => margin.node.market.id === market.id
|
|
|
|
)?.node;
|
2022-08-26 15:39:40 +00:00
|
|
|
const marginAccount = accounts?.find((account) => {
|
|
|
|
return account.market?.id === market.id;
|
|
|
|
});
|
2022-09-02 20:53:00 +00:00
|
|
|
if (
|
|
|
|
!marginAccount ||
|
|
|
|
!marginLevel ||
|
|
|
|
!marketData ||
|
|
|
|
position.node.openVolume === '0'
|
|
|
|
) {
|
2022-08-16 07:18:55 +00:00
|
|
|
return;
|
|
|
|
}
|
2022-08-26 15:39:40 +00:00
|
|
|
const generalAccount = accounts?.find(
|
2022-08-16 07:18:55 +00:00
|
|
|
(account) =>
|
|
|
|
account.asset.id === marginAccount.asset.id &&
|
2022-08-22 22:50:13 +00:00
|
|
|
account.type === AccountType.ACCOUNT_TYPE_GENERAL
|
2022-08-16 07:18:55 +00:00
|
|
|
);
|
2022-09-02 20:53:00 +00:00
|
|
|
const decimals = marginAccount.asset.decimals;
|
2022-08-16 07:18:55 +00:00
|
|
|
const { positionDecimalPlaces, decimalPlaces: marketDecimalPlaces } =
|
|
|
|
market;
|
2022-08-26 15:39:40 +00:00
|
|
|
const openVolume = toBigNum(
|
|
|
|
position.node.openVolume,
|
|
|
|
positionDecimalPlaces
|
2022-08-16 07:18:55 +00:00
|
|
|
);
|
|
|
|
|
2022-09-02 20:53:00 +00:00
|
|
|
const marginAccountBalance = toBigNum(marginAccount.balance ?? 0, decimals);
|
2022-08-26 15:39:40 +00:00
|
|
|
const generalAccountBalance = toBigNum(
|
|
|
|
generalAccount?.balance ?? 0,
|
2022-09-02 20:53:00 +00:00
|
|
|
decimals
|
2022-08-16 07:18:55 +00:00
|
|
|
);
|
2022-08-26 15:39:40 +00:00
|
|
|
const markPrice = toBigNum(marketData.markPrice, marketDecimalPlaces);
|
2022-08-16 07:18:55 +00:00
|
|
|
|
|
|
|
const notional = (
|
|
|
|
openVolume.isGreaterThan(0) ? openVolume : openVolume.multipliedBy(-1)
|
|
|
|
).multipliedBy(markPrice);
|
|
|
|
const totalBalance = marginAccountBalance.plus(generalAccountBalance);
|
|
|
|
const currentLeverage = totalBalance.isEqualTo(0)
|
|
|
|
? new BigNumber(0)
|
|
|
|
: notional.dividedBy(totalBalance);
|
|
|
|
const capitalUtilisation = totalBalance.isEqualTo(0)
|
|
|
|
? new BigNumber(0)
|
|
|
|
: marginAccountBalance.dividedBy(totalBalance).multipliedBy(100);
|
|
|
|
|
2022-08-26 15:39:40 +00:00
|
|
|
const marginMaintenance = toBigNum(
|
|
|
|
marginLevel.maintenanceLevel,
|
2022-08-16 07:18:55 +00:00
|
|
|
marketDecimalPlaces
|
|
|
|
);
|
2022-08-26 15:39:40 +00:00
|
|
|
const marginSearch = toBigNum(marginLevel.searchLevel, marketDecimalPlaces);
|
|
|
|
const marginInitial = toBigNum(
|
|
|
|
marginLevel.initialLevel,
|
2022-08-16 07:18:55 +00:00
|
|
|
marketDecimalPlaces
|
|
|
|
);
|
|
|
|
|
2022-09-02 20:53:00 +00:00
|
|
|
const searchPrice = marginSearch
|
|
|
|
.minus(marginAccountBalance)
|
|
|
|
.dividedBy(openVolume)
|
|
|
|
.plus(markPrice);
|
|
|
|
|
|
|
|
const liquidationPrice = BigNumber.maximum(
|
|
|
|
0,
|
|
|
|
marginMaintenance
|
|
|
|
.minus(marginAccountBalance)
|
|
|
|
.minus(generalAccountBalance)
|
|
|
|
.dividedBy(openVolume)
|
|
|
|
.plus(markPrice)
|
|
|
|
);
|
2022-08-16 07:18:55 +00:00
|
|
|
|
|
|
|
const lowMarginLevel =
|
|
|
|
marginAccountBalance.isLessThan(
|
|
|
|
marginSearch.plus(marginInitial.minus(marginSearch).dividedBy(2))
|
|
|
|
) && generalAccountBalance.isLessThan(marginInitial.minus(marginSearch));
|
|
|
|
|
|
|
|
metrics.push({
|
2022-09-07 18:37:39 +00:00
|
|
|
marketName: market.tradableInstrument.instrument.name,
|
2022-08-16 07:18:55 +00:00
|
|
|
averageEntryPrice: position.node.averageEntryPrice,
|
|
|
|
capitalUtilisation: Math.round(capitalUtilisation.toNumber()),
|
|
|
|
currentLeverage: currentLeverage.toNumber(),
|
|
|
|
marketDecimalPlaces,
|
|
|
|
positionDecimalPlaces,
|
2022-09-02 20:53:00 +00:00
|
|
|
decimals,
|
2022-08-16 07:18:55 +00:00
|
|
|
assetSymbol: marginLevel.asset.symbol,
|
2022-09-02 20:53:00 +00:00
|
|
|
totalBalance: totalBalance.multipliedBy(10 ** decimals).toFixed(),
|
2022-08-16 07:18:55 +00:00
|
|
|
lowMarginLevel,
|
|
|
|
liquidationPrice: liquidationPrice
|
|
|
|
.multipliedBy(10 ** marketDecimalPlaces)
|
|
|
|
.toFixed(0),
|
|
|
|
marketId: position.node.market.id,
|
|
|
|
marketTradingMode: position.node.market.tradingMode,
|
|
|
|
markPrice: marketData.markPrice,
|
|
|
|
notional: notional.multipliedBy(10 ** marketDecimalPlaces).toFixed(0),
|
|
|
|
openVolume: position.node.openVolume,
|
|
|
|
realisedPNL: position.node.realisedPNL,
|
|
|
|
unrealisedPNL: position.node.unrealisedPNL,
|
|
|
|
searchPrice: searchPrice
|
|
|
|
.multipliedBy(10 ** marketDecimalPlaces)
|
|
|
|
.toFixed(0),
|
|
|
|
updatedAt: position.node.updatedAt,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return metrics;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const update = (
|
2022-08-26 15:39:40 +00:00
|
|
|
data: Positions_party,
|
|
|
|
delta: PositionsSubscription_positions | null
|
2022-08-16 07:18:55 +00:00
|
|
|
) => {
|
2022-08-26 15:39:40 +00:00
|
|
|
return produce(data, (draft) => {
|
|
|
|
if (!draft.positionsConnection.edges || !delta) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const index = draft.positionsConnection.edges.findIndex(
|
2022-08-16 07:18:55 +00:00
|
|
|
(edge) => edge.node.market.id === delta.market.id
|
|
|
|
);
|
|
|
|
if (index !== -1) {
|
2022-08-26 15:39:40 +00:00
|
|
|
draft.positionsConnection.edges[index].node = delta;
|
2022-08-16 07:18:55 +00:00
|
|
|
} else {
|
2022-08-26 15:39:40 +00:00
|
|
|
draft.positionsConnection.edges.push({
|
|
|
|
__typename: 'PositionEdge',
|
|
|
|
node: delta,
|
|
|
|
});
|
2022-08-16 07:18:55 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2022-08-26 15:39:40 +00:00
|
|
|
export const positionDataProvider = makeDataProvider<
|
|
|
|
Positions,
|
|
|
|
Positions_party,
|
|
|
|
PositionsSubscription,
|
|
|
|
PositionsSubscription_positions
|
|
|
|
>({
|
|
|
|
query: POSITIONS_QUERY,
|
|
|
|
subscriptionQuery: POSITIONS_SUBSCRIPTION,
|
2022-08-16 07:18:55 +00:00
|
|
|
update,
|
2022-08-26 15:39:40 +00:00
|
|
|
getData: (responseData: Positions) => responseData.party,
|
|
|
|
getDelta: (subscriptionData: PositionsSubscription) =>
|
|
|
|
subscriptionData.positions,
|
|
|
|
});
|
|
|
|
|
|
|
|
export const positionsMetricsDataProvider = makeDerivedDataProvider<Position[]>(
|
|
|
|
[positionDataProvider, accountsDataProvider],
|
|
|
|
([positions, accounts]) => {
|
|
|
|
return sortBy(
|
|
|
|
getMetrics(
|
|
|
|
positions as Positions_party | null,
|
|
|
|
accounts as Accounts_party_accounts[] | null
|
|
|
|
),
|
|
|
|
'updatedAt'
|
|
|
|
).reverse();
|
|
|
|
}
|
2022-08-16 07:18:55 +00:00
|
|
|
);
|